Pipe never gets executed when server response with new data - javascript

I created a subscription in my template to watch for changes to an object. The initial loading of the object displays the correct data for the property tags, when I add an item to the data it goes to a web server and returns a list of all the tags that are attached to the item (to keep the item in sync with the server). However, the newly added item isn't reflected on the page. I am not 100% sure why. I think it is because my of() statement but I am not sure. What I am seeing is that zip().pipe() never gets executed.
Do I need to use something other than of?
Note: I am trying to follow the declarative pattern to eliminate the usage of .subscribe()
Sub-Note: Once I get it working I plan on trying to remove the subscribe on this line this.server.file().subscribe
Stackblitz
export interface FileInfo {
tags: string[];
}
#Component({
selector: 'my-app',
template: `
<input #tag /><button (click)="addTag(tag.value)">Add Tag</button>
<div *ngIf="data$ | async as data">
<div *ngFor="let tag of data.tags">{{ tag }}</div>
</div>
`,
})
export class AppComponent {
data$ = new Observable<FileInfo>();
constructor(
// Used to mimic server responses
private readonly server: WebServer
) {}
ngOnInit() {
// I plan on removing this subscribe once I get a grasp on this
this.server.file().subscribe((img) => {
this.data$ = of(img);
});
}
addTag(newTag: string) {
const data$ = this.server.save(newTag);
this.data$.pipe(concatMap((i) => this.zip(data$)));
}
private zip(tags$: Observable<string[]>) {
return zip(this.data$, tags$).pipe(
tap((i) => console.log('zipping', i)),
map(([img, tags]) => ({ ...img, tags } as FileInfo))
);
}
}

It sounds like what you want to do is have a single observable source that emits the latest state of your object as new tags are added. Then you can simply subscribe to this single observable in the template using the async pipe.
In order to accomplish this, you can create a dedicated stream that represents the updated state of your file's tags.
Here's an example:
private initialFileState$ = this.service.getFile();
private addTag$ = new Subject<string>();
private updatedfileTags$ = this.addTag$.pipe(
concatMap(itemName => this.service.addTag(itemName))
);
public file$ = this.initialFileState$.pipe(
switchMap(file => this.updatedfileTags$.pipe(
startWith(file.tags),
map(tags => ({ ...file, tags }))
))
);
constructor(private service: FileService) { }
addTag(tagName: string) {
this.addTag$.next(itemName);
}
Here's a StackBlitz demo.

You're missusing the observable. After you subscribe with it, in the template with the async pipe, you should not update it's reference.
If you need to update the data, you must use a Subject.
export class AppComponent {
private readonly data = new BehaviorSubject<FileInfo>(null);
data$ = this.data.asObservable();
constructor(
// Used to mimic server responses
private readonly server: WebServer
) {}
ngOnInit() {
this.server.file().subscribe((result) => this.data.next(result));
}
addTag(newTag: string) {
this.server
.save(newTag)
.subscribe((tags) => this.data.next({ ...this.data.value, tags }));
}
}
Also, your service could be a lot simpler:
#Injectable({ providedIn: 'root' })
export class WebServer {
private readonly tags = ['dog', 'cat'];
file(): Observable<FileInfo> {
return of({ tags: this.tags });
}
save(tag: string) {
this.tags.push(tag);
return of(this.tags);
}
}
Here's the working code:
https://stackblitz.com/edit/angular-ivy-my3wlu?file=src/app/app.component.ts

Try completely converting webserver.service.ts to provide an observables of tags and FileInfo like this:
import { Injectable } from '#angular/core';
import { concat, Observable, of, Subject } from 'rxjs';
import { delay, map, shareReplay, tap } from 'rxjs/operators';
import { FileInfo } from './app.component'; // best practice is to move this to its own file, btw
#Injectable({ providedIn: 'root' })
export class WebServer {
private fakeServerTagArray = ['dog', 'cat'];
private readonly initialTags$ = of(this.fakeServerTagArray);
private readonly tagToSave$: Subject<string> = new Subject();
public readonly tags$: Observable<string[]> = concat(
this.initialTags$,
this.tagToSave$.pipe(
tap(this.fakeServerTagArray.push),
delay(100),
map(() => this.fakeServerTagArray),
shareReplay(1) // performant if more than one thing might listen, useless if only one thing listens
)
);
public readonly file$: Observable<FileInfo> = this.tags$.pipe(
map(tags => ({tags})),
shareReplay(1) // performant if more than one thing might listen, useless if only one thing listens
);
save(tag: string): void {
this.tagToSave$.next(tag);
}
}
and now your AppComponent can just be
#Component({
selector: 'my-app',
template: `
<input #tag /><button (click)="addTag(tag.value)">Add Tag</button>
<div *ngIf="server.file$ | async as data">
<div *ngFor="let tag of data.tags">{{ tag }}</div>
</div>
`,
})
export class AppComponent {
constructor(
private readonly server: WebServer;
) {}
addTag(newTag: string) {
this.server.save(newTag);
}
}
Caveat: If you ever call WebServer.save while WebServer.tags$ or downstream isn't subscribed to, nothing will happen. In your case, no big deal, because the | async in your template subscribes. But if you ever split it up so that saving a tag is in a different component, the service will need to be slightly modified to ensure that the "save new tag" server API call is still made.

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

