Pipe fires only 1 time even though its value changes - javascript

I have a filter like so:
.html
<div>
<app-categories [categories]="categories"
(categoriesChange)="filterByCategories($event)">
</app-categories>
</div>
<p-dataView
[value]="userItemsService.userItemsChanged$ | async | categoryFilter:searchTerms">
//code
</p-dataView>
.ts
searchTerms: string[];
filterByCategories(searchTerms: string[]): void {
this.searchTerms = searchTerms;
}
This is the app-categories component
The issue here is categoryFilter fires only for 1st Category selection. After that, it won't fire again. Can you tell me why?
pipe
#Pipe({
name: 'categoryFilter'
})
export class CategoryFilterPipe implements PipeTransform {
transform(userItems: UserItemModel[], searchTerms: string[]): UserItemModel[] {
if (!userItems) { return []; }
if (!searchTerms) { return userItems; }
return userItems.filter(ui => {
return searchTerms.some(st => ui.item?.primaryCategory.name?.toLocaleLowerCase().includes(st.toLocaleLowerCase()));
});
}
}

Looking at your async pipe:
[value]="userItemsService.userItemsChanged$ | async | categoryFilter:searchTerms">
This will only update when userItemsService.userItemsChanged$ changes - not when you update searchTerms through the UI.
You should make searchItems a subject in your component and emit a value in filterByCategories:
searchTerms$ = new BehaviorSubject([]);
...
filterByCategories(searchTerms: string[]): void {
this.searchTerms$.next(searchTerms);
}
The rest can be done directly in the template using <ng-container>:
<ng-container *ngIf="{userItems: userItemsChanged$ | async, searchTerms: searchTerms$ | async} as data">
<div>{{ userItems | categoryFilter:searchTerms }}</div>
</ng-container>
Another approach would be to have a filtered userItemsChanged$ stream in your component:
this.userItemsChangedFiltered$ = this.searchTerms$.pipe(
switchMap(searchTerms => userItemsChanged$.pipe(
map(userItems => <apply filter logic here>)
)
)

I have made this.searchTerms immutable and then all use cases are working fine.
filterByCategories(searchTerms: string[]): void {
this.searchTerms = [...searchTerms];
}

Related

angular virtual scroll doesn't re-render the list when list item added

i have a problem... with angular virtual scroll.
i tried too many solutions. but i can't resolve the problem.
when i added new item to the favoriteList, i pushed the new item to the front of the favoriteList. but there are no changes to the list of the rendered view. how can i change the view when new item added?
my problem:
https://stackblitz.com/edit/angular-cdk-scroll-bug-vifffd?file=src/app/app.component.ts
Angular Input sees the changes when the value changes, but in our case the input is not of primitive type then he will only know the reference of the array.
This is the virtual scroll input:
/** The DataSource to display. */
#Input()
get cdkVirtualForOf(): DataSource<T> | Observable<T[]> | NgIterable<T> | null | undefined {
return this._cdkVirtualForOf;
}
set cdkVirtualForOf(value: DataSource<T> | Observable<T[]> | NgIterable<T> | null | undefined) {
this._cdkVirtualForOf = value;
if (isDataSource(value)) {
this._dataSourceChanges.next(value);
} else {
// If value is an an NgIterable, convert it to an array.
this._dataSourceChanges.next(
new ArrayDataSource<T>(isObservable(value) ? value : Array.from(value || [])),
);
}
}
_cdkVirtualForOf: DataSource<T> | Observable<T[]> | NgIterable<T> | null | undefined;
In your case, it would be best to create a new class and implement DataSource
something like that:
export class CustomDataSource<T extends { id: number }> extends DataSource<T> {
private _dataSource: BehaviorSubject<T[]>;
constructor() {
super();
this._dataSource = new BehaviorSubject<T[]>([]);
}
connect(): Observable<readonly T[]> {
return this._dataSource.asObservable();
}
disconnect(): void {}
add(item: T): void;
add(item: T[]): void;
add(item: T | T[]): void {
const currentValue = this._dataSource.value;
const items = Array.isArray(item) ? item : [item];
const nextValue = currentValue.concat(items);
this._dataSource.next(nextValue);
}
remove(id: T['id']): void {
const currentValue = this._dataSource.value;
const nextValue = currentValue.filter(item => item.id !== id);
this._dataSource.next(nextValue);
}
}
stackblitz implementation

Why is my ngFor always updating, but array is not

Problem is: When I start this component, my ngFor div always updates and my RAM becomes empty. As I know, ngFor updates when array is updated, but my array(announcements) update only once, in constructor.
I have two ngFor divs:
<mat-tab label="Classroom">
<div *ngFor="let announcement of announcements">
<mat-card class="example-card">
<mat-card-header>
<mat-card-subtitle>{{"Announcement: " + announcement.text}}</mat-card-subtitle>
</mat-card-header>
<mat-card-footer>
<div *ngFor="let comment of loadComments(announcement)">
<mat-card class="example-card comment">
<mat-card-header>
<mat-card-subtitle>{{"Comment: " + comment.text}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
</mat-card>
</div>
</mat-card-footer>
</mat-card>
</div>
</mat-tab>
ts file:
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup } from '#angular/forms';
import { environment } from 'src/environments/environment';
import { Announcement } from '../model/announcement';
import { Classroom } from '../model/classroom';
import { User } from '../model/user';
import { Comment } from '../model/comment';
import { ClassroomService } from '../service/classroom.service';
import { CommentService } from '../service/comment.service';
import { AnnouncementService } from '../service/announcement.service';
#Component({
selector: 'app-view-classroom',
templateUrl: './view-classroom.component.html',
styleUrls: ['./view-classroom.component.css']
})
export class ViewClassroomComponent implements OnInit {
announcements: Announcement[] | undefined;
comments: Comment[] | undefined;
constructor(private classroomService: ClassroomService,
private commentService: CommentService,
private announcementService: AnnouncementService,
private formBuilder: FormBuilder)
{
this.classroomService.getClassroomUsers(JSON.parse(localStorage.getItem(environment.classroom) || ''), 'teachers').subscribe(
(response: User[]) => this.teachers = response);
this.classroomService.getClassroomUsers(JSON.parse(localStorage.getItem(environment.classroom) || ''), 'students').subscribe(
(response: User[]) => this.students = response);
this.classroomService.getClassroomOwner(JSON.parse(localStorage.getItem(environment.classroom) || '')).subscribe(
(response: User) => this.owner = response);
this.classroom = JSON.parse(localStorage.getItem(environment.classroom) || '');
this.announcementService.getAnnouncementsByClassroom(JSON.parse(localStorage.getItem(environment.classroom) || '')).subscribe(
(response: Announcement[]) => this.announcements = response);
}
ngOnInit(): void {
}
loadComments(announcement: Announcement){
let an = announcement;
this.commentService.getCommentsByAnnouncement(an).subscribe(
(response: Comment[]) => this.comments = response);
return this.comments;
}
}
But when i remove inner ngFor, problem is gone.
What should i do?
What you are doing is wrong.
loadComments(announcement: Announcement){
let an = announcement;
this.commentService.getCommentsByAnnouncement(an).subscribe(
(response: Comment[]) => this.comments = response);
return this.comments; // <-- old values!!
}
As it is right now this metod will return an old version of this.comments, not the one from the response.
Change the metode like this:
loadComments(announcement: Announcement):Observable<Comment[]>{
let an = announcement;
return this.commentService.getCommentsByAnnouncement(an);
}
And in the html file:
<ng-container *ngIg="loadComments(announcement) | async as comments">
<div *ngFor="let comment of comments">
...
</div>
</ng-container>
You're seeing this issue as the data is populating asynchronously. To resolve this, one of solution is to apply reactive programming strategy using RxJs.
Step 1: Replace static array definition to a Subject (import from 'rxjs')
announcements: Announcement[] | undefined;
comments: Comment[] | undefined;
// above two line needs to be changed to
announcements$: Subject<Announcement[] | undefined>;
comments$: Subject<Comment[] | undefined>;
Step 2: Update assignments
this.announcementService.getAnnouncementsByClassroom(
JSON.parse(localStorage.getItem(environment.classroom) || '')
).subscribe(
// (response: Announcement[]) => this.announcements = response <- update this to:
(response: Announcement[]) => this.announcements$.next(response)
);
this.commentService.getCommentsByAnnouncement(an).subscribe(
// (response: Comment[]) => this.comments = response <- update this to:
(response: Comment[]) => this.comments$.next(response)
);
// return this.comments; <- this is not required any more
Step 3: Update HTML
<!-- old -->
<div *ngFor="let announcement of announcements">
<!-- new -->
<div *ngFor="announcements$ | async as announcement">
<!-- old -->
<div *ngFor="let comment of loadComments(announcement)">
<!-- new -->
<div *ngFor="comments$ | async as comment">
just change
*ngFor="let comment of loadComments(announcement)"
to
*ngFor="let comment of comments"
and
loadComments(announcement: Announcement) {
this.commentService.getCommentsByAnnouncement(announcement).subscribe((response: Comment[]) => {
this.comments = response
})
}

