Google Places with Observables in Angular2 - google-maps

I try to use Google Places with Observables in Angular 2.
To do that, I included the Google scripts in the index.html and then I get some inspiration with Observables from http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html
<!-- Script included in index.html -->
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
You can see the whole application there: https://embed.plnkr.co/LQaag2/
I think there is an issue with the events. For example, when the user type "P", nothing appears. But if he clicks on the page or he types "a", then he will see the results of places starting by "P".
Do you have an idea why?
app/main.ts
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app.module';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap'
platformBrowserDynamic().bootstrapModule(AppModule);
app/app.module.ts
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { JsonpModule } from '#angular/http';
import { ReactiveFormsModule } from '#angular/forms';
import { AppComponent } from './app.component';
import { GoogleSearchComponent } from './google-search.component'
import { GoogleService } from './google.service';
#NgModule({
imports: [BrowserModule, JsonpModule, ReactiveFormsModule],
declarations: [AppComponent, GoogleSearchComponent],
providers: [GoogleService],
bootstrap: [AppComponent]
})
export class AppModule {}
app/app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: 'app/app.component.html'
})
export class AppComponent { }
app/app.component.html
<google-search></google-search>
app/google-place.ts
export class GooglePlace {
constructor(public id: string,
public description: string
) {}
}
app/google-search.component.ts
import { Component } from '#angular/core';
import { FormControl } from '#angular/forms';
import { GoogleService } from './google.service';
import { GooglePlace } from './google-place';
#Component({
selector: 'google-search',
template: `
<div>
<h2>Google Search</h2>
<input type="text" [formControl]="term">
<ul>
<li *ngFor="let item of items | async">{{item.description}}</li>
</ul>
</div>
`
})
export class GoogleSearchComponent {
items: Observable<Array<GooglePlace>>;
term = new FormControl();
constructor(private googleService: GoogleService) {}
ngOnInit() {
this.items = this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(term => this.googleService.search(term));
}
}
app/google.service.ts
import { Injectable } from '#angular/core';
import { GooglePlace } from './google-place';
import { Observable } from 'rxjs/Observable';
declare var google: any;
#Injectable()
export class GoogleService {
search(term: string) {
return new Observable<GooglePlace[]>(observer => {
let result: GooglePlace[] = [];
let displaySuggestions = function(predictions: any, status: string) {
if (status != google.maps.places.PlacesServiceStatus.OK) {
alert(status);
return;
}
predictions.forEach(function(prediction: any) {
result.push(new GooglePlace(prediction.place_id, prediction.description));
});
observer.next(result);
observer.complete();
};
if (term) {
let service = new google.maps.places.AutocompleteService();
service.getQueryPredictions({ input: term }, displaySuggestions);
}
});
}
}

don't know if you're still interested but I was facing the same issue today with the bootstrap typeahead. I think I found a solution although I don't think it's the way one should do it.
Anyway, my approach was to gather the data and let the data display as if it was static.
ngOnInit(): void {
//this.recursiveTimeout();
this.items = this.searchTermStream
.debounceTime(300)
.distinctUntilChanged()
.switchMap((term: string) => this.placesService.search(term))
.catch(() => {
this.searchFailed = true;
return Observable.of([])
}
)
this.items.subscribe(res => {
this.places = res;
//places is a string array and stores all found places , in your case it
would be an array of GooglePlace
console.log(this.places);
});
}
Then you sould be able to access the data as soon as it is available.

I just had a very similar problem with google maps. I will share here my answer, all the same, although it is so late.
The problem is because the callback function displaySuggestions of the google maps getQueryPredictions is called outside of the 'angular zone', and so angular doesn't correctly detect the changes inside of it.
The solution is relatively simple. Just 4 little changes to the app/google.service.ts. See the comments.
// import NgZone
import { Injectable, NgZone } from '#angular/core';
import { GooglePlace } from './google-place';
import { Observable } from 'rxjs/Observable';
declare var google: any;
#Injectable()
export class GoogleService {
// Inject NgZone in the constructor
constructor(private _ngZone: NgZone) {}
search(term: string) {
// save 'this' to a constant or alternatively bind it to the callback function
const self = this;
return new Observable<GooglePlace[]>(observer => {
const result: GooglePlace[] = [];
const displaySuggestions = function(predictions: any, status: string) {
if (status !== google.maps.places.PlacesServiceStatus.OK) {
console.log('GoogleService search: ', status);
return;
}
// Wrap the prediction in the zone
self._ngZone.run(function() {
predictions.forEach(function(prediction: any) {
result.push(
new GooglePlace(prediction.place_id, prediction.description)
);
});
observer.next(result);
observer.complete();
});
};
if (term) {
const service = new google.maps.places.AutocompleteService();
service.getQueryPredictions({ input: term }, displaySuggestions);
}
});
}
}
Edit: Perhaps you should take out your API key from the plunker, although i suppose that it might not be to serious of a problem, if it is a free one and was created exclusively for the purpose of the example...

