There is a service UsersService
contains user logic:
interaface User {
id: number,
name: string,
checked?: boolean;
}
class UserService {
public users$ = new BehaviorSubject<User[]>([]);
public load() {
this.http.get('users').subscribe((users: Users[] => this.users$.next(users));
}
public get users() {
return this.users$.asObservable();
}
}
I get data from service inside component:
public users$: Observable<User[]>;
constructor(private userService: UserService) {
this.users$ = this.userService.users;
}
Template is:
<div> *ngFor="let user of users$ | async">
<div>{{ user.name }}</div>
<div><mat-checkbox [(ngModel)]="user.checked"></mat-checkbox></div>
</div>
<div>Checked users: {{ checkedusers | json }}</div>
How to get all checked users using rxjs approach {{ checkedusers | json }}
from stream after updated one checkbox or all checkboxes and incapsulate it into service?
CodePudding user response:
Use the scan operator to combine all of the sources that could modify the users list into one stream. Then in the component, instead of modifying user objects directly, send the changes to one of the subjects that is being used to modify the list:
Service
class UserService {
readonly loadUsersSubject = new Subject();
readonly userChangeSubject = new Subject<[userId: number, Partial<User>]>();
readonly users$ = combineLatest([
this.loadUsersSubject.pipe(
switchMap(() => this.http.get('users')),
map((users) => (s) => users))
),
this.userChangeSubject.pipe(map(([userId, partialUser]) => (s) => {
const index = s.findIndex(x => x.userId === userId);
return [...s.slice(0, index - 1), { ... s[index], partialUser }, ...s.slice(index 1)]
})
]).pipe(
scan((s, reducer) => reducer(s), []),
shareReplay(1)
)
}
Template
<div> *ngFor="let user of users$ | async">
<div>{{ user.name }}</div>
<div><mat-checkbox [ngModel]="user.checked"
(ngModelChange)="userService.userChangeSubject.next([user.id, { checked: $event.checked }])"></mat-checkbox></div>
</div>
<div>Checked users: {{ checkedusers | json }}</div>
A few comments:
- You don't have to call next on the subject directly on the service. I did that for brevity in the example. Feel free to wrap them in methods if that's your thing.
- The use of the tuple was again another decision made for brevity's sake.
CodePudding user response:
Something like this seemed to work:
import { Injectable } from '@angular/core';
import { merge, of, Subject } from 'rxjs';
import { map, scan } from 'rxjs/operators';
export interface User {
id: number;
name: string;
checked?: boolean;
}
@Injectable({ providedIn: 'root' })
export class UserService {
readonly userChangeSubject = new Subject<User>();
userChanged$ = this.userChangeSubject.asObservable();
allUsers$ = of(userData);
updateUser(user: User, checked: boolean) {
const updatedUser = { ...user, checked: checked };
this.userChangeSubject.next(updatedUser);
}
checkedUsers$ = merge(
this.allUsers$,
this.userChanged$
).pipe(
scan((users: User[], user: User) => users.map((u) => u.id === user.id ? user : u)),
map(users => users.filter((u) => u.checked))
);
}
export const userData: User[] = [
{ id: 1, name: 'Captain Jack', checked: false },
{ id: 2, name: 'Mal Reynolds', checked: false },
{ id: 3, name: 'NaomiNagata', checked: false },
];
Here is the updated link: https://stackblitz.com/edit/angular7-rxjs-ha6suw
- The Subject emits a user when the user's check is changed.
- The
allUsers$
will need to be changed to call your http endpoint - The updateUser is called from the component when a checkbox is checked or unchecked. It creates a new user object with the appropriate check flag and emits it into the Subject.
- The
checkedUsers$
merges the original set of users with each emitted changed user. It then usesscan
to retain the array of users. When an updated user is emitted, it locates that user in the array and replaces it. It then maps the resulting array to only display those that are checked.