How to show in template property from array of objects

I just try to show the value of a property in the template. But at the moment nothing is shown.
So this is the component:
export class ServerStatusComponent implements OnInit {
snovieCollection: SnovietatusDto = {};
constructor(private snovierStatus: snovieStatusService) {}
ngOnInit(): void {
this.sensorStatus
.getSensorStatuses()
.pipe(
map((data) => {
console.log(data.cameraSensors);
})
)
.subscribe((status) => {
});
}
}
And this is the template:
<p>Camera sensoren</p>
<tr *ngFor="let camera of snovieStatusCollection.key|keyvalue">
test
<h3> {{camera | json}}</h3>
</tr>
So I just want to show in the template the value of key. And the console.log returns this:
0: {key: "T", latestTimestamp: "2021-03-12T10:09:00Z"}
So I don't get any errors. But also nothing is shown.
Two things:
You aren't returning anything from the map. So undefined would be emitted to the subscription. Use tap for side-effects instead.
You aren't assigning the response to this.sensorStatusCollection in the subscription.
export class ServerStatusComponent implements OnInit {
sensorStatusCollection: SensorStatusDto = {};
constructor(private sensorStatus: SensorStatusService) {}
ngOnInit(): void {
this.sensorStatus
.getSensorStatuses()
.pipe(
tap((data) => { // <-- `tap` here
console.log(data.cameraSensors);
})
)
.subscribe((status) => {
this.sensorStatusCollection = status; // <-- assign here
});
}
}
Update: Type
As pointed out by #TotallyNewb in the comments, the type of this.sensorStatusCollection needs to be an array of type SensorStatusDto
export class ServerStatusComponent implements OnInit {
sensorStatusCollection: SensorStatusDto[] = [];
...
}

