Im learning Angular 4 and have run into a problem that I cannot seem to find a solution to. Here is the context:
I have a simple app that displays info about US Presidents.
The backend is a rest API provided by webapi...this works fine.
The front end is an Angular app.
Ive distilled the problem down to 3 components, 1 data service and 1 model.
Here is the model:
export class President {
constructor(
public id: number,
public presidentName: string,
public presidentNumber: number,
public yearsServed: string,
public partyAffiliation: string,
public spouse: string) {}
}
The 3 components are
1. SearchComponent
2. HomeComponent
3. PresidentComponent
When the app bootstraps, it loads the ApplicationComponent - it is the root component:
import { Component, ViewEncapsulation } from '#angular/core';
#Component({
selector: 'my-app',
template: `
<search-component></search-component>
<home-component></home-component>
`
})
export class ApplicationComponent {}
PresidentComponent is a child component of HomeComponent. When home component loads, it makes an http call to the api to get a list of presidents and renders 1 presidentComponent for each row returned. This works fine.
What Im trying to do is implement a search feature where the dataService exposes an EventEmitter and provides the search method as shown here:
import { Injectable, EventEmitter, Output } from '#angular/core'
import { President } from '../models/President'
import { Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class DataService {
constructor(private http: Http) {
}
searchEvent: EventEmitter<any> = new EventEmitter();
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any): Observable<President[]> {
let encParams = encodeParams(params);
console.log(encParams);
return this.http
.get(this.presidentUrl, {search: encParams})
.map(response => response.json());
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this.http.get(this.presidentUrl)
.map(response => response.json());
}
}
/**
* Encodes the object into a valid query string.
* this function is from the book Angular2 Development with TypeScript
*/
function encodeParams(params: any): URLSearchParams {
return Object.keys(params)
.filter(key => params[key])
.reduce((accum: URLSearchParams, key: string) => {
accum.append(key, params[key]);
return accum;
}, new URLSearchParams());
}
The Search Component houses the search form and when the search button is clicked, it executes the onSearch() function and calls emit on the data service:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.searchEvent.emit(this.formModel.value);
}
}
Then, in the HomeComponent, I want to subscribe to this event and execute a search via the dataservice when it fires:
ngOnInit(): void {
//when component loads, get list of presidents
this.dataService.getPresidents()
.subscribe(
presidents => {
console.log('sub');
this.presidents = presidents;
},
error => console.error(error)
)
//when search event is fired, do a search
this.dataService.searchEvent
.subscribe(
params => {
console.log('in home.ts subscribe ' + JSON.stringify(params));
this.result = this.dataService.search(params);
},
err => console.log("cant get presidents. error code: %s, URL: %s"),
() => console.log('done')
);
}
When I run this in the browser, everything works except the http call is never executed. If I subscribe() to the http.get call in the dataservice itself, it executes but why should I have to do that when I have a subscription being setup on the HomeComponent?
I want to handle the Observable in the HomeComponent and update the list of presidents that is being displayed in the UI based on the search result.
Any advice is greatly appreciated.
The entire idea of using EventEmitter in the service is not right. The EventEmitter should be used with #Output properties to send data from the child component to its parent.
Even though the EventEmitter is a subclass of the Subject, you shouldn't be using it in services. So inject the service into your component, subscribe to its observable in the component, and emit an event using EventEmitter to the parent component if need be.
In the code this.result = this.dataService.search(params);, result is an observable. You have not made a subscription.
In that case you should have used the async pipe to display the data.
Why not use Subject from rxjs. Here is what i am proposing:
DataService:
import { Observable, Subject } from "rxjs";
import 'rxjs/add/operator/catch';
#Injectable()
export class DataService {
private _dataSubject = new Subject();
constructor(private http: Http) {
this.http.get(this.presidentUrl)
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
);
}
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any){
let encParams = encodeParams(params);
console.log(encParams);
this.http
.get(this.presidentUrl, {search: encParams})
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this._dataSubject;
}
SearchComponent:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.search(this.formModel.value);
}
}
With these modifications you should be able to have only 1 subscriber in homeCompoent and then get new data emitted every time onSearch() is called.
Related
I am checking the Network Status in my Angular application from network.service.ts
// network.service.ts
import { Injectable } from '#angular/core';
import { BehaviorSubject } from "rxjs";
#Injectable()
export class NetworkStatusService {
public status: BehaviorSubject<any> = new BehaviorSubject<any>(null);
public appStatus() {
window.addEventListener('online', this.networkStatusChanged.bind(this));
window.addEventListener('offline', this.networkStatusChanged.bind(this));
}
public networkStatusChanged(): void {
this.status.next(!navigator.onLine);
}
}
In my component, I am injecting this service and in ngOnInit,
I am calling this appStatus method of the service and then subscribe to status (BehaviorSubject) to get the value.
In my component:
public ngOnInit() {
this.networkService.appStatus();
this.networkService.status.subscribe((x)=>{
console.log('status here', x);
if(x) {
// do something
}
});
}
This works and logs the boolean value whenever the application online/offline. But the problem is I would have to call this method and then subscribe & unsubscribe in pretty much every component.
I know addEventListener does not return a value but is there a way to refactor this, so that I just call appStatus() from the component and it returns a boolean value (true/false) whenever the application is offline/online?
You can just create a getter in any component where you want to call appStatus, it will return value of network status.
public get appStatus () {
return navigator.onLine
}
But if you need to listen every time BehaviorSubject emits value, u need to subscribe.
Add your listeners inside service class constructor instead of appStatus() function. This way you don't have to call it every time or from every component.
import { Injectable } from '#angular/core';
import { BehaviorSubject } from "rxjs";
#Injectable()
export class NetworkStatusService {
public status: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor() {
window.addEventListener('online', this.networkStatusChanged.bind(this));
window.addEventListener('offline', this.networkStatusChanged.bind(this));
}
public networkStatusChanged(): void {
this.status.next(!navigator.onLine);
}
}
Now subscribe this.networkService.status from any component as you are doing currently.
I'm lazy loading data using a fake backend. The backend returns an array. Because I want to lazy load the data I'm buffering it every 100 records. The event that will trigger the call for getting more data is going to be a custom event, but for the time being I'm testing it with a button.
multiselect.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders, HttpResponse } from '#angular/common/http';
import { BehaviorSubject } from 'rxjs';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
export interface ProductCategory {
id: number;
name: string;
}
#Injectable({
providedIn: 'root'
})
export class MultiSelectService {
constructor(private http: HttpClient) { }
private readonly categorySubject = new BehaviorSubject(undefined);
readonly categories$ = this.categorySubject.asObservable();
// URL to web api - mocked. Categories is the object present in the mock server file
private categoriesUrl = 'api/categories';
/** GET heroes from the server */
getCategories(): void {
this.http.get<Array<ProductCategory>>(this.categoriesUrl, httpOptions)
.subscribe( data => {
data.map( category => this.categorySubject.next(category));
this.categorySubject.subscribe( xyz => console.log(xyz));
});
}
}
multiselect.component.ts
import { Component, AfterViewInit } from '#angular/core';
import { zip, Observable, fromEvent } from 'rxjs';
import { MultiSelectService, ProductCategory } from './multiselect.service';
import { bufferCount, startWith, map, scan } from 'rxjs/operators';
#Component({
selector: 'multiselect',
templateUrl: './multiselect.component.html',
styleUrls: ['./multiselect.component.scss']
})
export class MultiselectComponent implements AfterViewInit {
SLICE_SIZE = 100;
constructor(private data: MultiSelectService) { }
ngAfterViewInit() {
const loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
this.data.getCategories(); // loads the data
zip(
this.data.categories$.pipe(bufferCount(this.SLICE_SIZE)),
loadMore$.pipe(startWith(0)),
).pipe(
map(results => results[0]),
scan((acc, chunk) => [...acc, ...chunk], []),
).subscribe({
next: v => console.log(v),
complete: () => console.log('complete'),
});
}
}
multiselect.component.html
<button>Fetch more results</button>
1) The elements count is 429, and I'm showing them in batches of 100 elements. After four clicks (4 x 100) the last 29 are never shown. What am I missing to display the last 29 elements? I was able to check that the subject produced all the values.
2) Is there any better way of achieving the same functionality that I'm developing here? I'm mapping (it can be a forEach loop instead) the array due to the fact that after requesting the data I will only get one item (the array) with all the elements (429). That won't allow me to buffer the items if I want to do lazy loading.
3) I'm required to provide an initial value for the behaviorSubject, is there any way to avoid that?
Thanks in advance!
This seems like a very convoluted way of doing things
data.map( category => this.categorySubject.next(category));
This line returns a new array the same length as data containing undefined in each slot, if you want to create an observable from an array then you go
this.categories$ = from(data);
A BehaviorSubject with no initial value is a Subject.
All that aside you already have your data in an array, no need to create a new observable from it.
Have a BehaviorSubject with a number in it and each key press increment the number and use a combineLatest to get the amout of elements you want from the array.
combineLatest(categories$, page$).pipe(map(([categories, page]) => categories.filter((item, index) => index < page * pageSize)))
Is all you really need to do where you start page at 1 and increment it each button click.
I am trying to get data from Firebase by the list function and to use it in Json format (to load markers on my Google Map). But I'm facing an issue with the returned snapshots: Property 'map' does not exist on type '{}'
My imports:
import { Component, ViewChild, ElementRef } from '#angular/core';
import { NavController, IonicPage } from 'ionic-angular';
import { Geolocation } from '#ionic-native/geolocation';
import { AngularFireDatabase } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
And the code snippet with the error:
markers: Observable<any[]>;
constructor(public navCtrl: NavController,
public geolocation: Geolocation,
public db: AngularFireDatabase) {
this.markers = this.db.list("empresa/", {preserveSnapshot: true})
.map(snapshots => {
return snapshots.map(snapshot => { // this second .map displays the error
return snapshot.val();
})
.subscribe((marcas)=> {
console.log(marcas);
});
});
}
Because of this, I think, the subscribe does not display anything on the console. I also tried to import in some different ways the rxjs, as in this link Property 'map' does not exist on type 'Observable<Response>', but without success.
Seems like a migration issue - since AF 5.0 list returns an AngularFireList<T> service, not FirebaseListObservable, you have to call an action on it to stream data into an observable collection, e.g.:
const subscription = this.db.list("empresa/").snapshotChanges()
.map(snapshots => {
return snapshots.map(snapshot => snapshot.payload.val());
})
.subscribe(values => {
console.log(values);
this.markers = values;
});
Note also that the subscribe is chained after the outer map, not the inner as in your example; and this.markers are being assigned results inside the subscription, so you have to change their type from Observable<any[]> to any[] (or whatever they are, just not observable).
See 3. Retrieving data as lists in the docs for details; there's an example at the end of the page on using the listed items inside an angular template with async pipe, where you don't need to subscribe, then you'd keep this.markers as Observable<any[]> and the rest would be:
this.markers = this.db.list("empresa/").snapshotChanges()
.map(snapshots => {
return snapshots.map(snapshot => snapshot.payload.val());
});
This is preferred as the async pipe manages the subscription for you (because if you are subscribing manually you should also unsubscribe).
I'm trying to create a user profile service for an Angular 4 project and struggling a little with how to properly initialize and update the observable Profile object. Currently, when the user authenticates (via Firebase), AuthService passes the user's auth info to UserProfileService via the latter's initialize() function. UserProfileService then looks up the user's profile (or creates one if none exists yet) and populates a public observable with the profile.
The problem I'm running into is with other parts of the application trying to subscribe to the profile observable before all this has happened. I'd originally been initializing the observable via ...
public profileObservable: UserProfile = null;
... which of course resulted in a "subscribe() does not exist on null" error, so I changed it to ...
public profileObservable: Observable<UserProfile> = Observable.of();
This at least doesn't throw any errors, but anything that subscribes to profileObservable before I've mapped the Firebase object to it never updates.
Complete code for user-profile.service.ts below. I'm still struggling to get my head around how some of this is meant to work, so hopefully someone can shed some light. Thanks!
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { FirebaseListObservable, FirebaseObjectObservable, AngularFireDatabase } from 'angularfire2/database';
import * as firebase from 'firebase/app';
export class UserProfile {
$exists: Function;
display_name: string;
created_at: Date;
}
#Injectable()
export class UserProfileService {
private basePath: string = '/user-profiles';
private profileRef: FirebaseObjectObservable<UserProfile>;
public profileObservable: Observable<UserProfile> = Observable.of();
constructor(private db: AngularFireDatabase) {
// This subscription will never return anything
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
this.profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
const subscription = this.profileRef.subscribe(profile => {
if (!profile.$exists()) {
this.profileRef.update({
display_name: auth.displayName || auth.email,
created_at: new Date().toString(),
});
} else subscription.unsubscribe();
});
this.profileObservable = this.profileRef.map(profile => profile);
// This subscription will return the profile once it's retrieved (and any updates)
this.profileObservable.subscribe(profile => console.log(profile));
}
};
You must not change observable references once you constructed them. The way I found to properly decouple subscribers from the datasource is to use an intermediate Subject, which is both an observer and an observable.
Your code would look something like this:
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
...
export class UserProfileService {
...
public profileObservable = new Subject<UserProfile>();
constructor(private db: AngularFireDatabase) {
// This subscription now works
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
const profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
...
profileRef.subscribe(this.profileObservable);
}
};
I'm using DevExpress' DevExtreme with Angular2. I have a data grid (below) that lists states and asks the user to select some states. It is possible that some states have already been stored in the database. How do I set the previously selected states? I can see in the documentation that I should use dataGrid.instance.selectRows(arrayOfPreviouslySelectedStates) but dataGrid is instantiated sometime after I try to set it which is in the ngOnInit().
My HTML grid:
<dx-data-grid #statesGrid id="statesContainer" [dataSource]="states" [selectedRowKeys]="[]" [hoverStateEnabled]="true" [showBorders]="true" [showColumnLines]="true" [showRowLines]="true" [rowAlternationEnabled]="true">
<dxo-sorting mode="multiple"></dxo-sorting>
<dxo-selection mode="multiple" [deferred]="true"></dxo-selection>
<dxo-paging [pageSize]="10"></dxo-paging>
<dxo-pager [showPageSizeSelector]="true" [allowedPageSizes]="[5, 10, 20]" [showInfo]="true"></dxo-pager>
<dxo-filter-row [visible]="true"></dxo-filter-row>
<dxi-column dataField="abbreviation" [width]="100"></dxi-column>
<dxi-column dataField="name"></dxi-column>
</dx-data-grid>
My componenet:
import 'rxjs/add/operator/switchMap';
import { Component, OnInit, ViewContainerRef, ViewChild } from '#angular/core';
import { CompanyService } from './../../../shared/services/company.service';
import { StateService } from './../../../shared/services/state.service';
import notify from 'devextreme/ui/notify';
import { DxDataGridModule, DxDataGridComponent } from 'devextreme-angular';
#Component({
selector: 'app-company-detail',
templateUrl: './company-detail.component.html'
})
export class CompanyDetailComponent implements OnInit {
#ViewChild(DxDataGridComponent) dataGrid: DxDataGridComponent;
companyStates: Array<ICompanyState>;
states: Array<IState>;
constructor(private CompanyService: CompanyService, private StateService: StateService) { }
ngOnInit() {
this.StateService.getStates().subscribe((states) => {
this.getSelectedStates();
this.states = states
});
}
public getSelectedStates = (): void => {
this.CompanyService.getStates(id).subscribe((states) => {
let preselectedStates: Array<IState> = this.companyStates.map((state) => {
return { abbreviation: state.Abbreviation, name: state.Name }
});
// I get an error here that says that dataGrid is undefined.
this.dataGrid.instance.selectRows(preselectedStates, false);
}
}
}
Thanks to #yurzui 's comment I was able to figure out my problems in the following way. [selectedRowKeys] deals with all preselection. It's "problem" is that it doesn't update itself when additional selections are made. So, I listened for onSelectionChanged and passed the event, which contains data about many things regarding selection, into my custom function which updates the selectedStates which I then use to save the data to the database when the save button is clicked.
Gets the preselected states from the database
public getCompanyStates = (): void => {
this.CompanyService.getStates().subscribe((states) => {
this.selectedStates = states;
});
}
Event handler
public onSelectionChanged = (e): void => {
this.selectedStates = e.selectedRowKeys;
}
The dx-data-grid portion of the HTML
<dx-data-grid #statesGrid id="statesContainer"
(onSelectionChanged)="onSelectionChanged($event)"
[selectedRowKeys]="selectedStates"
[dataSource]="states">
...
</dx-data-grid>