Reputation: 2909
I have an Api that returns an array of objects and converts it into an object for ng2-select2 like so
getRoles(): Observable<Array<Select2OptionData>> {
return this.authHttp.get(`${this.usersUrl}/GetRoles`)
.map(function (response) {
var result = response.json();
var temp = [];
result.forEach(function (dept) {
var d = {
'id': '0',
'text': dept.name,
'children': []
};
dept.roles.forEach(function (role) {
var r = {
'id': role.id.toString(),
'text': role.name
};
d.children.push(r);
}, this);
temp.push(d);
}, this);
return temp;
})
.catch(this.handleError);
}
The return of this is an Array of Select2OptionData that has children
export interface Select2OptionData {
id: string;
text: string;
children?: Array<Select2OptionData>;
additional?: any;
}
In order to load the Dropdown, I have to pass it the observable like so
getRoles(): void {
this.roleDepartments = this.userService
.getRoles();
}
<select2 class="form-control" [data]="roleDepartments | async" (valueChanged)="changed($event)" [width]="300"></select2>
This works as expected and the dropdown is populated with data when it is loaded. I now want to be able to capture the value that is selected on change and that is supported. The issue is I need to iterate over the values of "roleDepartments" and get the whole object for the selected value. Since the object is Observable, I cannot iterate over it. I have tried subscribing to it, assigning the result into a variable and iterating over that. When I try that, the Api that populates the Observable is called again. Am I going about doing this the best way? If not, what should I be doing?
Upvotes: 3
Views: 1129
Reputation: 38151
First of all, the reason that subscribing to the observable again is that it is what is known as a "cold observable". That means that every new subscriber will get a new copy of the observable, and kick off any work done to create that observable. In the case of Http
, it will make a new request.
So we need a way to turn it from a "cold" observable into one that won't resubmit the request when we subscribe, but rather return the most recent result immediately instead. We can do that by adding on the .publishReplay(1)
operator followed by the .refCount()
operator so that the first subscriber kicks off the request, and the last unsubscriber unsubscribes from the observable in question.
This way, in the change()
method, you can do:
this.roleDepartments.take(1).subscribe(roles => {
let option: Select2OptionData;
roles.forEach(role => {
if (role.id === selectedId) {
option = role;
}
});
});
That will get only the replayed value (the most recent value emitted by the original observable) and then unsubscribe thanks to the .take(1)
.
This approach is less prone to leaks, I believe, because there is no chance of leaving a dangling subscription to the observable that you forget to unsubscribe from. This is because using the async
pipe is safe - Angular takes care of subscribing and unsubscribing correctly for you, and the other subscription in the change()
method only happens after the initial load of data and then immediately automatically unsubscribes.
A simpler solution (that is easier to screw up and cause leaks) is to have your component subscribe to the observable once in the ngOnInit()
hook, store the array of roles away as well as the subscription object, and then in the ngOnDestroy()
hook, unsubscribe. Finally, in the template, you don't need to use the async
pipe at all.
It is easier to cause leaks because if you (or a future developer) forget to unsubscribe, you risk introducing a memory leak by preventing your controller instance from being cleaned up.
But if you are happy to take that risk, here is what it would look like:
@Component({
// ...
})
export class MyComponent implements OnInit, OnDestroy {
private rolesSubscription: Subscription;
private roleDepartments: Select2OptionData[];
constructor(private userService: UserService) { }
ngOnInit(): void {
this.rolesSubscription = this.userService.getRoles()
.subscribe(roles => this.roleDepartments = roles);
}
ngOnDestroy(): void {
this.rolesSubscription.unsubscribe();
}
change(selectedId: string) {
// find correct role from this.roles and do something with it
}
}
and then in your template, change it slightly to:
<select2 class="form-control" [data]="roleDepartments"
(valueChanged)="changed($event)" [width]="300">
</select2>
Upvotes: 1