So, I probably don't understand Observables well. I have snippet like this, and would like to access todos stored in this service via function (from another component) defined in service. Unfortunately, I have no idea how to do this.
todos;
// fetching todos from api
fetchTodos() :Observable<Todo[]>{
return this.http.get<Todo[]>(api_url);
}
constructor(private http:HttpClient) {
this.fetchTodos()
.subscribe(data => this.todos = data)
}
To do it right solve your problem as follows.
SERVICE
import { BehaviorSubject, Observable } from 'rxjs';
/* make the plain object a subject you can subscribe to */
todos: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([]);
constructor(private http:HttpClient) {
/* get the data and put it in the subject */
/* every subscriber to this event will be updated */
this.fetchTodos().subscribe(data => this.todos.next(data));
}
getTodos(): Observable<Todo[]> {
return this.todos.asObservable();
}
// fetching todos from api
private fetchTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(api_url);
}
COMPONENT
constructor(private service: Service) {}
ngOnInit(): void {
/* here you go. this subscription gives you the current content of todos[] */
/* whenever it gets updated */
this.service.getTodos().subscribe(data => {
console.log(data);
});
}
PLEASE NOTE
Subscriptions to an Observable should always be finished when you leave a component. The best way to reach this goal in your case is:
modified COMPONENT
import { Subscription } from 'rxjs';
private subscription: Subscription = new Subscription();
constructor(private service: Service) {}
ngOnInit(): void {
/* add each subscriber to the subscription */
this.subscription.add(
this.service.getTodos().subscribe(data => {
console.log(data);
});
);
}
ngOnDestroy(): void {
/* unsubscribe all subscribers at once */
this.subscription.unsubscribe();
}
Related
I have 1 feature module (Fund module) that displays fund data in one of its components and an AppModule that displays Advisor data. Fund data and Advisor data have one to many relationship.
Fund component gets data from services defined in the AppModule.
Data are passed to the Fund module using BehaviorSubject as data might change if the user updates or modifies the data in AppModule.
The subscription of the data in not working correctly. The fund module only displays the value (10) that the BehaviorSubject is initialized with. It doesn't display the updated value (100), nor does the subscription work properly to display the values second time.
Here's the code:
Service in AppModule:
test = new BehaviorSubject<number>(10);
getTest(): Observable<number> {
return this.test.asObservable();
}
public updateLocalData(sleeves: Sleeve[]): void {
.... update logic
this.test.next(100);
}
FundDataComponent in Fund Module
changeDetection: ChangeDetectionStrategy.OnPush,
ngOnInit(): void {
this.test = this.service.getTest().subscribe((number) => {
this.number = number;
console.log(number); // get's called only once to display 10
});
}
Create a model - resource.ts:
import { BehaviorSubject, Observable } from 'rxjs';
import { refCount, publishReplay } from 'rxjs/operators';
export class StreamResource {
private loading = false;
private behaviorSubject: BehaviorSubject<any>;
public readonly obs: Observable<any>;
constructor(defaultValue?: any) {
this.behaviorSubject = new BehaviorSubject(defaultValue);
this.obs = this.behaviorSubject.asObservable().pipe(publishReplay(1), refCount());
}
request(method: any): void {
if (method && !this.loading) {
this.loading = true;
method().toPromise().then((data: any) => {
if (data) { this.update(data); }
this.loading = false;
});
}
}
getValue() {
return this.behaviorSubject.getValue();
}
update(data: any) {
this.behaviorSubject.next(data);
}
refresh() {
const data = this.getValue();
if (data) { this.update(data); }
}
}
Create a StreamService
import { StreamResource } from '../models/resource';
public test = new StreamResource(10);
...
getTest() {
this.test.request(() => this.testService.getTest());
}
How to use?
constructor(private streamService: StreamService) { }
ngOnInit() {
this.streamService.test.obs.subscribe((data: any) => {
if (!data) {
return this.streamService.getTest();
} else {
this.test = data;
}
});
I am using ngrx/data and what I need to do is set a value in the store, let's call this ID. And then when I make any request to an entity to pull that ID from the store. I will use update as an example.
Here is an example of a Client Entity Service. I can easily map the returned data as super.update returns an observable.
import { Injectable } from '#angular/core';
import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '#ngrx/data';
import { Client } from '../../store/client/client.model';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class ClientEntityService extends EntityCollectionServiceBase<Client> {
constructor(
serviceElementsFactory: EntityCollectionServiceElementsFactory,
) {
super('Client', serviceElementsFactory);
}
public update(entity: Partial<Client>): Observable<Client> {
return super.update(entity);
}
}
However I want to use a store value to make the update. So focusing on the update I can do this:
public update(entity: Partial<Client>): Observable<Client> {
this.store.pipe(
tap((store) => {
console.log(store);
})
).subscribe();
return super.update(entity);
}
Which prints out the store and I can see the value I need, so I could do this
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1)
).subscribe((id) => {
return super.update({
id,
...update
});
});
}
However it is requiring me to subscribe to the observable to be able to trigger it. That would mean the caller could not pipe the results and is generally not ideal.
I am wondering if anyone knows of a good solution to be able to get the data from the store but not have to subscribe like I am doing above to get the data, ideally I would want to use a switchMap like this:
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
switchMap((id) => {
return super.update({
id,
...update
});
}),
take(1)
)
Thanks
You wrote it correctly in your ideal solution. The difference is that you just need to move take(1) to be right after the select.
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1),
switchMap((id) => {
return super.update({
id,
...update
});
}),
)
so store won't cause update requests on every change.
After stepping into the super.update call I could see that the dispatcher was calling:
update(entity, options) {
// update entity might be a partial of T but must at least have its key.
// pass the Update<T> structure as the payload
/** #type {?} */
const update = this.toUpdate(entity);
options = this.setSaveEntityActionOptions(options, this.defaultDispatcherOptions.optimisticUpdate);
/** #type {?} */
const action = this.createEntityAction(EntityOp.SAVE_UPDATE_ONE, update, options);
if (options.isOptimistic) {
this.guard.mustBeUpdate(action);
}
this.dispatch(action);
return this.getResponseData$(options.correlationId).pipe(
// Use the update entity data id to get the entity from the collection
// as might be different from the entity returned from the server
// because the id changed or there are unsaved changes.
map((/**
* #param {?} updateData
* #return {?}
*/
updateData => updateData.changes)), withLatestFrom(this.entityCollection$), map((/**
* #param {?} __0
* #return {?}
*/
([e, collection]) => (/** #type {?} */ (collection.entities[this.selectId((/** #type {?} */ (e)))])))), shareReplay(1));
}
Which effectively just dispatches some actions and then creates a selected observable from this.getResponseData$ using the correlationid etc.
In my use case because I am using the store to get the Id of the current client I don't need the updated client returned as I already have an observable.
On top of the ClientEntityService I have another facade which I am calling ClientService.
Which looks like this:
#Injectable({
providedIn: 'root'
})
export class ClientService {
constructor(
private clientEntityService: ClientEntityService,
private store: Store<AppState>
) {}
public getCurrentClient(): Observable<Client> {
return this.clientEntityService.entityMap$.pipe(
withLatestFrom(this.store.pipe(select(getCurrentId))),
map(([clients, currentId]) => clients[currentId])
);
}
public updateCurrentClient(update: Partial<Client>): Subscription {
return this.getCurrentClient().pipe(
take(1),
switchMap((client) => {
return this.clientEntityService.update({
id: client.id,
...update
});
})
).subscribe();
}
}
So now from within my component I have the constructor
constructor(
private clientService: ClientService,
) {
this.client$ = this.clientService.getCurrentClient();
}
And then on update I call:
this.clientService.updateCurrentClient(theUpdate);
And because I already have this.client$ as an observable of that client being updated I don't need updateCurrentClient to return Observable<Client>. So as per above I am just returning Subscription
I could modify updateCurrentClient to implement something similar to what the DefaultDataService returns, but I'd expect that could be subject to change in future versions. So for now. I am happy with this solution.
I need to run a method with 2 parameters, each parameter is gotten through some form of subscribe function. the first is the collection which is gotten through the url from angular's page routing. The second is the dokument, this is the firebase's firestore document.
export class FirebaseDocument implements OnInit {
collection: string;
dokument: any;
//== CONSTRUCTORS
constructor(
private route: ActivatedRoute,
private _db: AngularFirestore
) {}
//== Initialize
ngOnInit() {
console.log("__loading page component");
this.route.params.subscribe(params => {
this.collection = params["collection"];
});
console.log(this.collection);//collection populated correctly
//load the document from AngularFirestore
console.log("loading the document from firebase");
let itemsCollection = this._db.collection(url).valueChanges();
//subscribe to get the dok of the first document in the collection
itemsCollection.subscribe(docArr => {
this.dokument = docArr[0];
console.log(this.dokument);//dokument is populated
});
console.log(this.dokument);//dokument is undefined
this.doMultiParameterMethod(this.collection, this.dokument);
}
}
this.collection populates perfectly fine;
this.dokument is only populated inside the subscribe method
I need this to be populated by the time the next line is run. the console.log(this.dokument);
I have been dumbstruck by this because essentially the same code is used by the 2 subscribe methods but they don't behave the same way.
Sometimes a subscribe can be synchronous. This happens when the Observable is a ReplaySubject a BehaviorSubject or an Observable which has a shareReplay() pipe. (probably other options as well.
This will make the observable immediately fire on subscription. However, you should never count on this behavior, and always continue within your subscribe.. Or use pipes like mergeMap and create other observables which you can access in your template using the async pipe.
In your case. The this.route.params is obviously a 'replaying' Observable from which you get the latest value after subscribing. Otherwise you would have to wait for the params to change again until you get a value.
Your Database call cannot return an immediate response, because it's essentially a network request.
In your example code, you can update it to this, and use the async pipe in your template
export class FirebaseDocument implements OnInit {
readonly collection$: Observable<string> = this.route.params.pipe(
map((params) => params.collection)
);
readonly doc$: Observable<any[]> = this.db.collection(this.url).valueChanges().pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
constructor(private route: ActivatedRoute, private db: AngularFirestore) {}
ngOnInit() {
// don't forget to unsubscribe
combineLatest([
this.collection$,
this.doc$
]).subscribe((collection, document) => {
this.doMultiParameterMethod(collection, document);
});
}
}
Maybe you should make the Observable a Promise, in your case would be the following :
export class FirebaseDocument implements OnInit {
collection: string;
dokument: any;
//== CONSTRUCTORS
constructor(
private route: ActivatedRoute,
private _db: AngularFirestore
) {}
//== Initialize
ngOnInit() {
console.log("__loading page component");
this.route.params.subscribe(params => {
this.collection = params["collection"];
});
console.log(this.collection); //collection populated correctly
this.getDokument().then(docArr => {
this.dokument = docArr[0];
this.doMultiParameterMethod(this.collection, this.dokument);
});
}
getDokument(): Promise<any> {
let itemsCollection = this._db.collection(url).valueChanges();
return new Promise((resolve, reject) => {
itemsCollection.subscribe((response: any) => {
resolve(response);
}, reject);
});
}
}
Below are my code snippets
Angular service
export class CarService {
constructor(private httpClient: HttpClient) {}
// -------------------------------------------------
// define observable Subject(s) which will be subscribed by callers
private observableList = new Subject<CarModel>();
public getObservableList() {
return this.observableList.asObservable();
}
// -------------------------------------------------
// fetch required required data, then add into observable
public fetchCarInfo(carId: string) {
// call service
const url = '.../api/car/' + carId;
this.httpClient
.get<CarModel[]>(url)
.subscribe(
data => {
this.observableList.next(data);
}
);
}
}
Angular component
export class ListPage implements OnInit, OnDestroy {
car1: CarModel[];
car2: CarModel[];
constructor(private carService: CarService) {}
ngOnInit() {
// 1. subscribe Observable Subject
const carsSubscription = this.carService
.getObservableList()
.subscribe(serviceResult => {
// Here we can use returned serviceResult
// but our service will be called 2 times with 2 different parameters!
});
}
// in my business, I need to call the same service 2 times with different parameters
// The issue here, BOTH calls will refresh the same Observable
// so i can not know the returned serviceResult is related to the first or second call?
public coreBusiness(carId1: string, carId2 string) {
// 2. call service method by parameters to fetch/refresh Observable
this.carService.fetchCarInfo(carId1);
this.carService.fetchCarInfo(carId2);
}
}
The normal solution
To add into Service 2 Observables and in Component subscribe both!
But I think this is not a clean solution to add multiple Observables in the service equivalent to the number of service calls with different parameters.
Any suggestions?
I have a DataServive, that fetches content from an API:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError, retry } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
#Injectable()
export class DataService {
this.request = {
count: 10
}
constructor(private http: HttpClient) { }
private handleError(error) {
console.log(error);
}
public getData(count): Observable<any> {
this.request.count = count;
return this.http.post<any>(environment.api + '/path', this.request).pipe(
map(response => {
return response;
}),
catchError(error => {
this.handleError(error);
return [];
})
);
}
}
This DataServie is consumned by a component like this:
ngOnInit() {
const subscriber = this.dataService.getData(this.count).subscribe((data) => { this.data = data; });
}
And it works fine.
However the user is able to change the variable this.count (how many items should be displayed) in the component. So I want to get new data from the server as soon as this value changes.
How can I achieve this?
Of course I could call destroy on this.subscriber and call ngOnInit() again, but that dosn't seem like the right way.
Easiest ways is just to unsubscribe:
subscriber: Subscription;
ngOnInit() {
this.makeSubscription(this.count);
}
makeSubscription(count) {
this.subscriber = this.dataService.getData(this.count).subscribe((data) => { this.data = data; });
}
functionInvokedWhenCountChanges(newCount) {
this.subscriber.unsubscribe();
makeSubscription(newCount);
}
But because count argument is just a one number it means HTTP always asks for data from 0 to x. In this case, you can better create another subject where you can store previous results (so you don't need to make useless HTTP requests) and use that subject as your data source. That needs some planning on your streams, but is definitely the preferred way.
When the user changes count, call getData(count) again with the updated count value. Need to see your html file, but having a button with (click)="getData(count)" may help.