I found an awful solution. In app/google-search.component.ts, I've added the following function :
recursiveTimeout(ms: number = 1000): void {
setTimeout(() => {
this.recursiveTimeout(ms);
}, ms);
}
Then in the ngOnInit function, I call recursiveTimeout:
ngOnInit(): void {
this.recursiveTimeout();
// ...
}
With this solution, when the user type "P" (for example):
The result will be fetched on the Google API
The result will be displayed just after the event recursiveTimeout is triggered (maximum 1000 ms)
I am open to any better solution ;)

Related

Angular 'Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.'

I'm creating an Angular app which shows list of projects and list of users from postgresql database, but I'm having issues with showing list of users in html.
The problem is that Angular is considering my array as an object no matter what I do.
The same code worked for projects but didn't work for users.
This is my service:
import { environment } from "../../../environments/environment";
import { Observable } from 'rxjs';
import { Projet } from '../modele/projet.model';
import { Test } from '../modele/test.model';
import { HttpParams,HttpClient } from "#angular/common/http";
import { Injectable } from "#angular/core";
import { map } from 'rxjs/operators';
import { User } from '../modele/user.model';
import { Financement } from '../modele/financement.model';
#Injectable()
export class WebService {
constructor(private httpClient: HttpClient) { }
serverUrl: string = "http://localhost:8080/"
get(url: string): Observable<any> {
return this.httpClient.get(this.serverUrl + url);
}
}
The component :
import { Component, OnInit } from '#angular/core';
import { User } from '../../shared/modele/user.model';
import { Router } from '#angular/router';
import { WebService } from '../../shared/sevices/web.service';
import { FormGroup, FormBuilder, FormControl, Validators, Form } from '#angular/forms';
#Component({
selector: 'app-show-users',
templateUrl: './show-users.component.html',
styleUrls: ['./show-users.component.scss']
})
export class ShowUsersComponent implements OnInit {
ngOnInit(): void {
this.getData();
}
usersList: Array<User>
user: User
myForm: FormGroup;
constructor(private webService: WebService, private formBuilder: FormBuilder,private router: Router) { }
getData(): void {
this.webService.get("showUsers").subscribe(res => {
let response = JSON.parse(JSON.stringify(res))
this.usersList = response.data
})
}
}
The html :
<tr *ngFor="let user of usersList">
<td>{{user.name}}</td>
<td>{{user.username}}</td>
<td>{{user.email}}</td>
</tr>
This is the server response :
server response
NB: the EXACT same code worked for the object PROJECT
You need to make sure that the variable you pass into *ngFor is an array. You can make sure of this with Array.from(v) and can also strip any keys of an Object that might be sent from the serverside with Object.values(v):
this.webService.get("showUsers").subscribe(res => {
this.usersList = Array.from(Object.values(res.data.body.data));
})
In my case, I have a simple approach, but I spent a lot of time. You could try this:
datas: any;
this.token = JSON.parse(window.localStorage.getItem('token'));
this.authService.getData(this.token.id).subscribe(data => {
this.datas = data;
})
In the HTML template just use this.datas.id, this.datas.username instead of an *ngFor
You don't need this code:
let response = JSON.parse(JSON.stringify(res))
this.usersList = response.data
simply use:
this.userlist = res
Youe complete method:
this.webService.get("showUsers").subscribe(res => {
this.userlist = res
});

Angular 6: use a service to get local json data

