making API calls in angular redux app - json

I want to fetch json data from a server in a basic angular-redux todo app.Also please do explain how the data flow happens from the store.If u can kindly refer any blogs on the matter,it would be great.I could not make a lot of sense from ng2-redux or ngrx.Thank you in advance.

You should make API calls in Middleware. Read this book, its free, it will clear most of your doubts, it did it for me when I started learning.

You should make API calles (as knowen as side effects) with a middleware like Epic.
Let's considere an example of todo app that you need to get the todos from the server:
const BASE_URL = "https://some-server/api/";
#Injectable()
export class TodoEpics implements EpicMiddleware {
constructor(private httpService: HttpClient) {
}
#dispatch()
startLoading() {
return changeTodoStatus("loading");
}
getTodosEpic = (action$: ActionsObservable<GetTodosAction>, state$: StateObservable<AppState>): Observable<Action> => {
return action$.pipe(
ofType(todoConstants.GET_TODOS),
tap((action) => this.startLoading()),
mergeMap(action => this.httpService.get<GetTodosResponse>(`${BASE_URL}/todos`).pipe(
map((response) => getTodosSucceeded(response.items)),
catchError(error => of(getTodosFailed(error)))
))
);
}
getEpics(): Epic[] {
return [
this.getTodosEpic
];
}
}
and in the main store module:
import { NgModule } from '#angular/core';
import { NgReduxModule, NgRedux, DevToolsExtension } from '#angular-redux/store';
import { createLogger } from 'redux-logger';
import { AppState } from './models';
import { rootReducer } from './reducers';
import { TodoEpics } from "../todo-list/todo-list-state-management/epics";
import { combineEpics, createEpicMiddleware } from "redux-observable";
import { environment } from "../../environments/environment";
const epicMiddleware = createEpicMiddleware();
#NgModule({
imports: [NgReduxModule],
providers: [
TodoEpics
]
})
export class StoreModule {
constructor(private store: NgRedux<AppState>, private todoEpics: TodoEpics) {
const rootEpic = combineEpics(
...this.todoEpics.getEpics()
);
const middelwares = [epicMiddleware]
const devMiddelwares = [...middelwares, createLogger()];
const prodMiddelwares = [...middelwares];
store.configureStore(
rootReducer,
environment.production ? prodMiddelwares : devMiddelwares);
epicMiddleware.run(rootEpic)
}
}
a complete example can be found here: Todo app using angular-redux, redux-observable and epics

Related

Parse a json data from internal json file using Angular throwns error

I tried to get json from tne internal json file within angular.
with this service (village.service):
import { Injectable, OnInit } from '#angular/core';
import { Http, Response } from '#angular/http';
import { environment } from '../../environments/environment';
import { Observable } from 'rxjs'
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
#Injectable()
export class RecordsService {
data: any;
constructor(private http: Http) { }
getVillages(id) {
return this.http.get('../assets/data/villages.json')
.map(data => {
this.data = data.json();
return data.json();
}, err => {
if (err) {
return err.json();
}
});
}
}
and under commponet i put the:
ngOnInit() {
this.getVillages();
....
}
and here to load as the chain dropdown
onSubDistrictSelected(subDistrictId: number) {
if (subDistrictId) {
this.onLoading.emit(true);
this.customer.subDistrict = this.subDistricts.filter(c => (c.id == subDistrictId))[0].name;
this.customer.sdid = subDistrictId;
this.customer.subDistrictId = subDistrictId;
this.villages = this.getVillages().filter((item) => {
return item.subDistrictId === Number(subDistrictId)
});
this.onLoading.emit(false);
}
}
I got error when compile said: this.getVillages is not function, But is working correctly if i put the json value inside the component file:
getVillages() {
return [
{ json_data}
]
}
What I want to achieved is I want to used the JSon file instead put directly inside the commponet.
Thanks,
getVillages is a method in service, so you need to instantiate the service before you use it.
First you need to provide the RecordsService in a module, like,
app.module.ts
...
providers : [
RecordsService
]
...
And in your component,
abc.component.ts
constructor(public recordService : RecordsService) {
}
ngOnInit() {
this.recordService.getVillages();
}
Let me know if you still get the error or have some different error.
EDIT:
getVillages() is returning an Observable, so you need to subscribe in order to use the data returned.
this.recordService.getVillages().subscribe( data => {
console.log(data);
} )