remove item shopping cart angular

I would simply like to delete an item on click, I made a code but I have error, I've been stuck on it for 2 days.
ERROR TypeError: this.addedBook.indexOf is not a function
I have already asked the question on the site we closed it for lack of information yet I am clear and precise
Thank you for your help
service
export class BookService {
url: string = 'http://henri-potier.xebia.fr/books';
public booktype: BookType[];
item: any = [];
constructor(private http: HttpClient) { }
getBookList(): Observable<BookType[]> {
return this.http.get<BookType[]>(this.url);
}
addToBook() {
this.item.push(this.booktype);
}
}
addToBook() here for add book but i dont know how to use it to display added books in my ts file
ts.file
export class PaymentComponent implements OnInit {
addedBook: any = [];
product:any;
constructor(private bookService: BookService) { }
ngOnInit(): void {
this.addedBook = this.bookService.getBookList();
}
delete() {
this.addedBook.splice(this.addedBook.indexOf(this.product), 1);
}
}
html
<div class="product" *ngFor="let book of addedBook | async">
<div class="product-image">
<img [src]="book.cover" alt="book">
</div>
<div class="product-details">
<div class="product-title">{{book.title}}</div>
</div>
<div class="product-price">{{book.price | currency: 'EUR'}}</div>
<div class="product-quantity">
<input type="number" value="1" min="1">
</div>
<div class="product-removal">
<button class="remove-product" (click)="delete()">
Supprimé
</button>
</div>
interface
export interface BookType {
title: string;
price: number;
cover: string;
synopsis: string;
}
I think this.bookService.getBookList() returns Observable so for you case it is not the best solution use async pipe. You should simply subscribe to your server response and than asign it to your variable. and after deleting item only rerender your ngFor.
JS
export class PaymentComponent implements OnInit {
addedBook: any[] = [];
product:any;
constructor(private bookService: BookService) { }
ngOnInit(): void {
// Observable
this.bookService.getBookList().subscribe(response =>{
this.addedBook = response;
});
// Promise
/*
this.bookService.getBookList().then(response=>{
this.addedBook = response;
})*/
}
delete(){
this.addedBook.splice(this.addedBook.indexOf(this.product), 1);
// rerender your array
this.addedBook = [...this.addedBook];
}
}
HTML
<div class="product" *ngFor="let book of addedBook">
<div class="product-image">
<img [src]="book.cover" alt="book">
</div>
<div class="product-details">
<div class="product-title">{{book.title}}</div>
</div>
<div class="product-price">{{book.price | currency: 'EUR'}}</div>
<div class="product-quantity">
<input type="number" value="1" min="1">
</div>
<div class="product-removal">
<button class="remove-product" (click)="delete()">
Supprimé
</button>
</div>
UPDATE
I built a special stackblitz so you can see it in action
here is the link;
you can't use javascript splice on Observable stream, it is not an Array.
to be able to remove an item from a stream you need to combine it (the stream) with another stream (in your case) the id of the item you want to remove.
so first create 2 streams
// the $ sign at the end of the variable name is just an indication that this variable is an observable stream
bookList$: Observable<any[]>; // holds bookList stream
deleteBook$ = new Subject<{ id: string }>(); // holds book id stream
now pass the results you get from your database (which is an observable stream) to bookList$ stream you just created like that
ngOnInit(): void {
this.bookList$ = this.bookService.getBookList().pipe(
delay(0)
);
}
change your html template to that.. and pipe the results from database like that
<div class="product" *ngFor="let book of (bookList$ | sync)">
...
// make sure you include your`remove-product` button inside `*ngFor` loop so you can pass the `book id` you want to remove to the `delete()` function.
<button class="remove-product" (click)="delete(book)">
Supprimé
</button>
</div>
now back to your ts file where we gonna remove the item from the STREAM by modifying the Array and return a new stream.
bookList$: Observable<any[]>; // holds bookList stream
deleteBook$ = new Subject<{ id: string }>(); // holds book id stream
ngOnInit(): void {
this.bookList$ = this.this.bookService.getBookList().pipe(
delay(0)
);
combineLatest([
this.bookList$,
this.deleteBook$
]).pipe(
take1(),
map(([bookList, deleteBook]) => {
if (deleteBook) {
var index = bookList.findIndex((book: any) => book.id === deleteBook.id);
if (index >= 0) {
bookList.splice(index, 1);
}
return bookList;
}
else {
return bookList.concat(deleteBook);
}
})
).subscribe();
}
now all is left to do is remove the item
delete(book: any) {
this.deleteBook$.next({ id: book.id }); pass the book you want to remove to the stream, `combineLatest` will take care of the rest
}
if you make an exit please don't forget me :)
good luck!
From your code, we can see that getBookList() return an Observable. As addedBook is not a array reference it will won't have array methods. That is the cause for your issue.
If you want to do some operations from the service data, subscribe to the observable and store the reference of the value to addedBook.
export class PaymentComponent implements OnInit {
...
ngOnInit(): void {
this.bookService.getBookList().subscribe(
res => { this.addedBook = res }
);
}
...
}
And you need to remove the async keyword from your html
Typescript is mainly used to identify these kind of issues in compile time. The reason it doesn't throw error on compile time is that you've specified addedBook as any. While declaring you declare it as array and onInit you change it to observable, which can be avoided if you've specified type[] ex: string[]
I would suggest something like this
Service file
export class BookService {
url: string = 'http://henri-potier.xebia.fr/books';
//add an observable here
private bookUpdated = new Subject<bookType>();
public booktype: BookType[] = [];//initializa empty array
item: any = [];
constructor(private http: HttpClient) { }
//Ive changet the get method like this
getBookList(){
this.http.get<bookType>(url).subscribe((response) =>{
this.bookType.push(response);//Here you add the server response into the array
//here you can console log to check eg: console.log(this.bookType);
//next you need to use the spread operator
this.bookUpdated.next([...this.bookType]);
});
}
bookUpdateListener() {
return this.bookUpdated.asObservable();//You can subscribe to this in you TS file
}
}
Now in your TS file you should subscribe to the update listener. This is typically done in NgOnInit
Something like this:
export class PaymentComponent implements OnInit {
addedBook: BookType;
product:any;
constructor(private bookService: BookService) { }
ngOnInit(): void {
this.bookService.bookUpdateListener().subscribe((response)=>{
this.addedBook = response;//this will happen every time the service class
//updates the book
});
//Here you can call the get book method
this.bookService.getBookList();
}
delete() {
this.addedBook.splice(this.addedBook.indexOf(this.product), 1);
}
}
Essentially what happens is you are subscribed to when books get changed or updated. Now you can simply use addedBook.title or whatever you want in your HTML.

