Thomas Overfield
Thomas Overfield

Reputation: 145

Rerun pipe for observable when condition changes (Angular 7)

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

Answers (3)

Kisinga
Kisinga

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

joh04667
joh04667

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.

However...

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

rh16
rh16

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

Related Questions