Get Config Parameters before starting components

I have a problem with getting the config parameters out of a json file before my app components start. For these components I need the config parameters.
There is no error message, but the app.component.ts is not initiated. Somewhere the execution stops. Reading the json works fine.
functiontest.dev.config.json
{
"name": "DE164813",
"node-uri": "http://localhost:4000"
}
Config.ts
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { Http, Response } from '#angular/http';
#Injectable()
export class Config {
private _env: Object
constructor(private http: Http) {
}
load() {
return new Promise((resolve, reject) => {
this.http.get('functiontest.dev.config.json')
.map(res => res.json())
.subscribe((env_data) => {
this._env = env_data;
console.log("got env", this._env);
})
});
}
getEnv(key: any) {
return this._env[key];
}
};
app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { FormsModule } from '#angular/forms';
import { HttpModule } from '#angular/http';
import { DatePickerModule } from 'ng2-datepicker';
import { Config } from './service/Config';
import { APP_INITIALIZER } from '#angular/core';
import {
NgModule,
ApplicationRef
} from '#angular/core';
import {
removeNgStyles,
createNewHosts,
createInputTransfer
} from '#angularclass/hmr';
import {
RouterModule,
PreloadAllModules
} from '#angular/router';
/*
* Platform and Environment providers/directives/pipes
*/
import { ENV_PROVIDERS } from './environment';
import { ROUTES } from './app.routes';
// App is our top level component
import { AppComponent } from './app.component';
import { APP_RESOLVER_PROVIDERS } from './app.resolver';
import { AppState, InternalStateType } from './app.service';
import { HomeComponent } from './home';
import { AboutComponent } from './about';
import { SensorTestComponent } from './sensortest';
import { TestReviewComponent } from './testreview';
import { NoContentComponent } from './no-content';
import { XLargeDirective } from './home/x-large';
import { ContractSelectComponent } from './contractselect/contractselect.component';
// Application wide providers
const APP_PROVIDERS = [
...APP_RESOLVER_PROVIDERS,
AppState
];
type StoreType = {
state: InternalStateType,
restoreInputValues: () => void,
disposeOldHosts: () => void
};
function initConfig(config: Config){
return () => config.load()
}
/**
* `AppModule` is the main entry point into Angular2's bootstraping process
*/
#NgModule({
bootstrap: [ AppComponent ],
declarations: [
AppComponent,
AboutComponent,
HomeComponent,
NoContentComponent,
XLargeDirective,
ContractSelectComponent,
SensorTestComponent,
TestReviewComponent
],
imports: [ // import Angular's modules
BrowserModule,
FormsModule,
HttpModule,
DatePickerModule,
RouterModule.forRoot(ROUTES, { useHash: true, preloadingStrategy: PreloadAllModules })
],
providers: [ // expose our Services and Providers into Angular's dependency injection
ENV_PROVIDERS,
APP_PROVIDERS,
Config,
{
provide: APP_INITIALIZER,
useFactory: initConfig,
deps: [Config],
multi: true
}
]
})
export class AppModule {
constructor(
public appRef: ApplicationRef,
public appState: AppState
) {
}
public hmrOnInit(store: StoreType) {
if (!store || !store.state) {
return;
}
console.log('HMR store', JSON.stringify(store, null, 2));
// set state
this.appState._state = store.state;
// set input values
if ('restoreInputValues' in store) {
let restoreInputValues = store.restoreInputValues;
setTimeout(restoreInputValues);
}
this.appRef.tick();
delete store.state;
delete store.restoreInputValues;
}
public hmrOnDestroy(store: StoreType) {
const cmpLocation = this.appRef.components.map((cmp) => cmp.location.nativeElement);
// save state
const state = this.appState._state;
store.state = state;
// recreate root elements
store.disposeOldHosts = createNewHosts(cmpLocation);
// save input values
store.restoreInputValues = createInputTransfer();
// remove styles
removeNgStyles();
}
public hmrAfterDestroy(store: StoreType) {
// display new elements
store.disposeOldHosts();
delete store.disposeOldHosts;
}
}
app.routes.ts
import { Routes, RouterModule } from '#angular/router';
import { HomeComponent } from './home';
import { ContractSelectComponent } from './contractselect/contractselect.component';
import { SensorTestComponent } from './sensortest';
import { TestReviewComponent } from './testreview';
import { AboutComponent } from './about';
import { NoContentComponent } from './no-content';
import { DataResolver } from './app.resolver';
export const ROUTES: Routes = [
{ path: '', component: ContractSelectComponent },
{ path: 'sensortest/:orderId', component: SensorTestComponent },
{ path: 'testreview', component: TestReviewComponent },
{ path: '**', component: NoContentComponent },
];
contractselect.component.ts
import { Component } from '#angular/core';
import { OrderResource } from '../service/OrderResource';
import { ContractSelect } from './contractselect';
import { Order } from '../model/Order';
import { Router } from '#angular/router';
import { NodeResource } from '../service/NodeResource'
import { NodeData } from '../model/NodeData';
#Component({
selector: 'contractselect',
providers: [OrderResource, NodeResource],
templateUrl: 'contractselect.component.html'
})
export class ContractSelectComponent {
//...
constructor(private _orderResource: OrderResource, private _router:Router, private _nodeResource: NodeResource) {
this.orders = new Array<Order>();
this.orderResource = _orderResource;
this.nodeResource = _nodeResource;
// set delay settings
this.delay = 1;
console.log("created ContractSelect Component");
}
// ...
}
My Angular is a little bit rusty, but I don't think that just because you've specified Config & config.load() as a dependency to the Angular DI framework, it will actually respect it's Promise-based nature and delay construction of components until the Promise is resolved.
Somebody with more Angular experience can probably comment more and provide a solution.
However, you'd be better served with getting that configuration data in a different fashion to the application than an HTTP call. Most probably, application load looks like this:
Client browser makes an HTTP request to your server and gets a webpage + a big bundle of JS, representing the single page Angular app.
Single page app then makes another HTTP request to the server to get the configuration.
Application is initialized based on on the config.
You have an extra HTTP request, which slows down the initial load, and adds all this extra complexity, and doesn't really buy you that much.
You'd be better served with having that data as a constant in your code, perhaps with different values for different environments. It's unlikely you're going to change it often, and in that case, doing a redeploy of your app will probably be done anyway. Depending on how your serving is done, the web server which serves the javascript could bake the values straight into the JS, dependent on it's configuration. It's much easier to control the environment and configuration of server-side components.

