Reputation: 145
I have a table that I am filtering based on a user inputted field, and the source of that table is an observable made from a BehaviorSubject. Here's the setup:
class Component {
//filled with a http call to a webservice
private subject$ = new BehaviorSubject([])
public state = 'Open';
public workOrders$ = this.subjects$.asObservable().pipe(map(orders => {
return orders.filter(f => f.State == this.state)
}));
}
and the HTML for the tr that is being repeated is
<tr *ngFor="let order of workOrders$ | async">
and the HTML for the input field for the state
<input type="text" [(ngModel)]="state" />
On initial load, this correctly filters all the work orders by state, but whenever this.state
changes, I want to have that pipe refilter the array to update the table. What's the best practice for accomplishing this kind of task? I don't want to mutate the subject because I need to keep the original dataset for when the filter is changed again.
Upvotes: 0
Views: 2000
Reputation: 1729
You need to make a subscription to changes in the input value This can be done by either converting the form (Assuming the input field is within a form) into a reactive form, or calling a function whenever the input value changes More information on reactive forms can be found here I will go over the second and easiest method: HTML
<input type="text" (ngModelChange)="dataChanged()" [(ngModel)]="state" />
TS
class Component {
//filled with a http call to a webservice
private subject$ = new BehaviorSubject([])
public state = 'Open';
public allorders
//Variable to hold all the subjects before applying any filter
this.subjects$.asObservable().subscribe( val => {
this.allorders = val
})
}
dataChanged(){
this.allorders.filter(f => f.State == this.state)
}));
}
Upvotes: 0
Reputation: 7427
You mentioned state
was from an input field, which means (assuming the FormsModule
is imported, which it should be considering the ngModel
) we can get that value as an Observable.
You can use switchMap
to subscribe to one Observable, map its emissions to a second Observable, and have the resulting Observable emit values from the second Observable until the first Observable emits again (you switch to the new Observable).
You can get the Observable of value changes from your form control a number of different ways depending on how the form is set up, or even just using a (keyup)
binding. With no
ngForm
declared in the template attached to the control, you can just query the ngModel
via template binding:
<input type="text" [(ngModel)]="state" #value="ngModel"/>
then in your component:
class Component {
@ViewChild('value') value: NgModel;
//filled with a http call to a webservice
private subject = new BehaviorSubject([])
public state = 'Open'; // This remains as a component property, but we don't need it for the Observable stream as we'll get the value changes Observable from the form control.
public workOrders = this.subject.asObservable().pipe(
switchMap(this.value.valueChanges),
withLatestFrom(this.subject),
map([inputValue, orders] => {
return orders.filter(f => f.State === inputValue.value)
}));
}
You could also separately manage the subscription from both the http call and the input value changes, but that's no fun.
As you can see, the above gets a little complicated; we need withLatestFrom
to keep the value of the http call, which I'm assuming is static once received. Since you're using the async
pipe in the markup to subscribe to and call the Observable, it might be a better design choice to just filter that result with another pipe!
@Pipe({name: 'filterState'})
export class FilterStatePipe implements PipeTransform {
transform(value: YourTypeHere[], state: string) {
return value.filter(s => s.State === state);
}
}
Then declare it in your module and use it in your markup like so:
<input type="text" [(ngModel)]="state" />
<tr *ngFor="let order of workOrders$ | async | filterState:state">
The async
unwraps the Observable for you, then your custom pipe will filter that value based on the value of state
. When state
changes, the pipe should be re-run.
Upvotes: 0
Reputation: 1073
You could use the rxjs combineLatest function and have an observable track the need to recompute:
private subject$ = new BehaviorSubject([])
public state = 'Open';
public stateSubject = new BehaviorSubject(this.state);
public workOrders$ = combineLatest(
this.subjects$,
this.stateSubject,
(orders, state) => {
return orders.filter(f => f.State == state)
}
);
And then on your input:
<input type="text" (ngModelChange)="stateSubject.next($event)" [(ngModel)]="state" />
Upvotes: 1