Carlos Enrique Ramírez Flores
Francisco Manuel Velásquez Escobar
Hola, gracias Carlos! me ayudó a solventar algunas dudas.
Francisco Manuel Velásquez Escobar
Conecta un campo de búsqueda con resultados en tiempo real de Google usando Angular, RxJS y Bootstrap. Aquí verás cómo estructurar el HTML, activar formularios reactivos, renderizar una lista con async y optimizar llamadas con debounceTime para un type ahead fluido y veloz.
¿Cómo implementar type ahead con Angular, RxJS y Google?
Primero, se prepara el HTML para capturar la entrada y mostrar resultados. El input se enlaza a un FormControl llamado searchField. Cada tecla genera eventos que, vía RxJS, hacen la llamada al API de Google, formatean la respuesta y alimentan un stream de resultados. Se renderiza una lista con Bootstrap y el async pipe para manejar la naturaleza asíncrona.
¿Cómo se pinta la lista con async y Bootstrap?
<form> <div class="well"> <label>Buscar negocio en Google.</label> <input type="text" class="form-control" [formControl]="searchField" /> <ul class="list-group"> <li class="list-group-item" *ngFor="let result of results$ | async"> {{ result.formatted_address }} </li> </ul> </div> </form>
¿Qué error de Angular aparece y cómo se corrige?
Al usar [formControl] puede aparecer: "Can't bind to 'formControl' since it isn't a known property of 'input'". Causa: los formularios reactivos no se cargan por defecto. Solución: importar ReactiveFormsModule en AppModule.
// app.module.ts import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule, ] }) export class AppModule {}
¿Cómo reducimos llamadas y mejoramos performance?
Teclear rápido dispara muchos requests; incluso algunos fallan. Para evitar saturación, se usa debounceTime antes de switchMap. Así se espera un tiempo entre teclas y solo se ejecuta la última intención del usuario.
// crear.component.ts (fragmento) this.results$ = this.searchField.valueChanges.pipe( debounceTime(500), switchMap(term => this.search(term)), map(resp => this.format(resp)), map(list => this.onlyAddresses(list)) );
¿Qué operadores se concatenan y para qué sirven?
¿Qué sigue para completar la experiencia?
Al hacer clic en un resultado, se puede autocompletar el formulario con: calle y número, ciudad y país. El API ya provee esa data en address_components (por ejemplo: street_number, long_name). La tarea consiste en capturar el clic en el ítem y poblar esos inputs.
¿Te animas a implementar el clic en la lista y llenar las tres cajas? Comparte tu código y una captura en los comentarios para revisar juntos.
Como a mi me funcionó al 18/04/19:
const URL = 'https://maps.google.com/maps/api/geocode/json?key= + XXX--KEY--XXX + &address='; this.searchField = new FormControl() this.results$ = this.searchField.valueChanges .pipe( debounceTime(300) ,switchMap(query => this.http.get(`${URL}?address=${query}`)) ,map((response: any) => { return response.results }) );
Carlos Enrique Ramírez Flores
Francisco Manuel Velásquez Escobar
Hola, gracias Carlos! me ayudó a solventar algunas dudas.
Francisco Manuel Velásquez Escobar
Alexis Antonio Moraga Gallardo
Hasta el momento todo funciona bien, pero tengo un error al cambiar la dirección.
si hacemos como esta en el video de seleccionar y escribir una nueva dirección no hay problema.
pero si borramos el contenido y comenzamos a escribir desde cero marca el siguiente error
error_message : "Invalid request. Missing the 'address', 'bounds', 'components', 'latlng' or 'place_id' parameter." results : [] status : "INVALID_REQUEST"
cuando esto pasa la conexión se rompe y ya no se envían mas peticiones.
¿Como Puedo solucionar esto?
Saludos
Nyxent Corp
Yo tengo el mismo problema
Alexis Antonio Moraga Gallardo
Sigo sin poder resolver esto. que el profe @Eduardo se Pronuncie xD
Mi solución para Angula 6 (21/11/2018), utilizando **Subject **en vez de FormControl, y haciendo una comprobación previa para que no busque otra vez si el input tiene el mismo texto desde la última llamada.
crear.component.ts
import { Component, OnInit } from '@angular/core'; import { LugaresService } from 'src/services/lugares/lugares.service'; import { ActivatedRoute } from '@angular/router'; import { Observable, Subject } from 'rxjs'; import { switchMap, map, debounceTime, distinctUntilChanged } from 'rxjs/operators'; @Component({ selector: 'app-crear', templateUrl: './crear.component.html', styleUrls: ['./crear.component.scss'] }) export class CrearComponent implements OnInit { lugar: any = {}; results$: Observable<any>; private searchField = new Subject<string>(); constructor(private route: ActivatedRoute, private lugaresService: LugaresService) { const id = this.route.snapshot.params.id; if (id !== 'new') { this.lugar = this.lugaresService.getById(id) .subscribe(lugar => this.lugar = lugar); } } ngOnInit() { this.results$ = this.searchField.pipe( debounceTime(500), distinctUntilChanged(), switchMap((term: string) => this.lugaresService.obtenerGeoData(term)), map((response: any) => { console.log(response); return response.results; }) ); } search(term: string): void { this.searchField.next(term); } guardarLugar() { const direccion = this.lugar.calle + ',' + this.lugar.ciudad + ',' + this.lugar.pais; this.lugaresService.obtenerGeoData(direccion) .subscribe((res: any) => { this.lugar.lat = res.results[0].geometry.location.lat; this.lugar.lng = res.results[0].geometry.location.lng; if (!this.lugar.id) { this.lugaresService.create(this.lugar); this.lugar = {}; } else { this.lugaresService.edit(this.lugar); } }); } }
crear.component.html
<h2>Datos de Dirección</h2> <div class="alert alert-secondary"> <form> <label>Buscar dirección en Google Maps</label> <input type="text" #searchBox class="form-control" (keyup)="search(searchBox.value)"> </form> <ul class="list-group"> <li class="list-group-item" *ngFor="let result of results$ | async" (click)="seleccionarDireccion(result)"> {{result.formatted_address}} </li> </ul> </div>
Edwin Alberto Guerrero Acosta
cuando es recomendable usar rxjs?
Jonathan Jiménez
Buenas,
Dejo mi solución al reto,
crear.component.ts
recogerDireccion(result) { this.lugar.calle = result.address_components[1].long_name + ' ' + result.address_components[0].long_name; this.lugar.ciudad = result.address_components[3].long_name; this.lugar.pais = result.address_components[4].long_name; }
crear.component.html
<li (click)="recogerDireccion(result)" class="list-group-item" *ngFor="let result of results$ | async"> {{ result.formatted_address }} </li>
Nota: En mí país, España, las direcciones son más cortas, si quisierais hacerlo para latino América (Cogiendo como referencia los lugares usados por @Eduardo de la Cruz Ibarra), tendríais que cambiar en la función la asignación de la ciudad por el elemento 4 del array (result.address_components[4]) y el de país por el 5
saludos,
Wilson Fabian Pérez Sucuzhañay
para los que les falla con el .json(), ya que se cambia y ahora utilizamos el HttpClient va este codigo
this.results$ = this.searchField.valueChanges
.debounceTime(500)
.switchMap(query => this.http.get(${URL}?address=${query}))
.map((response: any) => response.results);
Edwin Alberto Guerrero Acosta
los servicios en angular implementan rxjs?
Carlos Enrique Ramírez Flores
cual es el link del repositorio?
Héctor Daniel Vega Quiñones
ProfesorLo puedes encontrar aquí 😉
Abigail Hernández Ponce
No porque:
Primero se centra la atención en el paréntesis grande y olvidate de lo demás (34+22+(3-2)), después empezaremos resolviendo el parentesis mas pequeño (3-2), que es igual a 1, luego resuelve la multiplicación 3*4 da como resultado 12, ahora ya teniendo esto tu sumas y la suma queda así: (12+22+(1)), es igual a 35 y por último sumas el 1 y da como resultado total 36
Camilo Arango Ochoa
Se debe tener mucho cuidado con hacer tantos llamados a las API ya que en el caso de API pagas se puede incurrir en costos o en el caso de API propias se pueden tener problemas de performance.
Darwel Duran
se tiene que importar ReactiveFormsModule en app.module.ts para hacer uso de la directiva [formControl]
imports: [ BrowserModule, FormsModule, ReactiveFormsModule,```
Carlos Uriel de Jesus Sánchez González
Buena forma de hacerlo
Marwin Gelvez
Buenas tardes, esta es mi solución al reto:
crear.component.ts
crear.component.html
Nelson Oswaldo Gonzalez Santiago
Jessie Buckland Pérez
Minuto 4:48, Eduardo Ibarra 4 President. No he visto forma más práctica y sencilla de explicar los observables, he conseguido entender en 3 videos lo que no me han sabido otros profesionales en 1 año. Gracias!!!!
Irving Sarmiento
Saludos, dejando mis resultados.
1. crear.component.ts
2. crear.component.html
XAVIER MALDONADO
Diego Forero
Team PlatziEs porque estas ejecutando desde tu local, intenta subir tu proyecto a heroku o para poder probarlo y te debe funcionar sin problema.
angular 6 publicado el 12/07/2018
ojo no funciona con interval o con .debounceTime(500). si alguien puede hacerlo funcionar comparta y avisen por favor
crear component.ts
import { Component } from '@angular/core'; import { LugaresService } from "../services/lugares.service" import { ActivatedRoute } from "@angular/router" import { Observable } from "rxjs" import * as Rx from 'rxjs'; import { FormControl } from "@angular/forms" import {Http} from '@angular/http' import { switchMap, map } from 'rxjs/operators'; @Component({ selector: 'app-crear', templateUrl: './crear.component.html' }) export class CrearComponent { lugar:any = {} id:any = null; results$: Observable<any>; private searchField: FormControl; constructor(private lugaresService: LugaresService, private route: ActivatedRoute, private http:Http){ this.id = this.route.snapshot.params['id']; if(this.id != 'new'){ this.lugaresService.getLugar(this.id) .valueChanges() .subscribe((lugar)=>{ this.lugar = lugar; }); } const _URL = 'https://maps.googleapis.com/maps/api/geocode/json' this.searchField = new FormControl() this.results$ = this.searchField.valueChanges .pipe( switchMap(query => this.http.get(`${_URL}?address=${query}`)) ,map(response => response.json().results) ); this.results$.subscribe() } guardarLugar() { var direccion = this.lugar.direccion+','+this.lugar.ciudad+','+this.lugar.pais this.lugaresService.obtenerGeoData(direccion) .subscribe((result)=>{ this.lugar.lat = result.json().results[0].geometry.location.lat this.lugar.lng = result.json().results[0].geometry.location.lng if(this.id != 'new'){ this.lugaresService.editarLugar(this.lugar) alert('exito al editar') }else{ alert(this.lugar.id); this.lugaresService.guardarLugar(this.lugar) alert('exito al guardar') } this.lugar = {} }) } }
crea.component.html
<div class="well"> <a routerLink="/lugares">Lugares</a> / crear lugar </div> <div class="container"> <div class="row"> <div class="col-md-6 col-sm-6"> </div> <div class="col-md-6 col-sm-6"> <div class="panel panel-default"> <div class="panel-body"> <form action=""> <div class="form-group"> <label for="Nombre">Nombre del negocio</label> <div class="input-group"> <span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-user"></span></span> <input type="text" class="form-control" [(ngModel)]="lugar.nombre" name="Nombre"> </div> </div> <div class="form-group"> <label for="Plan">Plan</label> <select name="Plan" id="Plan" class="form-control" [(ngModel)]="lugar.plan"> <option value=""></option> <option value="pagado">Pagado</option> <option value="gratis">Gratis</option> </select> </div> <div class="form-group"> <label for="Cercania">Cercania</label> <select name="Cercania" id="Cercania" class="form-control" [(ngModel)]="lugar.cercania"> <option value=""></option> <option value="1">Muy cercas</option> <option value="2">Cercas</option> <option value="3">Lejos</option> </select> </div> <div class="form-group"> <label for="Distancia">Distancia en km</label> <div class="input-group"> <span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-map-marker"></span></span> <input type="number" min="1" step="0.1" class="form-control" [(ngModel)]="lugar.distancia" name="Distancia" > </div> </div> <div class="form-group"> <label for="status">Status</label> <select name="status" id="status" class="form-control" [(ngModel)]="lugar.status"> <option value=""></option> <option value="1">Activo</option> <option value="2">Inactivo</option> </select> </div> <div class="form-group"> <label for="Descripcion">Descripción del negocio</label> <textarea name="Descripcion" class="form-control" [(ngModel)]="lugar.descripcion" rows="4"></textarea> </div> <hr> <h3>Datos de direccion</h3> <div class="well"> <form action=""> <label>Buscar direccion en Google Maps</label> <input type="text" [formControl]="searchField" class="form-control"> </form> <ul class="list-group"> <li class="list-group-item" *ngFor="let result of results$ | async">{{result.formatted_address}}</li> </ul> </div> <div class="form-group"> <label for="Domicilio">Calle y numero</label> <div class="input-group"> <span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-map-marker"></span></span> <input type="text" class="form-control" [(ngModel)]="lugar.domicilio" name="Domicilio" > </div> </div> <div class="form-group"> <label for="Ciudad">Ciudad</label> <div class="input-group"> <span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-map-marker"></span></span> <input type="text" class="form-control" [(ngModel)]="lugar.ciudad" name="Ciudad" > </div> </div> <div class="form-group"> <label for="Pais">Pais</label> <select name="Pais" id="Pais" class="form-control" [(ngModel)]="lugar.pais"> <option value=""></option> <option value="México">México</option> <option value="Colombia">Colombia</option> <option value="Peru">Peru</option> <option value="Estados unidos">Estados unidos</option> </select> </div> <div class="form-group text-right"> <button class="btn btn-primary" (click)="guardarLugar()" style="margin-right: 10px;"> <span class="glyphicon glyphicon-send"></span> Enviar </button> <a class="btn btn-default" routerLink="/lugares"> <span class="glyphicon glyphicon-remove"></span> Cancelar </a> </div> </form> </div> </div> </div> </div> </div>
app,module.ts
import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' import { AppComponent } from './app.component' import { FormsModule,ReactiveFormsModule } from '@angular/forms' import { AgmCoreModule } from '@agm/core' import { ResaltarDirective } from './directives/resaltar.directive' import { ContarClickDirective } from './directives/contar-clicks.directive' import { Routes, RouterModule } from "@angular/router" import { DetalleComponent } from "./detalle/detalle.component" import { LugaresComponent } from "./lugares/lugares.component" import { ContactoComponent } from "./contacto/contacto.component" import { CrearComponent } from "./crear/crear.component" import { LugaresService } from "./services/lugares.service" import { AngularFireModule } from 'angularfire2'; import { AngularFireDatabaseModule } from 'angularfire2/database'; import { environment } from '../environments/environment'; import { HttpModule } from '@angular/http' import { LinkyfystrPipe} from './pipes/linkifystr.pipe' import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { LoginComponent } from './login/login.component'; import { RegistroComponent } from './registro/registro.component'; import { AutorizacionService } from './services/autorizacion.service' import { AngularFireAuthModule } from 'angularfire2/auth' import { MyGuard } from './services/my-guard.service' const appRoutes: Routes = [ { path: '', component: LugaresComponent }, { path: 'lugares', component: LugaresComponent }, { path: 'detalle/:id', component: DetalleComponent }, { path: 'contacto', component: ContactoComponent }, { path: 'crear/:id', component: CrearComponent }, { path: 'login', component: LoginComponent }, { path: 'registro', component: RegistroComponent } ] @NgModule({ declarations: [ AppComponent, ResaltarDirective, ContarClickDirective, DetalleComponent, LugaresComponent, ContactoComponent, CrearComponent, LinkyfystrPipe, LoginComponent, RegistroComponent ], imports: [ BrowserModule, FormsModule, AgmCoreModule.forRoot({ apiKey: 'AIzaSyC5Cd6Tiu3oqs69v1GDk5_aTCwOk8B8ZIM' }), RouterModule.forRoot(appRoutes), AngularFireModule.initializeApp(environment.firebase), AngularFireDatabaseModule, HttpModule, BrowserAnimationsModule, AngularFireAuthModule, ReactiveFormsModule ], providers: [LugaresService, AutorizacionService,MyGuard], bootstrap: [AppComponent] }) export class AppModule { }
Carlos me ha funcionado el debounceTime agregándolo de la siguiente manera:
this.results$ = this.searchField.valueChanges .pipe( debounceTime(500) ,switchMap(query => this.http.get(`${URL}?address=${query}`)) ,map(response => response.json().results) ); this.results$.subscribe()
No olvides importarlo desde los operadores de rxjs:
import { switchMap, map, debounceTime } from 'rxjs/operators';
Saludos!
Cordial Saludo.
dejo mi solucion de reto del dia.
crear.componente.html
<div class="well"> <form> <label>Buscar Negocio en Google Maps</label> <input class="form-control" type="text" [formControl]="searchField"> </form> <ul class="list-group"> <li class="list-group-item" *ngFor="let result of results$ | async" (click)="obtenerDireccion(result)">{{result.formatted_address}}</li> </ul> </div> <label>Calle y Número</label> <input type="text" class="form-control" [(ngModel)]="lugar.calle"/> <label>Ciudad</label> <input type="text" class="form-control" [(ngModel)]="lugar.ciudad"/> <label>País</label> <input type="text" class="form-control" [(ngModel)]="lugar.pais"/>
y el archivo crear.component.ts
obtenerDireccion(result) { this.lugar.calle = result.address_components[1].long_name + ' ' + result.address_components[0].long_name; this.lugar.ciudad = result.address_components[3].long_name; this.lugar.pais = result.address_components[5].long_name; }
ayuda, me sale este error al escribir
Failed to load Redirect from ‘ to ‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘’ is therefore not allowed access.
results$: Observable<any>; private searchField: FormControl; constructor(private lugaresService: LugaresService, private route: ActivatedRoute, private http: Http) { this.id = this.route.snapshot.params['id']; if (this.id !== 'new') { this.lugaresService.getLugar(this.id).valueChanges() .subscribe((lugar) => { this.lugar = lugar; }); } const URL = 'https://map.google.com/maps/api/geocode/json'; this.searchField = new FormControl(); this.results$ = this.searchField.valueChanges .debounceTime(500) .switchMap(query => this.http.get(`${URL}?address=${query}`)) .map(response => response.json()) .map(response => response.results); }
///////////////////
<div class="card card-body bg-light"> <form> <label>Buscar direccion en google maps</label> <input type="text" class="form-control" [formControl]="searchField"/> </form> <ul class="list-group"> <li class="list-group-item" *ngFor="let result of results$ | async">{{result.formatted_address}}</li> </ul> </div>