Array not updated when changes occur Angular 10 - javascript

There is an Observable that sends an array of offers to my component.
But when the list is changes (one is deleted) it does not change the list that I get in the component.
I've tried it with ngOnChanges to subscribe to the list again and update the list in my component, but it doesn't detect any changes on the list.
When I use ngDoCheck it worked, but I want a little less drastic solution for this..
offer.service.ts:
// observable of offers list
public getAll(): Observable<Offer[]> {
return of(this.offers);
}
component.ts:
offers: Offer[] = [];
selectedOfferId = -1;
constructor(private offerService: OfferService) { }
ngOnInit(): void {
this.offerService.getAll().subscribe(data => {
this.offers = data;
});
}
ngOnChanges(): void {
this.offerService.getAll().subscribe(data => {
this.offers = data;
});
}

You can communicate between components using an Observable and a Subject (which is a type of observable), I won't go too much into the details, you can fin more info here, there are two methods: Observable.subscribe() and Subject.next().
Observable.subscribe()
The observable subscribe method is used by angular components to subscribe to messages that are sent to an observable.
Subject.next()
The subject next method is used to send messages to an observable which are then sent to all angular components that are subscribers of that observable.
A workaround solution:
offer.service.ts:
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs';
#Injectable({ providedIn: 'root' })
export class OfferService {
private subject = new Subject<any>();
//...
getOffers(message: string) {
return this.subject.asObservable();
}
removeOffers() {
//...remove logic
this.subject.next({this.offers})
}
}
component.ts:
subscription: Subscription;
ngOnInit(): void {
this.subscription = this.offerService.getOffers().subscribe(offers => {
//...
})
}

Related

What makes a component fail to update its template when a Boolean value changes, in the Angular app?

In an Angular 11 app, I have a simle service that mekes a get request and reads a JSON.
The service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Promo } from '../models/promo';
#Injectable({
providedIn: 'root'
})
export class PromoService {
public apiURL: string;
constructor(private http: HttpClient) {
this.apiURL = `https://api.url.com/`;
}
public getPromoData(){
return this.http.get<Promo>(`${this.apiURL}/promo`);
}
}
In the the component, I need to compare the array of products with the array of campaign products (included in the JSON mantioned above) and higlight the promoted products:
export class ProductCardComponent extends DestroyableComponent implements OnInit, OnChanges
{
public promoData: any;
public promoProducts: any;
public isPromoProduct: boolean = false;
public ngOnInit() {
this.getCampaignData();
}
public ngOnChanges(changes: SimpleChanges): void {
this.getCampaignData();
}
public getPromoData() {
this.promoService.getPromoData().pipe(takeUntil(this.destroyed$)).subscribe(data => {
this.promoData = data;
this.promoProducts = this.promoData.products;
let promoProduct = this.promoProducts.find((product:any) => {
return this.product.unique_identifier == product.unique_identifier;
});
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
}
});
}
}
In the component's html file (template), I have:
<span *ngIf="isPromoProduct" class="promo">Promo</span>
There are no compilation errors.
The problem
For a reason I have been unable to understand, the template does not react to the change of the variable isPromoProduct and the template is not updated, despite the fact that I call the function inside ngOnInit and ngOnChanges.
Questions:
Where is my mistake?
What is a reliable way to update the template?
subscribing to Observable inside .ts file it's mostly not a best practice.
try to avoid it by using async pipe of Angular.
you need to store the observable in the variable and not the data returned from the observable, for example:
// this variable holds the `observable` itself.
this.promoData$ = this.promoService.getPromoData()
and then in the template you can do it like this:
<div *ngIf="promoData$ | async as promoData">
here you can access the promoData
</div>
you can still use pipe() to map the data etc but avoid the subscribe()
The isPromoProduct boolean is not an input. The ngOnChanges gets triggered for changes on your properties that are decorated with the #Input decorator. For your particular case, you can inject the ChangeDetectorRef and trigger change detection manually:
constructor(private cdr: ChangeDetectorRef) {}
// ...
public getPromoData() {
this.promoService.getPromoData().subscribe(data => {
// ...
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
this.cdr.detectChanges();
}
});
}
You also don't need to manage httpClient subscriptions. The observables generated by a simple get or post request will complete after they emit the response of the request. You only need to explicitly manage the unsubscribe for hot observables (that you create from subjects that you instantiate yourself).

