I have a div and need to add attribute depends on store result. The store works as expected, when I click on certain div it's value changed (return true or false) and in my component I need to get updated value, but as I call it in ngOnInit it works only once and not updating. I've read about BehaviorSubject but don't understand how to convert my observable to it correctly. Would appreciate any help!
Here's my code
html
<div class="details__header" [attr.isOpened]="isOpened ? 'opened' : 'closed'">
ts
isOpened: boolean;
constructor(private store: Store<AppState>) { }
ngOnInit(): void {
this.store.select(selectSearchResultState).subscribe(el => this.isOpened = el);
}
I've tried async pipe in html and it works, but I need to use this variable in my code, so this doesn't work for me
You should define isOpened as an Observable like this:
isOpened$: Observable<boolean>;
constructor(private store: Store<AppState>) { }
ngOnInit(): void {
this.isOpened$ = this.store.select(selectSearchResultState);
}
Then use it in the template with an async pipe:
<div class="details__header" [attr.isOpened]="(isOpened$ | async) ? 'opened' : 'closed'">
There's more information about observables in the documentation here.
Related
Hi i'm building a chat app with angular for a school project i'm using firebase for my backend and i have an issue with my ngfor.
For exemple if i reload the page i will see nothing unless i hover my routerlink on my navbar. However sometime it will work after some time on the page without any action
When i recieve message i need to be on the page to see them ...
When i reload my page in first time my array is empty this may be what makes the ngfor bug
array on reload.
I'm using ngOnInit() to subscribe :
messages: Message[];
messageSubscription: Subscription;
constructor(private messageService: MessageService, private router: Router) {
}
ngOnInit(): void {
this.messageSubscription = this.messageService.messageSubject.subscribe(
(messages: Message[]) => {
console.log(messages)
this.messages = messages;
}
);
this.messageService.getMessage();
this.messageService.emitMessage();
}
ngOnDestroy(): void {
this.messageSubscription.unsubscribe();
}
This is my html template :
<div *ngFor="let message of messages" class="message-box">
<img [src]="message.photoURL" class="profile-picture">
<div class="content-box">
<div class="information">
<p class="username">{{message.displayName}}</p>
<p class="date">{{message.createdAt | date: 'short'}}</p>
</div>
<p class="text">{{message.text}}</p>
</div>
</div>
Here you can find my service with my getMessage() function and emitMessage():
messages:Message[] = [];
messageSubject = new Subject<Message[]>();
constructor() { }
emitMessage(){
this.messageSubject.next(this.messages);
}
saveMessage(newMessage: Message){
firebase.database().ref('/message').push(newMessage);
}
getMessage(){
firebase.database().ref('/message')
.on('value', (data) => {
this.messages = data.val() ? Object.values(data.val()): [];
this.emitMessage();
});
}
And this is the repo of my project: https://github.com/Zuxaw/AngularChatApp
If anyone has a solution I'm interested
Problem is, your firebase library is not Angular specific.
This means you some times need to make sure its code, mostly its event callbacks, run within an Angular zone (google to read about it) to make sure a change detection 'tick' is invoked when data changes.
message.service.ts
import { Injectable, NgZone } from '#angular/core';
// ...
constructor(private zone: NgZone) { }
// ..
getMessage(){
firebase.database().ref('/message')
.on('value', (data) => {
this.zone.run(() => {
this.messages = data.val() ? Object.values(data.val()): [];
this.emitMessage();
});
});
}
I think you might need to use the child_added event instead of value in your getMessage method.
Check if you're receiving data on time in your getMessage method, if not it's most probably, because of the event.
But one thing that I don't understand is why you're calling emitMessage inside getMessage and also calling it inside your component after getMessage, try to evade that.
I have a function to get rates from products, so lets say I have one product with two rates. So my product has two rates. Then, when I get those rates I must get the prices attached to my product. So for each rate I have to look for its prices.
The next code below explains this:
this.loadProductInfo = true; // bool to load data in my form
// First of all, I get rates from API
// const rates = this._http....
// Now, for each rate I must search If my product/products have a price:
this.rates.forEach((rate, index, arr) => {
this._glbGetPricesForProduct.getPrice(params).subscribe(response => {
if (!arr[index + 1]) {
this.initForm();
this.loadProductInfo = false;
}
})
});
The variable loadProductInfo it loads content in my form, so in my html I have:
<form *ngIf="!loadProductInfo"></form>
But form it still give me error: could not find control name.
But if I do this instead, it works correctlly:
setTimeout(() => {
this.initForm();
this.loadProductInfo = false;
}, 2000);
So what I want its to say my form to wait until I have all code loaded and then after it, load its contents. But instead it cant find the control because it loads before code. Any help I really appreciate it.
The main mistake I see there is that you are looping over async data which may not be there when your code execute the for each loop (your rates).
I would build an observable with your rates as a source:
...
$rates: Observable<any> = this._http.get(...);
rates.pipe(
mergeMap((rates) => {
const priceByRates: Observable<any>[] = rates.map((rate, index, arr) => this._glbGetPricesForProduct.getPrice(params));
return combineLatest(pricesByRates); // if getPrice complete right away, use forkJoin() instead
})
).subscribe(res => {
// No need to check for the last item, all rates have been checked for possible price
this.initForm();
this.loadProductInfo = false;
});
...
This implementation should wait for your api calls to resolve before printing your form.
Since you are hiding the entire form, it may be better to just move the API call into a resolver so that the page does not render until the data is ready.
Here is a minimal StackBlitz showcasing this behavior: https://stackblitz.com/edit/angular-ivy-4beuww
Component
In your component, include an ActivatedRoute parameter via DI.
#Component(/*omitted for brevity*/)
export class MyComponent {
constructor(private route: ActivatedRoute) {
// note: 'data' is whatever you label your resolver prop in your routing setup
route.data.subscribe(resolved => {
if ("data" in resolved) this.resolveData = resolved["data"];
});
}
}
Route Setup
And in your router setup you would have the following:
const routes: Routes = [
{
path: 'my-route-path',
component: MyComponent,
resolve: {
data: MyResolver
}
}
];
Resolver
Finally, your resolver would make your API call utilizing your service:
#Injectable({providedIn: 'root'})
export class MyResolver() implements Resolve<T> {
constructor(private service: MyService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | any {
return this.service.myRequest();
}
}
The final result will be that your view will not be rendered until your data is ready.
I'm pretty new to Angular and I'm not quite sure if I'm just doing something wrong with #Input() and ngOnChanges() in my code or if my whole setup isn't correct.
My setup is as follows:
I have some API generated data. My service gets the data and holds logic to do some filtering.
My parent component holds a filtercomponent which has a button "apply filter" (which uses the logic in my service) and a tablecomponent to display the data.
The filtering works fine and I do get the desired filtered data but how do I pass JUST this array to the tableviewcomponent? If I do it via #Input() and run ngOnChanges I get a nested array.
How do I solve this?
Parent TS:
tabellenDaten: any[];
constructor(private filterservice: BdService) {}
ngOnInit() {}
onDisplayTable(filter: BdFilter) {
this.filterservice.getBdTabelle(filter).subscribe(
(daten) => {
console.log('tabellendatenneu', daten);
this.tabellenDaten = daten;
},
(error) => console.log('error: ', error),
() => { }
);
}
Parent HTML which holds a filtercomponent and a tablecomponent:
<div class="v-flex-container-filter">
<app-allfilter-bd
(emitFilter)="onDisplayTable($event)"></app-allfilter-bd>
</div>
<div class="v-flex-container">
<app-tabelle
[tabellenDaten]="tabellenDaten"></app-tabelle>
</div>
When I log it I do get the desired table like: Array(148)[{...}, {...}, {...}, ...].
Now, when using [tabellenDaten]="tabellenDaten" and using #Input()...
Child TS (table logic):
#Input() tabellenDaten: any[];
ngOnChanges(...tabellenDaten: any) {
this.dataSource = new MatTableDataSource<any>(tabellenDaten);
console.log('TABELLENDATEN', tabellenDaten);
}
...I do get the results of the ngOnChanges method (currentValue, firstChange and previousValue) which ALSO holds my data array but how do I get JUST the single array?
Any help is very much appreciated.
The implementation of ngOnChanges is: ngOnChanges(changes: SimpleChanges): void
So your code should be:
ngOnChanges(changes: simpleChanges) {
// if 'tabellenDaten' has changed it will be available as a field on 'changes'.
if (changes.tabellenDaten) {
this.dataSource = new MatTableDataSource<any>(changes.tabellenDaten.currentValue);
}
}
Another approach is to use RxJS with async pipe provided by angular.
Instead of subscribe this.filterservice.getBdTabelle(filter), you can assign the observable :
tabellenDaten$: Observable<any[]>;
constructor(private filterservice: BdService) {}
ngOnInit() {}
onDisplayTable(filter: BdFilter) {
this.tabellenDaten$ = this.filterservice.getBdTabelle(filter);
}
Then in your template use async pipe :
<div class="v-flex-container-filter">
<app-allfilter-bd
(emitFilter)="onDisplayTable($event)"></app-allfilter-bd>
</div>
<div class="v-flex-container">
<app-tabelle
[tabellenDaten]="tabellenDaten$ | async"></app-tabelle>
</div>
And finally you don't need to assign your array to MatTableDataSource but you can pass you array directly to mat-table :
<mat-table [dataSource]="tabellenDaten">
You should always consider to not subscribe your Observable in your component unless you unsubscribe it manually. Otherwise use async and let the pipe subscribe/unsubscribe for you.
I have a form inside a service:
this.settingsForm = this.formBuilder.group({
names: this.formBuilder.array([]),
globalIDs: this.formBuilder.array([]),
topics: this.formBuilder.array([]),
emails: this.formBuilder.array([]),
description: ''
});
and a getter for convenience-
get description(): FormControl{
return this.settingsForm.get('description') as FormControl;
}
in some directive I am injecting the service and have a input that attached to this control. html-
<textarea matInput
[formControl]="settingsDataService.description">
</textarea>
In the directive I have a listener:
ngOnInit() {
this.listenToSearchInput();
}
listenToSearchInput() {
this.settingsDataService.description.valueChanges.pipe(
distinctUntilChanged(),takeUntil(this.descriptionDestroy))
.subscribe(value => {
//DO SOMETHING
});
}
but when I am typing in the textarea, my subscriber does not get called.
Maybe it is relevenat- but after listenToSearchInput() is called, in the service after I get an answer from the server I am filling description by-
this.settingsForm.patchValue({description:description});
What can be the reason?
probably you need to call the function in onChanges life cycle hook of your directive.
onChanges(): void {
this.settingsDataService.description.valueChanges.pipe(
distinctUntilChanged(),takeUntil(this.descriptionDestroy))
.subscribe(value => {
//DO SOMETHING
});
}
I found the answer- in the function that gets data from the server, I create a new seetingsForm to clean the previous data..
I, for the life of me cannot understand why I can't access a property on an angular 6 class. Here is some code:
#Component({
selector: 'admin-badge-component',
templateUrl: './badge.component.html'
})
export class AdminBadgeComponent implements OnInit {
// Badge Object
public badgeObject: IVisitorBadge = null;
// On Init
public ngOnInit() {
this.route.params.subscribe((params) => {
// Get Badge Object From API
this.visitorService.getVisitorBadge(params['aid'],params['vid'])
.subscribe((response: IVisitorBadge) => {
console.log(response);
this.badgeObject = response;
});
});
}
}
the console.log outputs every thing as intended:
{
"id":2,
"visit_id":325,
"visitor_id":45,
"created_at":"2018-09-29 15:00:10",
"updated_at":"2018-09-29 15:00:10",
"visitor": {
...
"firstname": "matthew",
"lastname": "brown",
...
}
}
However, when I goto access and display the visitor firstname in my template using the following code:
<div>
<h3>
{{ badgeObject?.visitor?.firstname }} {{ badgeObject?.visitor?.lastname }}
</h3>
</div>
Nothing displays. If I try to access the properties directly without the ? notation, I get cannot access 'firstname' of undefined. Even if I wrap the template in *ngIf and check for property first. I've also tried initting and setting a loadingBool that gets set to false after I have the API response, and using it in the *ngIf still nothing.
Here is screenshot of full class: https://imgur.com/a/eEfCSL3
public constructor(private _change: ChangeDetectorRef) { }
this.visitorService.getVisitorBadge(params['aid'],params['vid'])
.subscribe((response: IVisitorBadge) => {
this.badgeObject = response;
this._change.markForCheck();
});
});
You have to tell the change detector that the component is dirty when you lazy load data. The first time the template is rendered the value of badgeObject is null, but later it is assigned a value.
Use the ChangeDetectorRef:
https://angular.io/api/core/ChangeDetectorRef
Found the issue. Not mentioned above is the this.visitorService.getVisitorBadge method, which I was accidentally setting the responseType to text in the HttpClient callout. Reset that back to json, now it's working.