Christian Salazar Piñeros
Nicolas Molina
ProfesorLa geolocalización es una herramienta poderosa que permite a las aplicaciones web ofrecer experiencias personalizadas basadas en la ubicación del usuario. Mediante el uso de APIs nativas del navegador y servicios externos, podemos crear funcionalidades que muestren información relevante según la posición geográfica del visitante. En este artículo, exploraremos cómo implementar la geolocalización en una aplicación Angular para mostrar puntos de interés cercanos a la ubicación del usuario, una característica común en sitios de eCommerce que desean mostrar sus tiendas físicas.
¿Cómo funciona la API de geolocalización del navegador?
La API Navigator.geolocation es una interfaz nativa de los navegadores modernos que permite acceder a la ubicación geográfica del dispositivo. Esta API solo funciona del lado del cliente (browser) y requiere el consentimiento explícito del usuario para compartir su ubicación.
Para utilizar esta API en Angular, debemos asegurarnos de ejecutar el código relacionado con la geolocalización después de que el componente se haya renderizado en el navegador:
import { Component, Signal, signal } from '@angular/core'; import { afterNextRender } from '@angular/core'; @Component({ selector: 'app-locations', templateUrl: './locations.component.html' }) export class LocationsComponent { origin: Signal<string> = signal(''); constructor() { afterNextRender(() => { navigator.geolocation.getCurrentPosition((position) => { const newOrigin = `${position.coords.latitude},${position.coords.longitude}`; this.origin.set(newOrigin); }); }); } }
El método getCurrentPosition() devuelve un objeto con las coordenadas del usuario, que podemos formatear según nuestras necesidades. En este caso, creamos un string con el formato "latitud,longitud" que utilizaremos para consultar una API externa.
¿Por qué usar afterNextRender?
Es crucial utilizar afterNextRender cuando trabajamos con APIs que solo están disponibles en el navegador, ya que Angular utiliza renderizado universal (server-side rendering) para la primera carga. Esto garantiza que el código de geolocalización solo se ejecute cuando el componente esté completamente renderizado en el cliente.
¿Cómo consumir una API de localización con las coordenadas del usuario?
Una vez obtenida la ubicación del usuario, podemos utilizarla para consultar una API que nos proporcione puntos de interés cercanos. En nuestro ejemplo, utilizaremos un endpoint de la API de Platzi que acepta parámetros como:
Implementación del consumo de la API
async locationResource(request: { origin?: string } = {}) { const url = new URL(`${environment.API_URL}/api/v1/localizations`); if (request.origin) { url.searchParams.set('origin', request.origin); } const response = await fetch(url.toString()); return await response.json(); }
Este método recibe un objeto con parámetros opcionales y construye la URL de la petición utilizando la API URL, que nos permite agregar query params de forma sencilla. Si existe un origen, lo añadimos como parámetro a la URL.
Renderizando los resultados en el componente
Para mostrar los puntos de interés en nuestra interfaz, podemos utilizar un bucle en nuestro template:
<ul> @for (location of locations; track location.id) { <li>{{ location.name }}</li> } </ul>
¿Cómo mejorar la arquitectura de nuestra aplicación?
Aunque el código anterior funciona correctamente, no sigue las mejores prácticas de Angular. Es recomendable separar la lógica de negocio (obtención de datos) de la presentación (componente) siguiendo el principio de responsabilidad única de SOLID.
Creando un servicio dedicado
La forma correcta de implementar esta funcionalidad sería crear un servicio dedicado:
import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root' }) export class LocationsService { async getLocations(params: { origin?: string, size?: number, radius?: number } = {}) { const url = new URL(`${environment.API_URL}/api/v1/localizations`); // Añadir parámetros si existen if (params.origin) url.searchParams.set('origin', params.origin); if (params.size) url.searchParams.set('size', params.size.toString()); if (params.radius) url.searchParams.set('radius', params.radius.toString()); const response = await fetch(url.toString()); return await response.json(); } async getCurrentPosition(): Promise<string> { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (position) => { const coords = `${position.coords.latitude},${position.coords.longitude}`; resolve(coords); }, (error) => { reject(error); } ); }); } }
Este servicio encapsula toda la lógica relacionada con la obtención de la ubicación del usuario y la consulta a la API de localización, lo que hace que nuestro componente sea más limpio y mantenible.
Utilizando el servicio en el componente
import { Component, Signal, signal } from '@angular/core'; import { afterNextRender } from '@angular/core'; import { LocationsService } from './locations.service'; @Component({ selector: 'app-locations', templateUrl: './locations.component.html' }) export class LocationsComponent { locations: Signal<any[]> = signal([]); constructor(private locationsService: LocationsService) { // Cargar ubicaciones por defecto this.loadLocations(); // Cargar ubicaciones basadas en la posición del usuario afterNextRender(async () => { try { const origin = await this.locationsService.getCurrentPosition(); this.loadLocations({ origin }); } catch (error) { console.error('Error obteniendo la ubicación:', error); } }); } async loadLocations(params = {}) { const data = await this.locationsService.getLocations(params); this.locations.set(data); } }
Esta implementación es más robusta y sigue las mejores prácticas de Angular, separando claramente las responsabilidades entre el componente y el servicio.
La geolocalización es una herramienta poderosa que puede mejorar significativamente la experiencia de usuario en aplicaciones web. Al implementarla correctamente en Angular, podemos ofrecer contenido personalizado basado en la ubicación del usuario de manera eficiente y mantenible. ¿Has implementado funcionalidades de geolocalización en tus proyectos? Comparte tu experiencia en los comentarios.
Christian Salazar Piñeros
Nicolas Molina
ProfesorChristian Salazar Piñeros
Emilio Nicolás Mendoza Patti
Edgar Ramirez
Pablo Torres Pérez
Pablo Torres Pérez
Max Andy Diaz Neyra
Si desean implementar el mapa con google maps, les recomiendo verse el siguiente video, muy completo!
Muchas gracias, si justo en ese video también se usan las nuevas formas de hacer queries con signals muy útil
Aquí les dejo como implementé la solución de un servicio dedicado a los fetch de la API y una integración con Google maps básica.
Dejo aqui el codigo del service:
import { Injectable } from '@angular/core'; import { environment } from '@env/environment'; import { Location } from '@shared/models/location.model'; @Injectable({ providedIn: 'root', }) export class LocationService { async getLocationPromise(request: { origin?: string }): Promise<Location[]> { const url = new URL(`${environment.apiUrl}/api/v1/locations`); if (request.origin) { url.searchParams.set('origin', request.origin); } const response = await fetch(url.toString()); const data = await response.json(); return data; } }
y tambien el codigo de la page de locations:
import { afterNextRender, Component, inject, resource, signal, } from '@angular/core'; import { LocationService } from '@shared/services/location.service'; @Component({ selector: 'app-locations', imports: [], templateUrl: './locations.component.html', }) export default class LocationsComponent { locationService = inject(LocationService); $origin = signal(''); constructor() { afterNextRender(() => { navigator.geolocation.getCurrentPosition(position => { const origin = `${position.coords.latitude},${position.coords.longitude}`; this.$origin.set(origin); }); }); } locationResource = resource({ request: () => ({ origin: this.$origin() }), loader: ({ request }) => this.locationService.getLocationPromise(request), }); }
Mi servicio
@Injectable({ providedIn: 'root' }) export class LocationService { private http = inject(HttpClient); private _origin = signal<string>(''); readonly origin = this._origin.asReadonly(); updateOrigin(origin: string): void { this._origin.set(origin); } readonly locationResources = resource({ request: () => ({ origin: this.origin() }), loader: async ({ request }) => { const url = 'http://api.escuelajs.co/api/v1/locations'; const options = request.origin ? { params: { origin: request.origin } } : {}; return firstValueFrom(this.http.get<unknown[]>(url, options)); } }); }
Aquí la solución al segundo reto con base en el video del profe Nico que compartió @Christian Salazar Piñeros.
Muy entretenido este segundo reto, al igual que el primero, se aprende mucho!!
Aqui el servicio con observable:
import { inject, Injectable } from '@angular/core'; import { environment } from '@env/environment'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { Location } from '@shared/models/location.model'; @Injectable({ providedIn: 'root', }) export class LocationsService { private apiUrl = environment.apiUrl; private http = inject(HttpClient); getLocationsByOrigin(origin: string): Observable<Location[]> { if (!origin) { //Para consistencia return of([]); } const params = new HttpParams().set('origin', origin); return this.http.get<Location[]>(`${this.apiUrl}/api/v1/locations`, { params, }); } }
Location interface:
export interface Location { id: number; name: string; description: string; latitude: number; longitude: number; }
Componente:
import { afterNextRender, Component, inject, signal } from '@angular/core'; import { rxResource } from '@angular/core/rxjs-interop'; import { LocationsService } from '@shared/services/locations.service'; @Component({ selector: 'app-locations', imports: [], templateUrl: './locations.component.html', }) export default class LocationsComponent { locationsService = inject(LocationsService); $origin = signal(''); constructor() { afterNextRender(() => { navigator.geolocation.getCurrentPosition(position => { const origin = `${position.coords.latitude},${position.coords.longitude}`; this.$origin.set(origin); }); }); } locations = rxResource({ params: () => ({ origin: this.$origin() }), stream: ({ params }) => this.locationsService.getLocationsByOrigin(params.origin), }); }
locations.service:
import { Injectable } from '@angular/core'; import { Location } from '@shared/models/location.model'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) export class LocationsService { async getLocations({ origin }: { origin?: string } = {}): Promise< Location[] > { const url = new URL(`${environment.apiUrl}/api/v1/locations`); if (origin) { url.searchParams.set('origin', origin); } const response = await fetch(url); const data = await response.json(); return data; } } ```location.model: ```js export interface Location { id: number; name: string; description: string; latitude: number; longitude: number; } ```location.component: ```js import { afterNextRender, Component, inject, resource, signal, } from '@angular/core'; import { LocationsService } from '@shared/services/locations.service'; @Component({ selector: 'app-locations', imports: [], templateUrl: './locations.component.html', styleUrls: [], }) export default class LocationsComponent { locationsService = inject(LocationsService); locations = resource({ request: () => ({ origin: this.$origin() }), loader: ({ request }) => this.locationsService.getLocations(request), }); $origin = signal(''); constructor() { afterNextRender(() => { navigator.geolocation.getCurrentPosition(position => { const originValue = `${position.coords.latitude},${position.coords.longitude}`; this.$origin.set(originValue); }); }); } }