Have realtime updates for a single Firestore document

There is a lot of documentation and examples of firestore collections getting realtime updates. However, there is very little for those who wish to have a single document have real time updates. I want to have a single document (an item), on a page where only the item will be viewed and manipulated and any changes to document, will have realtime updating.
Here is my component that wants to do stuff with the item:
import { Component, OnInit } from '#angular/core';
import { ItemsService } from '../shared/items.service';
import { ActivatedRoute, Router } from '#angular/router';
#Component({
selector: 'app-view-item',
templateUrl: './view-item.component.html',
styleUrls: ['./view-item.component.css']
})
export class ViewItem implements OnInit {
item;
private sub: any;
constructor(
// Service used for Firebase calls
private itemsService: ItemsService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
// Item retrieved from */item/:id url
this.sub = this.route.params.subscribe(params => {
this.getItem(params['id']);
});
}
getItem = (id) => {
this.itemsService.getItem(id).subscribe(res => {
console.log(res);
this.item = res;
console.log(this.item);
});
}
And the service it uses for calls:
import { Injectable } from '#angular/core';
import { AngularFirestore, AngularFirestoreDocument } from '#angular/fire/firestore';
#Injectable({
providedIn: 'root'
})
export class ItemsService {
constructor(
private firestore: AngularFirestore
)
getItem(id) {
return this.firestore.collection('items').doc(id).snapshotChanges();
}
}
The log I get for console.log(this.item) is undefined. Calling this.item in the console returns the same. I am unsure of how to proceed and would appreciate any guidance. Logging res in the console returns a byzantine object. Perhaps that's how I access the item, but if so, why is it not saved in this.item and how do I access the item's values?
snapshotChanges returns an observable of actions, not the actual value.
You should extract the value with action.payload.doc.data():
So your code should look like the following example.
getItem(id) {
return this.firestore.collection('items').doc(id).snapshotChanges()
.pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data();
const id = a.payload.doc.id;
return { id, ...data };
})
);
}
Or you can use valueChanges of doc.
getItem(id) {
return this.firestore.collection('items').doc(id).valueChanges();
}

subscription to behaviour subject don't work on all components

