Comunicación con el servidor en Angular#
En Javascript vanilla podemos usar fetch
para comunicarnos con el servidor o librerias de terceros como Axios
o JQuery
. En Angular no se prohibe usar esas herramientas, pero su ecosistema está más preparado para usar su servicio HTTPClient
porque se integra con los Observables
, los Interceptores
, serialización de datos, gestión de errores y tests unitarios.
Antes de ver cómo usar ese servicio veamos lo que es un servicio.
Servicios en Angular#
En Angular, los servicios son componentes que proporcionan datos y funcionalidades reutilizables a lo largo de la aplicación. Generalmente, los servicios manejan operaciones CRUD (Create, Read, Update, Delete) y permiten mantener la lógica de negocio y la gestión de datos de forma centralizada y persistente.
Provisión de Información: Los servicios proporcionan datos a los componentes que los soliciten.
Operaciones CRUD: Realizan operaciones básicas de creación, lectura, actualización y eliminación.
Persistencia de Datos: Mantienen los datos de manera persistente a través de diferentes componentes.
Reutilizables: Son reutilizables en toda la aplicación, promoviendo un código limpio y modular.
Decorador @Injectable#
Las clases de servicio en Angular están decoradas con @Injectable()
. Este decorador indica al inyector de dependencias de Angular que debe proporcionar una instancia de la clase cuando sea necesario. Aquí hay un ejemplo de una clase de servicio:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Inicia que no hace falta que esté en providers
})
export class ProductService {
// Métodos y lógica del servicio
}
El decorador @Injectable
asegura que Angular gestione la instancia del servicio como un Singleton, lo que significa que se crea una única instancia del servicio y se comparte entre todos los componentes que lo requieran.
Si el servicio se declara con providedIn: 'root'
, no es necesario agregarlo a providers
porque Angular se encargará de su inyección automáticamente en toda la aplicación.
Inyección de Dependencias#
En Angular, la inyección de dependencias (DI) permite a los componentes solicitar servicios de manera eficiente. En lugar de crear instancias de servicios con new
, Angular maneja la creación y provisión de servicios mediante el constructor:
import { Component } from '@angular/core';
import { ProductService } from './product.service';
@Component({
selector: 'app-product',
templateUrl: './product.component.html'
})
export class ProductComponent {
constructor(private productService: ProductService) { }
}
Este enfoque hace que el código sea más legible y fácil de mantener. Además, permite que Angular gestione la creación de servicios como Singletons
, asegurando que todos los componentes utilicen la misma instancia del servicio.
En el ejemplo, se utiliza la inyección de dependencias en el constructor de una clase, en caso de necesitarlo en una función, podemos usar inject
, como en el ejemplo:
const router: Router = inject(Router);
HttpClientModule#
Los servicios en Angular a menudo obtienen datos de un servidor a través de HTTP. Para hacer esto, se debe importar HttpClientModule
:
import { HttpClient, HttpClientModule } from '@angular/common/http';
....
Servicios como Clientes HTTP#
Los servicios pueden utilizar HttpClient
para realizar solicitudes HTTP. Esto se logra mediante la inyección de dependencias. Aquí hay un ejemplo de un servicio que obtiene productos de un servidor:
import { Injectable } from '@angular/core';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Product } from './product.model';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private productURL = 'https://api.example.com/products';
constructor(private http: HttpClient) { }
getProducts(): Observable<Product[]> {
return this.http.get<{products: Product[]}>(this.productURL).pipe(
map(response => response.products)
);
}
}
En este ejemplo, getProducts
realiza una solicitud HTTP GET para obtener una lista de productos. Utiliza map
de RxJS para transformar la respuesta antes de devolverla como un Observable
.
Envío de Datos con POST#
Para enviar datos al servidor, se utiliza el método post
de HttpClient
:
import { HttpClient, HttpHeaders, HttpClientModule } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loginURL = 'https://api.example.com/login';
private httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
constructor(private http: HttpClient) { }
login(credentials: {username: string, password: string}): Observable<{token: string}> {
return this.http.post<{token: string}>(this.loginURL, JSON.stringify(credentials), this.httpOptions);
}
}
En este ejemplo, login
envía credenciales de usuario al servidor utilizando una solicitud HTTP POST. Se configuran las cabeceras HTTP para especificar que el contenido es JSON.
Envio de datos con POST Multipart#
En este caso es más sencillo incluso, ya que es el comportamiento por defecto de HTTPClient
:
export class UploadService {
private apiUrl = 'https://api.ejemplo.com/upload'; // Reemplazar con la URL real
constructor(private http: HttpClient) {}
uploadForm(documento: File, nombre: string, email: string): Observable<any> {
const formData = new FormData();
formData.append('documento', documento); // "documento" es el nombre del campo en el backend
formData.append('nombre', nombre);
formData.append('email', email);
return this.http.post(this.apiUrl, formData);
}
}
Gestión de errores de comunicación#
Los errores al comunicarse con el servidor son por dos causas principales: Error del servidor (400, 500…) o error de la red o de la URL. HTTPClient
genera Observables
que emiten la función error
de los Observers
en caso de fallo. Estos errores hay que tratarlos antes de enviar datos a los componentes que están suscritos al servicio.
getData(url: string): Observable<any> {
return this.http.get(url).pipe(
catchError(this.handleError),
map(response => {
if (!response.success) {
throw new Error(response.message);
}
return response.data;
}),
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
let errorMessage = 'Ocurrió un error desconocido';
if (error.error instanceof ErrorEvent) {
// Error de red o del cliente (URL mal escrita, sin conexión, etc.)
errorMessage = `Error de red: ${error.error.message}`;
} else {
// Error del servidor (códigos 400, 500, etc.)
errorMessage = `Error del servidor ${error.status}: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}
En este caso capturamos el error dentro del pipe
con catchError
y lo tratamos. La función handleError
acepta un HttpErrorResponse
y retorna un Observable<never>
que en TypeScript y RxJS indica que el observable nunca emitirá un valor exitoso.
Aquí se manejan varios tipos específicos de error: HttpErrorResponse
es una respuesta que da el HTTPClient cuando algo no va bien. Esta respuesta siempre la da si falla. Su contenido .error
puede ser ErrorEvent
o otro específico. Si es ErrorEvent
indica que es un error de red. Además, el throwError
emite un Error
que es más genérico.
Algunos servidores envían una respuesta 200, pero con un mensaje en su interior que indica un error. En ese caso, se puede capturar el mensaje con un operador map
y lanzar el error. En el ejemplo, en caso de que no falle la red, pasará al map
que lanzará el error dependiendo del contenido.
Datos asíncronos#
En Angular utiliza la librería RxJS para implementar una versión avanzada de manejo de datos asíncronos conocida como Observables, que ofrece capacidades más robustas en comparación con las Promesas tradicionales de JavaScript.
Promesas vs. Observables#
Aunque se puede trabajar con promesas para obtener datos, Angular utiliza por defecto los Observables de RxJS debido a sus ventajas:
Valores Múltiples: Mientras que una promesa retorna un solo valor o un error, un Observable puede emitir múltiples valores a lo largo del tiempo.
Lazy Loading: Una promesa comienza su ejecución en el momento de su creación, mientras que un Observable sólo empieza a emitir valores cuando alguien se suscribe a él.
Cancelación: Los observables pueden ser cancelados mediante la cancelación de las suscripciones, lo que permite un control más fino sobre el flujo de datos.
Operadores: RxJS proporciona una amplia gama de operadores como
map
,filter
yreduce
que permiten manipular fácilmente los datos emitidos por los observables.
Uso de Operadores en Observables#
Los operadores en RxJS son funciones que permiten transformar, filtrar y combinar flujos de datos de observables. Aquí hay un ejemplo de cómo se utilizan los operadores map
y filter
:
map
: Manipula los datos y los retorna.filter
: Deja pasar sólo los datos que cumplen con una condición específica.
Estos operadores se aplican como parámetros del método pipe
de la clase Observable
.
...
export class ProductService {
private productURL = 'https://api.example.com/products';
constructor(private http: HttpClient) { }
getProducts(): Observable<Product[]> {
return this.http.get<{products: Product[]}>(this.productURL).pipe(
map(response => response.products),
filter(product => product.price > 20)
);
}
}
Procesamiento de Respuestas de Observables#
Un observable puede tener múltiples suscriptores y sólo comienza a emitir datos cuando alguien se suscribe a él. El método subscribe()
acepta tres funciones como parámetros:
Función de éxito: Se ejecuta cuando el observable emite un valor.
Función de error (opcional): Se ejecuta si el observable o alguno de sus operadores falla.
Función de finalización (opcional): Se ejecuta siempre al finalizar la emisión de datos.
products: Product[] = [];
ngOnInit(): void {
this.productsService.getProducts().subscribe(
{ // Observer literal
next: prods => this.products = prods,
error: (err) => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
}
);
}
Mostrar Datos Asíncronos#
La carga de datos asíncronos puede retrasarse, lo que puede causar errores si Angular intenta acceder a datos que aún no están disponibles. Para manejar esto, se pueden utilizar varias técnicas:
Objetos Vacíos: Crear un objeto con datos vacíos para evitar errores.
Directiva
@if
: Mostrar los datos sólo cuando estén completamente cargados.Operador
?
: Asegurar que los datos no se accedan hasta que tengan un valor válido.
Signals#
Las señales (signals
) son una opción más simple y menos potente que los observables para tareas reactivas básicas. Desde Angular 17, se consideran una buena opción para tareas reactivas simples.
constructor(){
effect(()=>{console.log(`Valor de num: ${this.num()}`); });
}
num = signal(0);
updateNum(){ this.num.update((n: number) => n + 1); }
ngOnInit(): void { this.num.set(1); }
Resolver#
A veces es necesario obtener datos del servidor antes de acceder a una ruta específica. Para esto, se utiliza un tipo especial de servicio llamado Resolver.
Un Resolver es un servicio que implementa el método resolve
, el cual obtiene los datos antes de que la ruta se cargue:
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ProductService } from './product.service';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Product } from './product.model';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class ProductResolver implements Resolve<Product> {
constructor(private productsService: ProductService, private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Product | Observable<Product> | Promise<Product> {
return this.productsService.getProduct(route.params.id).pipe(
catchError(error => {
this.router.navigate(['/products']);
return of(null);
})
);
}
}
Este resolver utiliza el servicio real para obtener los productos y maneja cualquier error redirigiendo al usuario a una ruta segura.
Configuración de Rutas con Resolver#
Las rutas pueden configurarse para utilizar un resolver, asegurando que los datos necesarios estén disponibles antes de cargar el componente:
const routes: Routes = [
{ path: 'product/edit/:id',
canActivate: [ProductDetailGuard],
canDeactivate: [LeavePageGuard],
resolve: { product: ProductResolver },
component: ProductEditComponent
},
// Otras rutas
];
Autenticación con Angular#
En Angular, la autenticación puede ser manejada de varias formas, dependiendo de si la aplicación está alojada en el mismo servidor que el backend o si se utiliza un servicio externo. En este capítulo, exploraremos diferentes técnicas de autenticación y autorización en Angular, incluyendo el uso de cookies, tokens, interceptores, y guards.
Interceptores#
Los interceptores en Angular permiten interceptar y manipular solicitudes HTTP antes de que se envíen al servidor. Esto es útil para agregar tokens de autenticación a cada petición automáticamente.
Ejemplo de Interceptor de Autenticación:
import { HttpRequest, HttpHandler, HttpEvent, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable } from 'rxjs';
export function authInterceptor(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('idToken');
if (token) { // Clonamos la petición y añadimos el token
const authReq = req.clone({ url: req.url.concat(`?auth=${token}`) });
return next.handle(authReq); // Enviamos la petición con el token
}
// Sin token, enviamos la petición original
return next.handle(req);
}
El método clone()
se utiliza para crear una copia exacta de una solicitud HTTP (HttpRequest
) en Angular. Esto es necesario porque los objetos de tipo HttpRequest
son inmutables.. Al clonarlos, se puede crear una nueva instancia basada en la original y agregar, eliminar o modificar partes de la solicitud, como encabezados, parámetros, cuerpo o URL.
El método acepta un objeto de opciones donde se puede especificar qué partes de la solicitud cambiar. Algunos de los campos más comunes son:
setHeaders
: Agregar o sobrescribir encabezados.setParams
: Agregar o sobrescribir parámetros de consulta (query params).body
: Modificar el cuerpo de la solicitud (útil para solicitudesPOST
oPUT
).url
: Cambiar la URL de la solicitud.
Para utilizar este interceptor, se debe proporcionar en el componente principal:
bootstrapApplication(AppComponent, {providers: [
provideHttpClient(
withInterceptors([authInterceptor]),
)
]});
El interceptor anterior añade un token a la URL, pero el comando clone puede servir para manipular cualquier cosa de una petición. Aquí vemos un ejemplo en el que se añaden los datos típicos que necesita Supabase al header:
export function authInterceptor(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const clonedRequest = req.clone({
setHeaders: {
apiKey,
Authorization: authentication
}
});
return next.handle(clonedRequest);
}
En algunos proyectos no es necesario interceptar todas las peticiones. Esto se puede conseguir poniendo alguna condición. Por ejemplo:
if (req.url.includes('https://api.mi-dominio.com')) {
clonedRequest = req.clone({
setHeaders: {
apiKey: 'mi-clave-api',
Authorization: 'Bearer mi-token'
}
});
}
Guards#
Los guards son servicios que permiten o deniegan el acceso a ciertas rutas en una aplicación Angular. El guard CanActivate
se utiliza para proteger rutas y asegurar que solo usuarios autenticados puedan acceder a ellas.
Los Guards
pueden ser una clase o una función. En caso de ser una clase, debe implementar CanActivate
y en caso de ser una función debe ser una función del tipo CanActivateFn
. Es mucho más sencillo con funciones, así que esa será nuestra elección:
Ejemplo de Guard CanActivateFn
export const supabaseLoginGuard: CanActivateFn = (route, state) => {
const router: Router = inject(Router);
const supabaseService: SupabaseService = inject(SupabaseService);
const urlTree: UrlTree = router.parseUrl('./main');
return supabaseService.loggedSubject.getValue() ? true : urlTree;
};
Se puede ver cómo hemos inyectado el router y el servicio de autenticación con inject
. El router sirve para redirigir en caso de que el usuario no tenga la sesión iniciada.
Ejemplo de Guard CanActivate
:
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductDetailGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const id = route.params.id;
if (isNaN(id) || id < 1) {
console.log('La ID no es válida');
return this.router.parseUrl('/catalog');
}
return true;
}
}
Configuración de la ruta con Guard:
{ path: 'product/:id', canActivate: [ProductDetailGuard], component: ProductDetailComponent },
Variables como Observables#
En una aplicación autenticada, es importante que los componentes reaccionen a los cambios en el estado de autenticación sin necesidad de recargar la página. Esto se puede lograr usando BehaviorSubject
o Subject
para mantener y observar el estado de autenticación.
Ejemplo de Uso de BehaviorSubject
:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private loguedInfo: BehaviorSubject<boolean>;
constructor() {
this.loguedInfo = new BehaviorSubject<boolean>(false);
}
isLogued(): Observable<boolean> {
return this.loguedInfo.asObservable();
}
login() {
// Lógica de autenticación
this.loguedInfo.next(true);
}
logout() {
// Lógica de cierre de sesión
this.loguedInfo.next(false);
}
}
Suscripción al Estado de Autenticación en un Componente:
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth.service';
@Component({
selector: 'app-root',
template: `<div *ngIf="logued">Usuario autenticado</div>`
})
export class AppComponent implements OnInit {
logued = false;
constructor(private auth: AuthService) {}
ngOnInit(): void {
this.auth.isLogued().subscribe(logued => {
this.logued = logued;
});
}
}
Websockets#
No es posible establecer directamente una conexión WebSocket utilizando HttpClient
. Esto se debe a que HttpClient está diseñado específicamente para manejar solicitudes HTTP basadas en el protocolo HTTP/HTTPS, mientras que WebSocket utiliza un protocolo diferente.
Para trabajar con WebSockets en Angular, podemos usar directamente la API nativa de WebSocket de JavaScript o bibliotecas como RxJS para integrar la funcionalidad de WebSocket de manera reactiva.
Websockets con RxJS
#
Esta librería simplifica mucho la gestión de los sockets i permite trabajar directamente con Observables
.
Aquí tenemos un ejemplo de cómo usar un Websocket:
import { Injectable } from '@angular/core';
import { WebSocketSubject, webSocket } from 'rxjs/webSocket';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class WebSocketService {
private socket$: WebSocketSubject<any>;
constructor() {
this.socket$ = webSocket('ws://your-websocket-url');
}
// Send a message to the server
sendMessage(message: any) {
this.socket$.next(message);
}
// Receive messages from the server
getMessages(): Observable<any> {
return this.socket$.asObservable();
}
// Close the WebSocket connection
closeConnection() {
this.socket$.complete();
}
}
La clase WebSocketSubject
y webSocket
admiten la url y permiten tratar esa comunicación como un Subject
. https://rxjs.dev/api/webSocket/webSocket
Puesto que los websockets no se cierran inmediatamente, hay que recordar la desuscripción en el
ngOnDestroy
de los componentes que se suscriban a este servicio
Esta solución no contempla las posibles desconexiones, errores o la autenticación.
Tutorial interesante con más detalles: https://medium.com/@saranipeiris17/websockets-in-angular-a-comprehensive-guide-e92ca33f5d67
SSE#
Además de los WebSockets, existe la posibilidad de establecer una conexión unidireccional desde el servidor hacia el cliente mediante Server-Sent Events (SSE). A diferencia de WebSockets, que permiten una comunicación bidireccional, los SSE
facilitan una transmisión de datos solo en una dirección, desde el servidor al cliente. Esta comunicación se realiza sobre HTTP y está basada en el estándar de eventos del navegador, específicamente el objeto EventSource
.
Server-Sent Events (SSE):
A diferencia de los WebSockets, los SSE son un mecanismo unidireccional que solo permite que el servidor envíe datos al cliente. Esta tecnología también se basa en HTTP, pero usa una conexión persistente para transmitir datos al cliente de manera continua, como si fueran “eventos”.
La principal ventaja de los SSE es su simplicidad y la integración directa con los navegadores que soportan esta tecnología mediante el objeto
EventSource
, sin la necesidad de utilizar bibliotecas externas o un protocolo adicional como WebSockets.Los SSE son ideales para situaciones donde el servidor necesita enviar actualizaciones periódicas al cliente, como notificaciones en tiempo real, actualizaciones de precios o información de estado, pero sin la necesidad de recibir información desde el cliente.
SSE Utiliza HTTP estándar (
http://
ohttps://
) para mantener una conexión persistente de tipo “event-stream”. A diferencia de WebSockets que Utiliza su propio protocolo (ws://
owss://
) después del “handshake” inicial HTTP.Perfecto para escenarios donde solo el servidor necesita enviar datos al cliente de manera continua o periódica, como actualizaciones de feeds, notificaciones o dashboards en tiempo real.
En un entorno como Angular, SSE puede ser fácilmente transformado a Observables para aprovechar las características reactivas del framework. El cliente se suscribe a este Observable para recibir actualizaciones a medida que el servidor envía nuevos eventos, de forma similar a como se gestionan los WebSockets. Esto permite que los eventos SSE se manejen de manera reactiva dentro de la aplicación, facilitando el flujo de datos en tiempo real.
No existe como con los WebSockets, una función específica de RxJS, pero se puede crear un Subject
y actualizar manualmente en los eventos onmessage
y onerror
.
Así quedaría un servicio que se conecta a SSE
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class SseService {
private eventSource: EventSource | null = null;
private messagesSubject = new Subject<string>();
// Observable para los mensajes SSE
messages$ = this.messagesSubject.asObservable();
constructor() {}
// Método para conectar al servidor SSE
connectToSse(): void {
if (this.eventSource) {
this.eventSource.close(); // Cerrar conexión previa si existe
}
// Crear una nueva conexión SSE
this.eventSource = new EventSource('http://localhost:8081/events');
// Manejar los mensajes recibidos
this.eventSource.onmessage = (event) => {
//console.log(event);
this.messagesSubject.next(event.data); // Emitir el mensaje recibido
};
// Manejo de errores
this.eventSource.onerror = (error) => {
console.error('Error SSE:', error);
this.eventSource?.close();
};
}
// Método para cerrar la conexión SSE
disconnectFromSse(): void {
if (this.eventSource) {
this.eventSource.close();
console.log('Conexión SSE cerrada');
}
}
}
Integración de Angular con Supabase#
Supabase es una plataforma de backend como servicio (BaaS) que ofrece una variedad de servicios para aplicaciones web y móviles, como bases de datos en tiempo real, autenticación y almacenamiento. Supabase es compatible con TypeScript, lo que facilita su integración con aplicaciones Angular. En este capítulo, veremos cómo configurar y utilizar Supabase en una aplicación Angular.
Para comenzar, necesitamos instalar el SDK de Supabase utilizando npm:
npm install @supabase/supabase-js
Después de instalar el SDK, configuramos nuestras credenciales de Supabase en el archivo environment.ts
. Este archivo es utilizado por Angular para gestionar diferentes configuraciones de entorno, como las variables de entorno para desarrollo y producción. se crea con:
ng generate environments
En src/environments/environment.ts
, añade las siguientes líneas:
export const environment = {
production: false,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
};
A continuación, creamos un servicio en Angular para inicializar y gestionar Supabase. Este servicio será responsable de la configuración inicial y de proporcionar métodos para interactuar con la base de datos.
Crea un nuevo servicio utilizando Angular CLI:
ng generate service supabase
En el archivo supabase.service.ts
, inicializa Supabase de la siguiente manera:
import { Injectable } from '@angular/core';
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient;
constructor() {
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey);
}
// Métodos para interactuar con Supabase
async getData(table: string) {
const { data, error } = await this.supabase.from(table).select('*');
if (error) {
console.error('Error fetching data:', error);
throw error;
}
return data;
}
async insertData(table: string, row: any) {
const { data, error } = await this.supabase.from(table).insert(row);
if (error) {
console.error('Error inserting data:', error);
throw error;
}
return data;
}
async updateData(table: string, row: any, id: number) {
const { data, error } = await this.supabase.from(table).update(row).eq('id', id);
if (error) {
console.error('Error updating data:', error);
throw error;
}
return data;
}
async deleteData(table: string, id: number) {
const { data, error } = await this.supabase.from(table).delete().eq('id', id);
if (error) {
console.error('Error deleting data:', error);
throw error;
}
return data;
}
}
Conversión de Promesas a Observables#
El SDK de Supabase funciona con promesas, pero en Angular es común trabajar con Observables para aprovechar las capacidades de programación reactiva de RxJS. Podemos convertir promesas a observables utilizando el operador from
de RxJS.
getDataObservable(table: string): Observable<any> {
return from(this.getData(table));
}
private async getData(table: string) {
const { data, error } = await this.supabase.from(table).select('*');
if (error) {
console.error('Error fetching data:', error);
throw error;
}
return data;
}
En el componente, podemos suscribirnos al Observable para obtener los datos:
ngOnInit() {
this.supabaseService.getDataObservable('your_table_name').subscribe(
(data) => {
this.data = data;
},
(error) => {
console.error('Error loading data:', error);
}
);
}
Hay que tener en cuenta que Supabase
en su SDK
ya maneja cualquier error de comunicación. Así que la promesa se cumplirá y emitirá un objeto com data
o error
. Con el throw
estamos emitiendo un reject que se convertirá en un error
dentro del Observable
. Aunque se puede tratar dentro de un pipe
, como en este ejemplo con el login
:
login(email: string, password: string) {
const loginResult = from(
this.supabase.auth.signInWithPassword({
email,
password,
})
).pipe(
map(({ data, error }) => {
if (error) {
throw error;
}
return data;
}),
tap(() => this.isLogged()),
);
return loginResult;
}
Envío y recepción de ficheros con Angular#
En Angular, el manejo de ficheros implica tanto el envío como la recepción de archivos. Este punto cubre diferentes enfoques para trabajar con archivos en Angular, desde el envío con FormData
, que es la forma recomendada cuando el servidor acepta multipart/form-data
, hasta la conversión a Base64
en caso de que los datos se envíen en formato JSON. Además, se explica cómo recibir ficheros en formato binario y mostrarlos en un <embed>
, cómo descargar archivos desde el servidor y cómo manejar archivos codificados en Base64
, convirtiéndolos en Blob
para optimizar su visualización.
Enviar ficheros con FormData#
Esta es la manera recomendada en caso de que el servidor acepte ficheros multipart
. Es la más sencilla:
export class UploadService {
private apiUrl = 'https://api.ejemplo.com/upload'; // Reemplazar con la URL real
constructor(private http: HttpClient) {}
uploadForm(documento: File, nombre: string, email: string): Observable<any> {
const formData = new FormData();
formData.append('documento', documento); // "documento" es el nombre del campo en el backend
formData.append('nombre', nombre);
formData.append('email', email);
return this.http.post(this.apiUrl, formData);
}
}
Mostrar una barra de progreso#
El siguiente código implementa la subida de un archivo en usando HttpClient
y RxJS Observables para proporcionar actualizaciones de progreso. Primero, crea un FormData
y adjunta el archivo recibido como parámetro. Luego, envía una petición POST
a this.uploadUrl
, configurando reportProgress: true
y observe: 'events'
para recibir eventos del proceso. Mediante pipe(map())
, evalúa el tipo de evento: si es HttpEventType.UploadProgress
, calcula el porcentaje de progreso (loaded / total * 100
); si es HttpEventType.Response
, indica que la subida ha finalizado devolviendo 100
. Finalmente, devuelve un Observable<number>
que emite el progreso en tiempo real.
uploadFile(file: File): Observable<number> {
const formData = new FormData();
formData.append('file', file);
return this.http.post<HttpEvent<any>>(this.uploadUrl, formData, {
reportProgress: true,
observe: 'events'
}).pipe(
map(event => {
switch (event.type) {
case HttpEventType.UploadProgress:
return Math.round((event.loaded / (event.total ?? 1)) * 100); // Porcentaje de progreso
case HttpEventType.Response:
return 100; // Completado
default:
return 0;
}
})
);
}
Luego para mostrarlo, en el componente:
uploadFile(file: File) {
this.uploadProgress = 0; // Inicializa la barra de progreso
this.fileUploadService.uploadFile(file).subscribe(progress => {
this.uploadProgress = progress; // Actualiza el progreso
});
}
Después ya se puede mostrar, por ejemplo con Bootstrap
:
<div class="progress">
<div
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
[style.width.%]="uploadProgress"
aria-valuenow="{{ uploadProgress }}"
aria-valuemin="0"
aria-valuemax="100">
{{ uploadProgress }}%
</div>
</div>
Enviar ficheros con JSON#
Para enviar con JSON es más complejo, ya que no acepta datos binarios, hay que convertir a Base64
. Para ello usaremos un FileReader
y sus eventos.
assignPDF(file: File, recipe: IRecipe) {
const reader = new FileReader();
fromEvent(reader, 'load')
.pipe(
map(() => {
const base64String = (reader.result as string).split(',')[1]; // Quitar "data:mimeType;base64,"
const fileData = {
base64: base64String,
mimeType: file.type,
};
console.log(fileData);
return fileData;
}),
switchMap((fileData) => {
return from(
this.supabase
.from('meals')
.update({ pdf: fileData.base64, mimepdf: fileData.mimeType })
.eq('idMeal', recipe.idMeal)
.select()
);
})
)
.subscribe();
reader.readAsDataURL(file);
}
El código convierte un archivo en Base64 y lo sube a una base de datos usando Observables de RxJS. Primero, crea un FileReader
para leer el archivo y usa fromEvent
para convertir su evento 'load'
en un Observable. Luego, con map()
, extrae la cadena Base64 quitando el prefijo "data:mimeType;base64,"
y la estructura en un objeto con el tipo MIME. Con switchMap()
, transforma ese objeto en una petición asíncrona a Supabase usando .update()
para guardar el PDF y su MIME en la tabla meals
, filtrando por idMeal
. Finalmente, subscribe()
ejecuta la operación, y reader.readAsDataURL(file)
inicia la conversión.
Recibir ficheros binarios#
Se pueden recibir binarios y crear la URL temporal para poder insertarlos en un <embed>
o similar:
getPDF(recipe: IRecipe): Observable<string> {
return from(
this.supabase.storage.from('recipes').download(`${recipe.pdf}`)
).pipe(
map(({ data, error }) => {
let blobimg = '';
if (data) {
blobimg = URL.createObjectURL(data);
}
return blobimg;
})
);
}
También se podrían recibir i hacer que el navegador ofrezca descargar el fichero:
downloadPDF(recipe: IRecipe): void {
from(this.supabase.storage.from('recipes').download(`${recipe.pdf}`))
.pipe(
map(({ data, error }) => {
if (data) {
const blobUrl = URL.createObjectURL(data);
const a = document.createElement('a');
a.href = blobUrl;
a.download = recipe.pdf; // Nombre del archivo a descargar
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl); // Liberar memoria
}
})
)
.subscribe();
}
Recibir ficheros en Base64#
Cuando tratamos con Base64
, lo que recibimos es un string. Suponiendo que ha llegado junto a otros datos de la misma tabla obtenidos ya, el servicio puede hacer que se vea, por ejemplo:
getPDFBase64(recipe: IRecipe): string {
return `data:application/pdf;base64,${recipe.pdf}`;
}
En este caso se supone que recipe.pdf
está en Base64
. Esto se puede insertar en una src
:
loadPDFBase64(){
this.PDFbase64 = this.sanitizer.bypassSecurityTrustResourceUrl(this.supabaseService.getPDFBase64(this.recipe!));
}
Hay que sanitizar
la URL.
Luego en la plantilla lo podemos poner así:
<embed [src]="PDFbase64" type="application/pdf">
Esta solución no siempre es la mejor. Si los ficheros son muy grandes es más eficiente convertir en un Blob
:
getPDFBase64blob(recipe: IRecipe): string {
const byteCharacters = atob(recipe.pdf!);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return URL.createObjectURL(
new Blob([byteArray], { type: recipe.mimepdf! })
);
}
Este código es preciso, ya que los Blob
aceptan binarios y hay que convertir el string
en un Arrray de enteros Uint8Array
.
Tutorial: https://www.geeksforgeeks.org/how-to-convert-base64-to-file-in-javascript/