Angular: set selected value for <select>

I have data set for my <select> loaded asynchronously. I use hot observable, as the data can change in time. The problem is I am unable to set selected value and also Angular does not point to first element by itself (there is no value set until user does it). I'm trying to subscribe to my own observable and... it doesn't work, I wonder why? How can I solve this problem?
PLNKR: https://plnkr.co/edit/uDmTSHq4naYGUjjhEe5Q
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
</div>
<select [(ngModel)]="selectedValue">
<option *ngFor="let value of (values$ | async)"
[value]="value">{{ value }}
</option>
</select>
`,
})
export class App implements OnInit, OnDestroy {
public name: string;
public selectedValue: string = '';
public values$: Observable<Array<string>> = new Observable(observer => this.observer = observer);
protected observer: Subscriber<Array<string>>;
protected subscription: Subscription;
constructor() {
this.name = `Angular! v${VERSION.full}`
}
ngOnInit() {
this.subscription = this.values$.subscribe((values) => {
console.log('never fired...');
this.selectedValue = values[0];
});
setTimeout(() => {
this.observer.next(['some', 'test', 'data']);
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
You subscribe to your observable twice. Async pipe does it internally after your subscription.
When subscribe method is being executed it executes subscribe function
observer => this.observer = observer
and overrides this.observer property so it will have effect only for async pipe(last subscriber)
I would use share operator to solve it
new Observable(observer => this.observer = observer).share();
Plunker Example
To see why this.observer is overrided just run this code
let myObserver;
const observable$ = new Rx.Observable(function subscribe(observer) {
console.log('subscribe function has been called');
myObserver = observer;
});
observable$.subscribe(function next1() { console.log('next1'); });
observable$.subscribe(function next2() { console.log('next2'); });
observable$.subscribe(function next3() { console.log('next3'); });
myObserver.next();
jsbin.com
As i mentioned early async pipe subscribes to observable internally
https://github.com/angular/angular/blob/4.3.x/packages/common/src/pipes/async_pipe.ts#L23
You should be using
[ngValue]="value"
instead of [value]
Assigned a value as
public selectedValue: string = 'test';
HTML should be changed as
<select [(ngModel)]="selectedValue">
<option *ngFor="let value of values$ | async"
[ngValue]="value">{{ value }}
</option>
</select>
Updated Plunker
You should bind it to ngValue:
<div>
<h2>Hello {{name}}</h2>
</div>
<select [(ngModel)]="selectedValue">
<option *ngFor="let value of (values$ | async)"
[ngValue]="selectedValue">{{ value }}
</option>
</select>

Categories

Resources