angular component doesnt update data to the dom after fetching using api unless i open the component again

im using fakeapi store to fetch products and loop on them to show in a component im using a service to do that and it goes as follows
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { environment } from 'src/environments/environment';
#Injectable({
providedIn: 'root',
})
export class ProductsService {
eleProducts: any[];
spinnerElec: boolean;
constructor(private http: HttpClient) {
this.spinnerElec = true;
this.eleProducts = [];
}
getElectronics() {
this.spinnerElec = true;
this.http
.get(environment.baseUrl + '/category/electronics')
.subscribe((res: any) => {
this.eleProducts = res;
this.spinnerElec = false;
});
}
then in the electronics-component.ts
export class ElectronicsComponent implements OnInit {
Products: any[];
spinner: boolean;
constructor(public service: ProductsService) {
this.Products = [];
this.spinner = true;
}
ngOnInit(): void {
this.getProducts();
}
getProducts() {
this.spinner = this.service.spinnerElec;
this.service.getElectronics();
this.Products = this.service.eleProducts;
}
and to display data in electronics-component.html
<div class="row gap-3 justify-content-center" *ngIf="!spinner">
<div
class="card col-lg-4 col-md-6 col-sm-12 bg-warning"
style="width: 18rem"
*ngFor="let product of Products"
>
<app-product
[elecCart]="service.elecCart"
[Product]="product"
(target)="addToCart($event)"
></app-product>
</div>
</div>
<div
class="spin d-flex justify-content-center align-items-center w-100"
*ngIf="spinner"
>
<app-spinner></app-spinner>
</div>
im using ngif on the spinner to clear it from the dom and show data once it fetched sucessfuly the problem is that i must open the componet then go to another component then come back to it again to show the data otherwise the data wont show... thanks in advance
You are not returning anything in getProducts, and you should let the component to subscribe to that http call, not your service.
With your current code in the service, even you add a return to it, it will return a Subscription, not the data you want to handle in the component.
I highly recommend the reactive approach, which is a good practice in Angular:
service
getElectronics(): Observable<any[]> { //you should create your own type, avoid using any
return this.http.get(environment.baseUrl + '/category/electronics').pipe(
tap(() => this.spinnerElect = false)
);
}
Component
theProducts$!: Observable<any[]>; //it will store the data wrapped as an Observable
ngOnInit(): void {
// store the observable from the service containing the products data
this.theProducts$ = this.service.getElectronics();
}
template HTML
<app-product
[theProducts]="theProducts$ | async"
(target)="addToCart($event)"
></app-product>
Using the async pipe, you subscribe to the observable in the template HTML, without managing the subscription.
You are not suscribing in your componen, so as http is an async operation the first time you use call it in ngOnInit the code will continue and don't return anything because your component aren't listening that change.
There are two ways to avoid this, if you are not familiar with Rxjs (as i am) you can use setTimeout to create a little delay allowing your code to "wait" until the call is made.
getProducts() {
this.spinner = this.service.spinnerElec;
this.service.getElectronics();
setTimeout(() => {
this.Products = this.service.eleProducts;
}, 100);
}
BUT this is a way just for a quick fix. The correct way is to :
a) return the http petition to the component and suscribe there.
b) Convert eleproducts in an observable, when the http is load use .next(res) to emit the new value. In your component you'll suscribe to that observable and it will updated
Look here to know more about how to suscribe to a service variable: https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject

Angular. How to switch component depending on service's actions

Lets say I have 2 components, aComponent and bComponent. I have them redered inside the AppComponent
<app-a>
<app-b>
And I have service myService that has method .trigger().
What I want is to show only aComponent, but whenever I call myService.trigger() from another part of code, it would switch and show bComponent. That's perfect implementation that I can't reach.
Question is: Is it possible to do so? And if not what is the best closest solution.
The only working solution I got:
I added .trigger() inside AppComponent
export class AppComponent {
title = 'spa';
show: boolean = false;
trigger() {
this.show = true;
}
}
And rendered components like so:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
Then whenever I want to trigger switching, I add instance of the app to the constructor and call it's method:
export class AnotherComponent implements OnInit {
constructor(
private app: AppComponent
) {}
ngOnInit(): void {
this.app.trigger();
}
}
Even though it's working pretty good, I myself see that it's a dirty solution. Components are not intended to be used inside another components, but Services are.
You can use Subject from rxjs library for that.
In your service file:
// a-service.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable({ providedIn: 'root' })
export class AService {
private subject = new Subject<any>();
trigger(state: boolean) {
this.subject.next(state);
}
getTrigger(): Subject<any> {
return this.subject;
}
}
and in your AppComponent:
// app.component.ts
...
private show = false;
constructor (private aService: AService) { }
ngOnInit() {
this.aService.getTrigger().subscribe(state => {
this.show = state;
});
}
the template can be as you provided - it's fine:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
And if you want to trigger from another component, you do it like this:
// another.component.ts
...
constructor (private aService: AService) { }
ngOnInit() {
this.aService.trigger(true);
}
One way to communicate between different components and services which aren't directly related, is via 'Subjects'.
You can try to create a subject and pass in values to it from myService.trigger(). And you can subscribe to that subject from whichever component you want to access that trigger data.