Ensure json configuration is loaded in Angular2 [duplicate]

Is there a way to pass arguments rendered on the backend to angular2 bootstrap method? I want to set http header for all requests using BaseRequestOptions with value provided from the backend. My main.ts file looks like this:
import { bootstrap } from '#angular/platform-browser-dynamic';
import { AppComponent } from "./app.component.ts";
bootstrap(AppComponent);
I found how to pass this arguments to root component (https://stackoverflow.com/a/35553650/3455681), but i need it when I'm fireing bootstrap method... Any ideas?
edit:
webpack.config.js content:
module.exports = {
entry: {
app: "./Scripts/app/main.ts"
},
output: {
filename: "./Scripts/build/[name].js"
},
resolve: {
extensions: ["", ".ts", ".js"]
},
module: {
loaders: [
{
test: /\.ts$/,
loader: 'ts-loader'
}
]
}
};
update2
Plunker example
update AoT
To work with AoT the factory closure needs to be moved out
function loadContext(context: ContextService) {
return () => context.load();
}
#NgModule({
...
providers: [ ..., ContextService, { provide: APP_INITIALIZER, useFactory: loadContext, deps: [ContextService], multi: true } ],
See also https://github.com/angular/angular/issues/11262
update an RC.6 and 2.0.0 final example
function configServiceFactory (config: ConfigService) {
return () => config.load();
}
#NgModule({
declarations: [AppComponent],
imports: [BrowserModule,
routes,
FormsModule,
HttpModule],
providers: [AuthService,
Title,
appRoutingProviders,
ConfigService,
{ provide: APP_INITIALIZER,
useFactory: configServiceFactory
deps: [ConfigService],
multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
If there is no need to wait for the initialization to complete, the constructor of `class AppModule {} can also be used:
class AppModule {
constructor(/*inject required dependencies */) {...}
}
hint (cyclic dependency)
For example injecting the router can cause cyclic dependencies.
To work around, inject the Injector and get the dependency by
this.myDep = injector.get(MyDependency);
instead of injecting MyDependency directly like:
#Injectable()
export class ConfigService {
private router:Router;
constructor(/*private router:Router*/ injector:Injector) {
setTimeout(() => this.router = injector.get(Router));
}
}
update
This should work the same in RC.5 but instead add the provider to providers: [...] of the root module instead of bootstrap(...)
(not tested myself yet).
update
An interesting approach to do it entirely inside Angular is explained here https://github.com/angular/angular/issues/9047#issuecomment-224075188
You can use APP_INITIALIZER which will execute a function when the
app is initialized and delay what it provides if the function returns
a promise. This means the app can be initializing without quite so
much latency and you can also use the existing services and framework
features.
As an example, suppose you have a multi-tenanted solution where the
site info relies on the domain name it's being served from. This can
be [name].letterpress.com or a custom domain which is matched on the
full hostname. We can hide the fact that this is behind a promise by
using APP_INITIALIZER.
In bootstrap:
{provide: APP_INITIALIZER, useFactory: (sites:SitesService) => () => sites.load(), deps:[SitesService, HTTP_PROVIDERS], multi: true}),
sites.service.ts:
#Injectable()
export class SitesService {
public current:Site;
constructor(private http:Http, private config:Config) { }
load():Promise<Site> {
var url:string;
var pos = location.hostname.lastIndexOf(this.config.rootDomain);
var url = (pos === -1)
? this.config.apiEndpoint + '/sites?host=' + location.hostname
: this.config.apiEndpoint + '/sites/' + location.hostname.substr(0, pos);
var promise = this.http.get(url).map(res => res.json()).toPromise();
promise.then(site => this.current = site);
return promise;
}
NOTE: config is just a custom config class. rootDomain would be
'.letterpress.com' for this example and would allow things like
aptaincodeman.letterpress.com.
Any components and other services can now have Site injected into
them and use the .current property which will be a concrete
populated object with no need to wait on any promise within the app.
This approach seemed to cut the startup latency which was otherwise
quite noticeable if you were waiting for the large Angular bundle to
load and then another http request before the bootstrap even began.
original
You can pass it using Angulars dependency injection:
var headers = ... // get the headers from the server
bootstrap(AppComponent, [{provide: 'headers', useValue: headers})]);
class SomeComponentOrService {
constructor(#Inject('headers') private headers) {}
}
or provide prepared BaseRequestOptions directly like
class MyRequestOptions extends BaseRequestOptions {
constructor (private headers) {
super();
}
}
var values = ... // get the headers from the server
var headers = new MyRequestOptions(values);
bootstrap(AppComponent, [{provide: BaseRequestOptions, useValue: headers})]);
In Angular2 final release, the APP_INITIALIZER provider can be used to achieve what you want.
I wrote a Gist with a complete example: https://gist.github.com/fernandohu/122e88c3bcd210bbe41c608c36306db9
The gist example is reading from JSON files but can be easily changed to read from a REST endpoint.
What you need, is basically:
a) Set up APP_INITIALIZER in your existent module file:
import { APP_INITIALIZER } from '#angular/core';
import { BackendRequestClass } from './backend.request';
import { HttpModule } from '#angular/http';
...
#NgModule({
imports: [
...
HttpModule
],
...
providers: [
...
...
BackendRequestClass,
{ provide: APP_INITIALIZER, useFactory: (config: BackendRequestClass) => () => config.load(), deps: [BackendRequestClass], multi: true }
],
...
});
These lines will call the load() method from BackendRequestClass class before your application is started.
Make sure you set "HttpModule" in "imports" section if you want to make http calls to the backend using angular2 built in library.
b) Create a class and name the file "backend.request.ts":
import { Inject, Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Injectable()
export class BackendRequestClass {
private result: Object = null;
constructor(private http: Http) {
}
public getResult() {
return this.result;
}
public load() {
return new Promise((resolve, reject) => {
this.http.get('http://address/of/your/backend/endpoint').map( res => res.json() ).catch((error: any):any => {
reject(false);
return Observable.throw(error.json().error || 'Server error');
}).subscribe( (callResult) => {
this.result = callResult;
resolve(true);
});
});
}
}
c) To read the contents of the backend call, you just need to inject the BackendRequestClass into any class of you choice and call getResult(). Example:
import { BackendRequestClass } from './backend.request';
export class AnyClass {
constructor(private backendRequest: BackendRequestClass) {
// note that BackendRequestClass is injected into a private property of AnyClass
}
anyMethod() {
this.backendRequest.getResult(); // This should return the data you want
}
}
Let me know if this solves your problem.
Instead of having your entry point calling bootstrap itself, you could create and export a function that does the work:
export function doBootstrap(data: any) {
platformBrowserDynamic([{provide: Params, useValue: new Params(data)}])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
}
You could also place this function on the global object, depending on your setup (webpack/SystemJS). It also is AOT-compatible.
This has the added benefit to delay the bootstrap, whenit makes sense. For instance, when you retrieve this user data as an AJAX call after the user fills out a form. Just call the exported bootstrap function with this data.
The only way to do that is to provide these values when defining your providers:
bootstrap(AppComponent, [
provide(RequestOptions, { useFactory: () => {
return new CustomRequestOptions(/* parameters here */);
});
]);
Then you can use these parameters in your CustomRequestOptions class:
export class AppRequestOptions extends BaseRequestOptions {
constructor(parameters) {
this.parameters = parameters;
}
}
If you get these parameters from an AJAX request, you need to bootstrap asynchronously this way:
var appProviders = [ HTTP_PROVIDERS ]
var app = platform(BROWSER_PROVIDERS)
.application([BROWSER_APP_PROVIDERS, appProviders]);
var http = app.injector.get(Http);
http.get('http://.../some path').flatMap((parameters) => {
return app.bootstrap(appComponentType, [
provide(RequestOptions, { useFactory: () => {
return new CustomRequestOptions(/* parameters here */);
}})
]);
}).toPromise();
See this question:
angular2 bootstrap with data from ajax call(s)
Edit
Since you have your data in the HTML you could use the following.
You can import a function and call it with parameters.
Here is a sample of the main module that bootstraps your application:
import {bootstrap} from '...';
import {provide} from '...';
import {AppComponent} from '...';
export function main(params) {
bootstrap(AppComponent, [
provide(RequestOptions, { useFactory: () => {
return new CustomRequestOptions(params);
});
]);
}
Then you can import it from your HTML main page like this:
<script>
var params = {"token": "#User.Token", "xxx": "#User.Yyy"};
System.import('app/main').then((module) => {
module.main(params);
});
</script>
See this question: Pass Constant Values to Angular from _layout.cshtml.

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)"))}
});
}

Google Places with Observables in Angular2

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 ;)