Reputation: 3082
So, this is my use case: I get an observable from rest API, then by using async
within the template I foreach the data. On click, I get the ID of particular user and then fill-in the form with the data of that user. To achieve the latter, I reuse the existing observable and filter the data, then subscribe to it. I was wondering if my approach is correct, since I believe that app is too slow when I click "edit" for the form to get filled, so I'm guessing here that I create too many subscriptions or whatnot?
Also, for the sake of argument, I created two observables (user
and users
) and one (user
) gets a "reference" to another (users
), then it is displayed in the template with async
, while I also subscribe to it and set it to a "regular" variable. Is this an issue? To display an observable with async
and also subscribing to it?
Here's the code, since my question might be a bit confusing:
//our root app component
import {Component, NgModule, VERSION, OnInit, OnDestroy} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/find';
import 'rxjs/add/operator/takeUntil';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserService } from './service';
@Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
<div>
{{user | async | json}}
{{selected | json}}
</div>
<div *ngFor="let u of users | async">
{{u.first_name}} {{u.last_name}}
<br />
<img [src]="u.avatar" />
<br />
<br />
<a (click)="edit(u.id)">Edit</a>
<br />
<br />
</div>
<div>
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" novalidate>
<input type="text" formControlName="first_name" />
<input type="text" formControlName="last_name" />
</form>
<p>userForm value: {{ userForm.value | json}}</p>
<p>validity: {{ userForm.valid }}</p>
</div>
</div>
`,
styles: ['a { cursor: pointer; }']
})
export class App implements OnInit, OnDestroy {
users: Observable<any>;
user: Observable<any>;
destroy$: Subject<boolean> = new Subject<boolean>();
name:string;
userForm: FormGroup;
constructor(private fb: FormBuilder, private service: UserService) {
this.name = `Angular! v${VERSION.full}`;
this.createForm();
}
createForm() {
this.userForm = this.fb.group({
first_name: ['', {updateOn: 'blur', validators: [Validators.required, Validators.minLength(2), Validators.maxLength(10)]}],
last_name: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(20)]]
})
}
edit(id) {
this.user = this.users.map(x => x.find(x => x.id === +id));
this.user.takeUntil(this.destroy$).subscribe(u => {
console.log(u);
this.selected = u;
this.userForm.patchValue({
first_name: u.first_name,
last_name: u.last_name
});
});
}
ngOnInit() {
console.clear();
console.log('hello world');
this.users = this.service.all();
}
ngOnDestroy() {
this.destroy$.next(true);
this.destroy$.unsubscribe();
}
}
@NgModule({
imports: [ BrowserModule, HttpClientModule, ReactiveFormsModule ],
providers: [UserService],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule {}
and here's the link: http://plnkr.co/edit/F0dheIdgoNNw53hNVjZm?p=preview
Upvotes: 0
Views: 1389
Reputation: 5764
Let's say you want to use observables for this task. Although there is more straightforward method in the answer from Leon but for sake of learning about Observables let's do it :-)
You are subscribing to user observable in the template using async pipe but also in edit method. The subscription in edit method is never unsubscribed so there is a memory leak.
Also you redefine this.user
observable each time you click on user and letting async pipe in template to resubscribe. This results in another memory leak since async pipe has no clue about the overriden observable which should be unsubscribed.
This is not really how you should compose your observable streams. First you should define the observable data transformation.
When I look on your application you have two sources of data/events:
Users are just observable from a service. Clicks should be really Subject
. Because on edit you should emit the userId which was clicked (observable) and react on this click by displaying user detail (observer behaviour).
editClick: Subject<number> = new Subject<number>;
So let's define how user
observable should look like (all this code in ngOnInit
):
this.users = this.service.all().shareReplay();
this.user = this.editClick
.switchMapTo(this.users, (userId, users) => ({ userId, users}))
.map(x => x.users.find(user => user.id === +x.userId))
.do((u) => {
console.log(u);
this.selected = u;
this.userForm.patchValue({
first_name: u.first_name,
last_name: u.last_name
});
});
Here I define user observable based on editClick - thats where I will get id from later. Using switchMapTo
I combine users
observable and id from editClick
into object which I use later to filter out clicked user.
Now how to trigger editClick
in onEdit
method? Just emit userId
on each click like this:
edit(id) {
this.editClick.next(id);
}
Notice how I define data stream in ngOnInit
. Just once at beginning. No recreation or something. I have two observables to subscribe:
users
to display users listuser
to display detail - this one will emit each time I trigger editClick
Also there is only subscription in template therefore no memory leaks since async
pipe will manage unsubscribe
for you.
Upvotes: 1
Reputation: 7672
You can greatly simplify the edit method by passing the whole user instead of just the id.
<a (click)="edit(u)">Edit</a>
edit(user) {
this.selected = user;
this.userForm.patchValue({
first_name: user.first_name,
last_name: user.last_name
});
}
As for calling service.all()
multiple times will cause multiple subscriptions and multiple calls to the backend.
If you want to call it multiple times use the .share()
rxjs operator.
That will create an wrapper subject around the observable which will return the same value each time it is called.
Upvotes: 0