I my global service I instiante a behaviourSubject variable
dataWorkFlowService:
export class CallWorkflowService {
url = 'http://localhost:3000/';
selectedNode : BehaviorSubject<Node> = new BehaviorSubject(new Node(''))
dataflow : BehaviorSubject<any> = new BehaviorSubject<any>({});
constructor(private http: HttpClient) {}
getDataflow() {
return this.http.get(this.url);
}
updateNode(node :Node) {
this.selectedNode.next(node);
}
}
In my component ReteComponent I set behaviourSubject value using
this.dataFlowService.selectedNode.next(node);
Im my second component I subscribe to the BehaviourSubject
export class ComponentsMenuComponent implements OnInit {
constructor(private callWorkflowService:CallWorkflowService) { }
selectedNode:Node = new Node('');
dataFlow:any;
nxtElements:String[]=[]
ngOnInit() {
this.callWorkflowService.dataflow.subscribe(data=> {
this.dataFlow=data
})
this.callWorkflowService.selectedNode.subscribe( (node) => {
this.selectedNode=node; <=== ###### Subscription is not triggered
if(this.dataFlow) {
this.nxtElements=this.dataFlow[node.name].next;
}
})
}
When I trigger new value to selectedNode my subscription does not work
But in another component it's working well
export class AppComponent {
opened:boolean=false;
events: string[] = [];
constructor(private callWorkflowService:CallWorkflowService) { }
ngOnInit() {
this.callWorkflowService.selectedNode.pipe(
skip(1)
)
.subscribe( (node) => {
this.opened=true; <== subscription is working
})
}
}
I have noticed in that in ComponentsMenuComponent when I change it to
export class ComponentsMenuComponent implements OnInit {
constructor(private callWorkflowService:CallWorkflowService) { }
selectedNode:Node = new Node('');
dataFlow:any;
nxtElements:String[]=[]
ngOnInit() {
this.callWorkflowService.getDataflow().subscribe(data=> {
this.dataFlow=data;
}) ####CHANGE HERE ### <== using `getDataFlow` method which is not observable
this.callWorkflowService.selectedNode.subscribe( (node) => {
this.selectedNode=node; ### <=== subscription is triggered
if(this.dataFlow) {
this.nxtElements=this.dataFlow[node.name].next;
}
})
}
the selectNode subscription is working.
Update
I have tried to change how I proceed
In my service I added a method that return last value
updateDataFlow() {
return this.dataflow.getValue();
}
In ComponentsMenuComponent
this.callWorkflowService.node.subscribe( (node) => {
this.dataFlow = this.callWorkflowService.updateDataFlow();
this.selectedNode=node;
if(this.dataFlow) {
this.nxtElements=this.dataFlow[node.name].next;
}
})
Here again subscription is not working..
I have tried to comment the line
this.dataFlow = this.callWorkflowService.updateDataFlow();
And here surprise.. subscription works.
I don't know why it don't subscribe when I uncomment the line that I have mentioned
You must be providing your CallWorkflowService incorrectly and getting a different instance of the service in different components. If one component is working and another is not then I would guess that they are not both subscribed to the same behavior subject.
How are you providing the service? Is it provided in a module, component or are you using provided in?

Subject Subscription is triggered twice when I call .next() once in Angular app

i'm trying to create a reusable Modal component.
in a ModalService i have a Subject, and a method that that calls next() on the subject.
The ModalComponent subscribes to that subject, but whenever the method in the service is being called, the next function of the observer gets triggers twice.
Anyone know what causes this?
export class ModalService {
openModal = new Subject();
constructor() { }
open(cmp) {
this.openModal.next(cmp);
}
}
Modal Component:
export class ModalComponent implements OnInit {
component: ComponentRef<any>;
#ViewChild('entry', { read: ViewContainerRef }) entry: ViewContainerRef;
constructor(
private resolver: ComponentFactoryResolver,
private modalService: ModalService
) {}
ngOnInit() {
this.modalService.openModal.subscribe(cmp => {
// CALLD TWICE EVRY TIME THE SERVICE CALLS .next()
console.log(cmp);
});
}
It is not clear in your question where and how open() method is called. Is it the open() called twice or subscribe() triggered twice?
But if you want to share the last value with the subscribers you could use shareReplay() in pipe() like this:
export class ModalService {
openModalSubject = new Subject();
openModal = this.openModalSubject.asObservable().pipe(shareReplay());
constructor() { }
open(cmp) {
this.openModalSubject.next(cmp);
}
}
UPDATE
And in your modal component, you need to unsubscribe from the observable when navigating from it. You can do it two ways.
First Way:
modalSubscription: Subscription;
ngOnInit() {
this.modalSubscription = this.modalService.openModal.subscribe(cmp => {
console.log(cmp);
});
}
ngOnDestroy(){
this.modalSubscription.unsubscribe();
}
Second Way:
unsubscribeSignal: Subject<void> = new Subject();
ngOnInit() {
this.modalSubscription = this.modalService.openModal
.pipe(
takeUntil(this.unsubscribeSignal.asObservable()),
)
.subscribe(cmp => {
console.log(cmp);
});
}
ngOnDestroy(){
this.unsubscribeSignal.next();
}
I prefer the second way mostly. This way, you can unsubscribe more than one observable at once.
The best way is to push all subscriptions in the array and unsubscribe it into the ngondestroy.
First import the Subscription from rxjs
import { Subscription} from 'rxjs';
second create global property in component
subscriptions: Subscription[] = [];
Third push all the subscribe in subscriptions property
constructor(){
this.subscriptions.push(this.Service.subject1.subscribe((result) => {
console.log('subject1');
}));
this.subscriptions.push(this.dataService.subject2.subscribe((data) => {
console.log('subject2')
}
Lastly unsubscribe it
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}

Route Resolver not firing observable without subscribe

I have a route which needs some data from my Firebase db before the route is loaded. It feels like the Route is not calling subscribe so the request is never being fired off. Am I missing a step?
(Angular 5)
My router:
{
path: 'class/:idName',
component: ClassComponent,
resolve: {
classData: ClassResolver
}
},
My Resolver:
#Injectable()
export class ClassResolver implements Resolve<any> {
constructor(
private db: AngularFireDatabase
) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> | Promise<any> | any {
// return 'some data'; //This worked fine
return this.db
.list('/')
.valueChanges() // Returns Observable, I confirmed this.
//.subscribe(); // This returns a Subscriber object if I call it and I never get any data
}
// I tried this and it didnt work either
//const list = this.db
// .list('/')
// .valueChanges();
//console.log('list', list); // Is a Observable
//list.subscribe(data => {
// console.log('data', data); // returned data
// return data;
//});
//return list; // never gets to the component
}
My Component:
public idName: string;
// Other vars
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
private db: AngularFireDatabase
) {
// Form stuff
}
ngOnInit() {
// Never makes it here
this.idName = this.route.snapshot.params.idName;
const myclass = this.route.snapshot.data.classData;
console.log('myclass', myclass);
}
I never makes it to the component. It waits for the component to load, which it never does. If I add the subscribe and console.out the data it returns quite quickly with the correct data, so its not the service.
After calling .subscribe() in my Resolver that now returns a Subscriber object. Because my return signature allows for any its returning this Subscriber as if it was the data. This seems obvious now.
My question now becomes why isn't it resolving my Observable?
Your resolve function is returning an Observable that never completes. The Observable is indeed firing (and this can be verified by adding a tap to its pipeline with some console-logging)—but the resolve phase won't end (and therefore your component won't load) until the Observable completes. (The docs are not great at highlighting this.)
Obviously you don't want your Observable to complete either, because then you wouldn't get further data updates.
The simplest “fix” is to wrap your Observable in a Promise:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
return this.db.list('/').valueChanges();
}
but this won't guarantee that Firebase has emitted its initial response, which I feel is what you're trying to ensure before the route loads.
The only approach I can see that would:
ensure that the component doesn't load until Firebase has returned data at least once; and
prevent two different Firebase reads (one by the resolver and then one by the component) for one effective operation
is to wrap your Firebase Observable in a service:
import { Injectable, OnDestroy, OnInit } from '#angular/core';
import { AngularFireDatabase } from '#angular/fire/database';
import { Subscription } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
#Injectable({
providedIn: 'root',
})
export class DataService implements OnInit, OnDestroy {
constructor(private readonly db: AngularFireDatabase) {}
/**
* Observable to the data.
* shareReplay so that multiple listeners don't trigger multiple reads.
*/
public readonly data$ = this.db
.list('/')
.valueChanges()
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
/**
* To trigger the first read as soon as the service is initialised,
* and to keep the subscription active for the life of the service
* (so that as components come and go, multiple reads aren't triggered).
*/
private subscription?: Subscription;
ngOnInit(): void {
this.subscription = this.data$.subscribe();
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
and then your resolver would look like this:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
// ensure at least one emission has occurred
await this.dataService.data$.pipe(take(1)).toPromise();
// ...then permit the route to load
return this.dataService.data$;
}
By wrapping your Firebase Observable in a service, you get OnInit and OnDestroy lifecycle hooks, which you can use to ensure that the observable "lives on" between component loads (and prevent multiple Firebase reads where one would suffice). Because the data is then hanging around, subsequent loads of the data would also be quicker. Lastly, this still enables you to use a resolver to ensure that the data will be instantly available before proceeding to load the component.
Your code looks to be correct. Have you been passing a parameter to your class route? It wont resolve without a parameter, that might be why you are not reaching your ngOnInit function. I would suggest console logging your route snapshots as well to make sure you are grabbing the right objects. I'll also post a resolve example that I got working:
Component.ts
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
public data: Observable<any>;
constructor(private router: ActivatedRoute) { }
ngOnInit() {
this.data = this.router.snapshot.data.test;
}
}
Routing.ts
{ path: 'home/:id', component: HomeComponent, resolve: { test: ResolverService } },
ResolverService
import { Injectable } from '#angular/core';
import { Resolve } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
#Injectable()
export class ResolverService implements Resolve<Observable<any>> {
constructor() { }
public resolve(route: ActivateRouteSnapShot): Observable<any> {
return Observable.of({test: 'Test Observable'});
}
}
HTML
{{this.data.test}}
You just need to add a take(1) operator to the Observable the resolver returns so that it completes.
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.db.list('/').valueChanges()
.pipe(take(1)); // <-- The Magic
}
#AlexPeters was on the right track, but you don't have to go so far as to return a promise. Just force the completion with take(1). Alex is also spot-on that the docs are not very clear on this. I just spent an couple hours debugging this same issue.

Categories