angular-redux / form

Keep your Angular2+ form state in Redux
MIT License
41 stars 15 forks source link

Infinite bidirectional loop when altering state on FORM_CHANGED #57

Closed colinturner closed 6 years ago

colinturner commented 6 years ago

This is a...

What toolchain are you using for transpilation/bundling?

Environment

NodeJS Version: Typescript Version: Angular Version: @angular-redux/store version: @angular/cli version: (if applicable) OS:

Link to repo showing the issus

(optional, but helps a lot)

Expected Behaviour:

I have used this library to connect a form input to my redux store. However, on the frontend I would like to apply an angular pipe to the input field that will format the entered integer into a comma-separated string (e.g. 10000 --> '10,000'). I would like my store to maintain the integer (10000) as the input's value, whereas the user will see the string '10,000'.

Actual Behaviour:

I tried applying a transformation to the payload (the formatted string), when '@@angular-redux/form/FORM_CHANGED' is triggered, to transform the formatted string from the input field into a simple integer. However, this resulted in an infinite loop of "FORM_CHANGED" actions being dispatched.

Stack Trace/Error Message:

Additional Notes:

This is POC code that resolves the issue described.

ngAfterContentInit() {
    Promise.resolve().then(() => {
      this.resetState();

      this.stateSubscription = this.store.subscribe(() => {
        this.formChangedSoOmitNextChange = true;              // SET TRUE HERE
        this.resetState()
      });

      Promise.resolve().then(() => {
        this.formSubscription = (<any>this.form.valueChanges)
          .debounceTime(0)
          .subscribe((values: any) => {
            if (this.formChangedSoOmitNextChange) {
              this.formChangedSoOmitNextChange = false;       // SET FALSE HERE
            } else this.publish(values)            
          });
      });
    });
  }

image

Template code:

<input
          class="form-control border-none"
          name="chairs"
          required
          min="0"
          [ngModel]="(woodshopState$ | async).data.chairs | number: '1.0-0' | numberHideZero"
        >

FORM_CHANGED action handler code:

switch (action.type) {
    case '@@angular-redux/form/FORM_CHANGED':
      return <IWoodshopState>{
        ...state,
        data: {
          ...state.data,
          chairs: has('chairs', action.payload.value)
          ? toNumber(action.payload.value.chairs)
          : state.data.chairs,
        },
      };
smithad15 commented 6 years ago

One option I might suggest is to decouple your form state from your final store state, especially in situations like this. Form state is usually temporary. Once a user is done with a form, the state of the form is no longer required. Once the form has been submitted, the data can be stored in a more permanent fashion somewhere else in your state tree. This allows for a couple of things.

The first, which you are dealing with right now, is that the value you display to your user doesn't have to match the final value that the application uses as you can do a one time transformation on submission of a valid form.

The other affordance is that your form no longer has to exactly match the final data structure that you want to use inside of your application. No need to deal with ngModelGroup to create nested object structures if so desired.

Another option would be to look into an input mask library like ngx-mask that allows for different values between model and display.

As a side note, do be careful about how input masks affect the accessibility of your site as they can sometimes cause issues.

I'm going to close this as I don't believe this is a problem that this library should address at this point in time. I will add the idea of input masks/transformations onto the to be considered list though. Thanks for highlighting the use case. Best of luck!