Rxjs : prevent pushing data to subjects from outisde the service

Within my anguular app , i ve this service :
#Injectable()
export class myService{
myBehaviouSubject= new BehaviorSubject("");
setData(){
this.myBehaviouSubject.next("123");
}
}
Inside my app.component , i m able to get the value , but i want to keep it readonly or editable only inside the service itself , i want to prevent to push any data from component (.next('DATA'))
#Component({
})
export class AppComponent implements OnInit {
constructor(public myService : MyService) { }
getData(){
// GET VALUE
this.myService.myBehaviouSubject.value
}
unwantedMethodToSetValue(){
// SET VALUE -> i want to prevent this
this.myService.myBehaviouSubject.next("unwanted value")
}
}
Suggestions ?
You can keep the observable inside service only by declaring it as private field of a class.
#Injectable()
export class myService {
private myBehaviouSubject = new BehaviorSubject("");
// Use this observable inside the app component class.
myBehaviouSubjectObservable = myBehaviouSubject.asObservable();
setData() {
this.myBehaviouSubject.next("123");
}
}
#Component({
})
export class AppComponent implements OnInit {
constructor(public myService: MyService) {}
getData() {
// You can subscribe to observable and can get value here
this.myService.myBehaviouSubjectObservable.subscribe((value) => {
console.log(value);
})
}
unwantedMethodToSetValue() {
// SET VALUE -> you cannot do this here now.
this.myService.myBehaviouSubject.next("unwanted value")
}
}
Use property access modifiers:
#Injectable()
export class MyService{
private myValueSubject: BehaviorSubject<string> = new BehaviorSubject<string>("");
public readonly myValueObservable: Observable<string> = this.myValueSubject.asObservable();
public setData() {
this.myValueSubject.next("123");
}
public getData(): string {
return this.myValueSubject.value;
}
}
Instances of MyService will not have a publicly accessible subject.
I usually try to avoid a method like getData, favoring subscriptions to the related observable. If I ever find myself writing those kinds of methods, it's a warning flag to re-evaluate my architecture. If you just want to store a value and get/set it with methods, use a plain old private property. The entire purpose of the subject is defeated if you are only ever getting the value through a method like getData()
Check out the documentation for typescript classes, which discusses access modifiers: https://www.typescriptlang.org/docs/handbook/classes.html
The traditional answer : If you return the Subject as an observable, you disallow .next() calls.
But in your case, you also want direct access to the current value without subscribing, so you could add a getter for that too.
#Injectable()
export class myService{
private readonly myBehaviouSubject = new BehaviorSubject("");
setData(){
this.myBehaviouSubject.next("123");
}
public get myObservable$(): Observable<string>{
return this.myBehaviourSubject;
}
public get currentValue(): string{
return this.myBehaviourSubject.value;
}
}
https://stackblitz.com/edit/angular-protected-rxjs-subject
in this solution which I hope meet you needs:
be aware that there is no subscription
fetching updates handled manually
Property 'myBehaviourSubject' is private and only accessible
within class 'TestService'.

how to create a component that i can set fetching url data in an attribute in angular 2

I'm trying to create a application with angular 2 , i want to create a component in angular 2 that I set URL in attribute and want use several times from this component and each component have own data...
i want something like this :
its possible or not?
I'll really appreciate if someone help me.
new movies :
<comp url="www.aaaa.com/movies?type=new"></comp>
old movies :
<comp url="www.aaaa.com/movies?type=old"></comp>
#Component({
selector: 'comp',
template: '<div>{{data}}</div>'
})
export class Component {
#Input() url: string;
constructor(private http:Http) {
}
ngOnChanges(changes) {
this.http.get(this.url)
.map(res => res.json())
.subscribe(val => this.data = val);
}
}
If the component has more than one input then you need to check which one was updated. See https://angular.io/api/core/OnChanges for more details.
You can use compoenent as mentioned above answer.
But for this kind of task I always choose directive. You can also create a directive which will take one parameter and do the stuff.
In this way you don't have to create a tag but in any tag you can apply your directive.
#Directive({
selector: '[comp]',
})
export class compDorective implements OnInit {
#Input() url: string;
constructor(private http:Http, private elementRef: ElementRef) {
}
ngOnInit(changes) {
this.http.get(this.url)
.map(res => res.json())
.subscribe(val => this.elementRef.nativeElement.innerHtml = val);
}
}
you can apply this directive to any element like this
<div comp [url]="www.aaaa.com/movies?type=new"></div>
<span comp [url]="www.aaaa.com/movies?type=old"></span>

Categories