Compare Previous Values with Output Emitter and ValueChanges

Viewed 3420

We have an Angular Child Form emitting values with Valuechanges. In the Parent Component we want to run tasks if the new zip code value from child is different from previous zip code value.

Right now, we are creating a variable to store previous zip codes.

Is there a cleaner way to conduct this with Output? I know NgOnchanges for Input, we can track previous changes with changes.previousValue.

Curious if Output value changes have something similar?

Child Form:

public addressSub = new Subscription();
public editAddressForm: FormGroup;
@Output addressFormChange = new EventEmitter<any>();

this.addressSub.add(this.editAddressForm.valueChanges.subscribe(data=> {
    this.addressFormChange.emit(this.editAddressForm);
})))

Parent Component:

In the Parent, it is subscribing as.

public addressFormChangeEvent(addressFormEvent){
    this.addressMailingForm = addressFormEvent;
    if (this.addressMailingForm.get('zipCode') != previousZipCode) {
         doSomethingetc();
    }
    previousZipCode = this.addressMailingForm.get('zipCode');
}

Does Angular have something in library to see previous values with ValueChanges Subscription?

3 Answers

IMO much simpler way of doing this is with help of pairwise and startWith.
Also, you could pass down the form from parent component to the child and subscribe in the parent component, eg:

form.valueChanges
    .pipe(startWith(undefined), pairwise())
    .subscribe(valuesArray => {
         const newVal = valuesArray[1];
         const oldVal = valuesArray[0];
         if (newVal !== oldVal) {
             // do your thing
         }
    })

I would try not to emit duplicates from the child component, so that the parent only receives zip code when it changes.

First improvement

We'll use two form listeners:

  • valueChanges : listens form value changes. Emits every change, e.g: when you are typing in the zip code input '1'... '15'... '150'... every new letter will be a new emit event
  • statusChanges : listens form status. So we know if the form (and zip code) is valid or invalid.

We'll combine these so you will only emit valid values to the parent.

E.g Component output events

I'll only include zip codes in the examples data, valid when length is 5.

  • BEFORE: [1, 15, 150, 1510, 15109, 2, 28, 286, 2860, 28600, 2, 28, 286, 2860, 28600, 7, 74, 746, 7462, 74625, 7, 74, 746, 7462, 74625]
  • AFTER: [15109, 28600, 28600, 74625, 74625]

Second improvement

Ok, now we want to filter those valid values to only emit not duplicated ones.

For this we'll use rxjs distinctUntilChanged

combineLatest([
  this.editAddressForm.statusChanges,
  this.editAddressForm.valueChanges,
]).pipe(
  filter(()=> this.editAddressForm.valid)
  map([status, value] => value),
  distinctUntilChanged()
)
.subscribe(value => {
  this.addressFormChange.emit(value);
});

That should emit only not duplicated values :)

E.g Component output events

  • BEFORE: [1, 15, 150, 1510, 15109, 2, 28, 286, 2860, 28600, 2, 28, 286, 2860, 28600, 7, 74, 746, 7462, 74625, 7, 74, 746, 7462, 74625]
  • AFTER: [15109, 28600, 74625]

Personally I would prefer rxjs namely: BehaviorSubject, ReplaySubject and AsyncSubject rather than EventEmitter in order to share data between components in Angular. For big size projects Redux is of course another option. Looking at your situation, it is good idea to use ReplaySubject as it can send “old” values to new subscribers.

The ReplaySubject:

The ReplaySubject is comparable to the BehaviorSubject in the way that it can send “old” values to new subscribers. It however has the extra characteristic that it can record a part of the observable execution and therefore store multiple old values and “replay” them to new subscribers.

When creating the ReplaySubject you can specify how much values you want to store and for how long you want to store them. In other words you can specify: “I want to store the last 5 values, that have been executed in the last second prior to a new subscription”. See example code below:

replaySubject.js:

import * as Rx from "rxjs";

const subject = new Rx.ReplaySubject(2);

// subscriber 1
subject.subscribe((data) => {
    console.log('Subscriber A:', data);
});

subject.next(Math.random())
subject.next(Math.random())
subject.next(Math.random())

// subscriber 2
subject.subscribe((data) => {
    console.log('Subscriber B:', data);
});

subject.next(Math.random());

// Subscriber A: 0.3541746356538569
// Subscriber A: 0.12137498878080955
// Subscriber A: 0.531935186034298
// Subscriber B: 0.12137498878080955
// Subscriber B: 0.531935186034298
// Subscriber A: 0.6664809293975393
// Subscriber B: 0.6664809293975393


As mentioned before you can also specify for how long you wan to store values in the replay subject.

replaySubject-2.js:

import * as Rx from "rxjs";

const subject = new Rx.ReplaySubject(2, 100);

// subscriber 1
subject.subscribe((data) => {
    console.log('Subscriber A:', data);
});

setInterval(() => subject.next(Math.random()), 200);

// subscriber 2
setTimeout(() => {
  subject.subscribe((data) => {
    console.log('Subscriber B:', data);
  });
}, 1000)

// Subscriber A: 0.44524184251927656
// Subscriber A: 0.5802631630066313
// Subscriber A: 0.9792165506699135
// Subscriber A: 0.3239616040117268
// Subscriber A: 0.6845077617520203
// Subscriber B: 0.6845077617520203
// Subscriber A: 0.41269171141525707
// Subscriber B: 0.41269171141525707
// Subscriber A: 0.8211466186035139
// Subscriber B: 0.8211466186035139

For more detailed information have a look at Understanding rxjs BehaviorSubject, ReplaySubject and AsyncSubject. Hope this helps...

Related