I have a movies.json that contain a list of movies and I want to create a MoviesServices to get the data where I want.
My MoviesServices:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { HttpErrorResponse } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class MoviesService {
movies: string[];
constructor(private httpService: HttpClient) {
this.getMovies();
}
getMovies() {
this.httpService.get('../../assets/movies.json').subscribe(
data => {
this.movies = data as string[];
console.log(this.movies); // My objects array
},
(err: HttpErrorResponse) => {
console.log(err.message);
}
);
console.log(this.movies); // Undefined
}
}
Firstly, I have no idea why the first console.log() works and the second not, can you tell me why ?
Here is my component where I need to get the data:
import { Component, OnInit } from '#angular/core';
import { MoviesService } from '../services/movies/movies.service';
#Component({
selector: 'app-movies',
templateUrl: './movies.component.html',
styleUrls: ['./movies.component.css']
})
export class MoviesComponent implements OnInit {
title = 'films-synopsys';
movies;
constructor(private myService: MoviesService) {}
ngOnInit() {
console.log(this.myService.movies); // Undefined
}
}
Of course this is not working. Can you tell me how must I do ? I'm newbie angular
So basically you need to return an Observable from your service and then subscribe to it from your Component. You can then assign your response to the Component property movies
Try this:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable()
export class MoviesService {
constructor(private httpService: HttpClient) { }
getMovies() {
return this.httpService.get('../../assets/movies.json');
}
}
And in your Component:
import { Component } from '#angular/core';
import { MoviesService } from './movies.service';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
title = 'films-synopsys';
movies;
constructor(private myService: MoviesService) {}
ngOnInit() {
this.myService.getMovies()
.subscribe(res => this.movies = res);
}
}
Here's a Sample StackBlitz for your ref.
Change your method to return an Observable which you can subscribe to:
import { Observable } from 'rxjs/Observable';
...
getMovies(): Observable<string []> {
this.httpService.get('../../assets/movies.json').subscribe(
data => {
this.movies = data as string[];
return this.movies;
},
(err: HttpErrorResponse) => {
console.log(err.message);
}
);
}
In your calling code:
import { Subscription } from 'rxjs/Subscription';
this.myService.getMovies().subscribe(movies => {
console.log(movies); // My objects array
}
The reason the first console log works is because you are doing it within an observable's subscription. Subscriptions have three states, Next, Error, Complete and so when you console log the first time, within the subscription next state you get the value that was pushed out from the event stream.
In your component the reason why it doesn't work is due to the fact that observables are lazy, and that you need to initialize the data by calling this.myService.getMovies() first to make the subscription happen.
A better way to do this would been to pass observables around and use async pipe in the html template.

Angular 2 api data

I want to get data from Riot API and display it in html view.
However, i can not "hold" this data in my variable. Console log show empty array.
I can see json data only in function scope.
I guess, i didn`t use observable function corretly, am i wrong?
Here is my component.
import { Component, OnInit } from '#angular/core';
import { FRIEND } from '../../services/_friends/mock-friends';
import { APIKEY } from '../../services/_lolapi/apikey';
import { Http, Response } from '#angular/http';
import { KeysPipe } from '../../pipes/key';
import { JsonPipe } from '#angular/common';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-friends',
templateUrl: './friends.component.html',
styleUrls: ['./friends.component.css']
})
export class FriendsComponent implements OnInit {
friends = FRIEND;
apikey = APIKEY;
nick: string[];
query: string;
private apiUrl =
'https://eun1.api.riotgames.com/lol/summoner/v3/summoners/by-name/';
data: Array<string> = [];
constructor(private http: Http) {
}
getFriendData(query) {
return this.http.get(query)
.map((res: Response) => res.json());
}
getContacts() {
this.getFriendData(this.query).subscribe(data => {
this.data = data;
console.log(this.data);
});
}
ngOnInit() {
for (let i of this.friends) {
this.query = `${this.apiUrl}${i.nick}${this.apikey}`;
this.getFriendData(this.query);
this.getContacts();
console.log(i.nick);
}
}
}
You don't need this.getFriendData(this.query) in ngOnInit as in the next line you call getContacts that wraps getFriendData.
Now, your API returns SummonerDTO - a complex object and you are trying to store it as an Array? That doesn't seem right.
Additionally, it think you want to store every result in an array, right?
In that case you should rather use:
this.data.push(data);

Angular 2/4 - Can't resolve all parameters for GameEditComponent: ([object Object], [object Object], ?)

I am developing the services of my application, but when I try to load the page it shows the following error:
Can't resolve all parameters for GameEditComponent: ([object Object],
[object Object], ?).
I tried in the service to put as an array or just leave any, but even then the error continued
game-edit.service.ts
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs';
#Injectable()
export class GameEditService {
constructor(private http: Http) { }
getGame(id): Observable<any> {
return this.http.get('http://localhost:8080/lightning/api/game' + id).map(res => res.json()).catch(error => {
throw new Error(error.message);
});
}
getManufactures(): Observable<any> {
return this.http.get('http://localhost:8080/lightning/api/manufacture').map(res => res.json()).catch(error => {
throw new Error(error.message);
});
}
getPlatforms(): Observable<any> {
return this.http.get('http://localhost:8080/lightning/api/platform').map(res => res.json()).catch(error => {
throw new Error(error.message);
});
}
}
game-edit.component.ts
import { ActivatedRoute, Params } from '#angular/router';
import { Component, OnInit } from '#angular/core';
import { GameEditService } from './game-edit.service';
#Component({
moduleId: module.id,
selector: 'app-game-edit',
templateUrl: './game-edit.component.html',
styleUrls: ['./game-edit.component.css', '../styles.css' ]
})
export class GameEditComponent implements OnInit {
constructor(private activatedRoute: ActivatedRoute, private gameEditService: GameEditService, private id) {
this.gameEditService.getPlatforms().subscribe(platforms => {
console.log(platforms);
}), erro => console.log(erro);
this.gameEditService.getManufactures().subscribe(manufactures => {
console.log(manufactures);
}), erro => console.log(erro);
}
ngOnInit() {
this.activatedRoute.params.subscribe((params: Params) => {
this.id = params['id'];
console.log(this.id);
});
this.gameEditService.getGame(this.id).subscribe(game => {
console.log(game);
}), erro => console.log(erro);
}
onSubmit(form){
console.log(form);
}
verificaValidTouched(campo){
return !campo.valid && campo.touched;
}
aplicaCssErro(campo){
return {
'subError': this.verificaValidTouched(campo)
}
}
}
This is the json that is coming, the first is for a selected game, the second is for the platforms and the third is for the manufacturers
json game selected
{
"id":1,
"name":"Street Fighter",
"category":"luta",
"price":20.5,
"quantity":1000,
"production":true,
"description":"descricao",
"image":"ps4.jpg",
"manufacture":
{
"id":1,
"name":"Sony",
"image":"ps4.jpg",
"imageFullPath":"http://localhost:8080/lightning/images/ps4.jpg"
}
}
json platforms
{
"id":1,
"name":"PC",
"image":"ps4.jpg",
"imageFullPath":"http://localhost:8080/lightning/images/ps4.jpg"
}
json manufactures
{
"id":1,
"name":"Sony",
"image":"ps4.jpg",
"imageFullPath":"http://localhost:8080/lightning/images/ps4.jpg"
}
Console
I'm using angular cli with with all packages in the most current versions.
I do not know if maybe this error is because of the platforms you have inside the game, or some other code problem, if you know something that could do to repair, I tried several solutions that I found through the internet, but none worked.
Thanks in advance.
The problem is the last argument in the component's constructor, private id. Angular will try to resolve this dependency, but can't find an injectable class for id. When looking at the code, I think there is no need to inject id into the constructor. Just define it as a property on your component:
// ... import statements
#Component({
moduleId: module.id,
selector: 'app-game-edit',
templateUrl: './game-edit.component.html',
styleUrls: ['./game-edit.component.css', '../styles.css' ]
})
export class GameEditComponent implements OnInit {
private id; // put the declaration of id here
// remove id declaration from the constructor, no need to inject it
constructor(private activatedRoute: ActivatedRoute,
private gameEditService: GameEditService) { // ...constructor code}
// other code
}
I solved it otherwise: My problem was that the HttpClient has a rare condition, it's not the same "import" line on the component that on the app.module...
On the Component is this:
import { HttpClient } from '#angular/common/http';
in app module is this:
import { HttpClientModule } from '#angular/common/http';

How can I sanitize css properties to use in template given from a data service

I need to generate sanitized css property to use with my component template to set the background image of the div:
<div *ngFor="let Item of Items"
[style.background-image]="Item.imageStyle
(click)="gotoDetail(Item.iditems)">
</div>
using data obtained through a data service. The component is:
import { Component } from '#angular/core';
import { Router } from '#angular/router';
import { DomSanitizer } from '#angular/platform-browser';
import { OnInit } from '#angular/core';
import { Item } from '../models/Item';
import { CollectionDataService } from '../services/CollectionData.service';
#Component({
selector: 'mainpage',
templateUrl: 'app/mainpage/mainpage.component.html',
styleUrls: ['app/mainpage/mainpage.component.css']
})
export class MainpageComponent implements OnInit {
Items: Item[];
ngOnInit() {
this.collectionDataService.getItems().subscribe(
Items => this.Items = Items
);
// Generates and sanitizes image links
this.Items.map(
(LItem) => LItem.imageStyle = this.sanitizer.bypassSecurityTrustStyle("url(template/images/"+LItem.iditems+".jpg)")
)
}
constructor(
private router: Router,
private sanitizer: DomSanitizer,
private collectionDataService: CollectionDataService
) {
}
gotoDetail($iditems: number): void {
this.router.navigate(['/viewer', $iditems]);
}
}
But it doesn't work because the statement that generates the sanitized property
this.Items.map(
(LItem) => LItem.imageStyle = this.sanitizer.bypassSecurityTrustStyle("url(template/images/"+LItem.iditems+".jpg)")
)
doesn't find the loaded data. The error that I'm seeing in the browser console is:
core.umd.js:3070 EXCEPTION: Uncaught (in promise): Error: Error in ./MainpageComponent class MainpageComponent_Host - inline template:0:0 caused by: Cannot read property 'map' of undefined
TypeError: Cannot read property 'map' of undefined
The data service is:
import { Injectable } from '#angular/core'
import { Http } from '#angular/http'
import { Item } from '../models/Item';
import { DomSanitizer } from '#angular/platform-browser';
#Injectable()
export class CollectionDataService {
constructor(
private http: Http,
private sanitizer: DomSanitizer
) { }
getItems() {
return this.http.get('app/mocksdata/items.json').map(
response => <Item[]>response.json().items
)
}
}
And the provided items.json:
{
"items": [{
"iditems": 1,
"imageStyle": ""
}, {
"iditems": 2,
"imageStyle": ""
}]
}
If I set static data in the component, instead of using the data service, everything works:
export class MainpageComponent implements OnInit {
Items: Item[];
ngOnInit() {
this.Items = [{
"iditems": 1,
"imageStyle": ""
}, {
"iditems": 2,
"imageStyle": ""
}]
// Generates and sanitizes image links
this.Items.map(
(LItem) => LItem.imageStyle = this.sanitizer.bypassSecurityTrustStyle("url(template/images/"+LItem.iditems+".jpg)")
)
}
How can I force the sanitizer statement to wait that the async data are fully loaded? Alternatively how can I generate sanitized properties directly in the service?
EDIT
The best answer comes from PatrickJane below:
Items: Item[] = [];
ngOnInit() {
this.collectionDataService.getItems().subscribe(Items => {
this.Items = Items;
this.Items.map(LItem => LItem.imageStyle = this.sanitizer.bypassSecurityTrustStyle("url(template/images/"+LItem.iditems+".jpg)"))}
});
}
I also solved this problem working directly in the service method (credits), but it is more verbose:
return this.http.get('app/mocksdata/items.json')
.map( (responseData) => {
return responseData.json().items;
})
.map(
(iitems: Array<any>) => {
let result:Array<Item> = [];
if (iitems) {
iitems.forEach((iitem) => {
iitem.imageStyle = this.sanitizer.bypassSecurityTrustStyle("url(template/images/"+iitem.iditems+".jpg)");
result.push(<Item>iitem);
});
}
return result;
}
)
The subscribe function is async so your map function called before the subscribe function run. So in this phase the array is undefined because you doesn't set any initial value.
The solution is to do this inside the subscribe function and to initialize the Items with empty array.
Items: Item[] = [];
ngOnInit() {
this.collectionDataService.getItems().subscribe(Items => {
this.Items = Items;
this.Items.map(LItem => LItem.imageStyle = this.sanitizer.bypassSecurityTrustStyle("url(template/images/"+LItem.iditems+".jpg)"))}
});
}