cloudnc / ngx-sub-form

Utility library for breaking down an Angular form into multiple components
https://cloudnc.github.io/ngx-sub-form
MIT License
314 stars 33 forks source link

get model updates when invalid #221

Open codehunter13 opened 3 years ago

codehunter13 commented 3 years ago

Hi,

Is there a buildin way to get the form model in the container component when the form is invalid? if i get it correctly, dataOutput only fires when the form is valid?

my case: all field are required initial values send from container to form: null ,null,null => form is invalid user change a value: "A", null, null => form still invalid

container components gets a value from another service that must be send to the form. So i need to send "A", newvalue, null from container to form But it doesnt know there is value A in the first field.

i can add my own eventemitter to the form but just want to be sure there isnt something buildin.

Thx in advance

maxime1992 commented 3 years ago

Hi @codehunter13 :wave:

What you said is correct :heavy_check_mark:

When using the NgxAutomaticRootFormComponent we do the following check:

https://github.com/cloudnc/ngx-sub-form/blob/master/projects/ngx-sub-form/src/lib/ngx-automatic-root-form.component.ts#L18

And when using NgxRootFormComponent we do the following check:

https://github.com/cloudnc/ngx-sub-form/blob/master/projects/ngx-sub-form/src/lib/ngx-root-form.component.ts#L98

If this is in your case a 1 time thing and you don't really need to reuse this behavior then I'd do what you said and have a custom event. If it's a recurring pattern on your side it may be worth creating a new class in your project, extends NgxRootFormComponent and override the manualSave method to now include the check on this.formGroup.valid.

@zakhenry this is the first time someone mention this so maybe a little bit early to really consider the use case which has an easy workaround but here's the open question: May it be worth to have some config passed when we create the form to say whether it should emit or not when the form is invalid? :thinking: It'd be fairly easy to implement I reckon but I'm not 100% sure it makes sense to have this supported.

@codehunter13 another suggestion if you want to skip the custom event emitter:

If you need to access the form value (valid or invalid) from the parent component you can simply add a template reference on your form component and access the formgroup instance like this:

<my-root-form-component #myRootForm></my-root-form-component>

and then using ViewChild to access it and it's public variable giving access to the form group :+1:

codehunter13 commented 3 years ago

i've already extended NgxRootFormComponent and added a second eventemitter. The current implementation for manualsave is correct for me. The form has a save button, this button calls the manualsave and this can only happen when the form is valid. I my case i need the updated values in the container without manualsave called. i would suggest 2 eventmitters in NgxRootFormComponent: one for when manualsave is called (so when the form is valid) => this one can be used to save to backend or whatever,.. and one that always fires when there is a change in the form (to be able to combine the current values with new data and send it back to the form, or to update an internal state service for example).

JacoboGarcesO commented 2 years ago

@codehunter13 I have the same need

maxime1992 commented 2 years ago

Hello :wave:

There's one problem I can think of and have no idea how to go around:

While it'd work fine on the root level because it'd be as easy as creating an output and instead of creating a new event emitter for it, set it to this.form.formGroup.valueChanges which would emit without any conditions based on form validity... How would that work for sub forms? Sub forms are by design not emitting while in invalid state. And ControlValueAccessor only exposes one way in, one way out. Meaning the only solution would be to manually bind an output and reconcile that with some side form state in the parent in order not to affect the main one and put it in invalid state. It'd mean that you'd have to probably only use sub forms you're in charge of to make sure you implement that pattern all the way up your form. That seems a bit error prone and tedious.

Can anyone think of a better way to achieve this?

JacoboGarcesO commented 2 years ago

Hi, it is not really necessary to add this Event Emitter in the subforms, if the root forms have it, it is enough.

It is my POV and it would work for me if I implemented a strategy as fast as possible in the root forms.

In previous versions, this need was solved with this:

  onFormUpdate(formUpdate: FormUpdate<UserProfileModel>): void {
    setTimeout(() => {
      if (formUpdate?.groupId) { this.getUsersInGroup(); }
      this.cdRef.markForCheck();
    });
  }

Now I don't see anything unreasonable to add an Event Emitter in the root forms that is emitting the value of the form every time that one of its controls changes.

In my opinion, the subforms do not need to be changed.

I hope it helps.

maxime1992 commented 2 years ago

Thanks for sharing your POV.

On

In my opinion, the subforms do not need to be changed.

I think it may be quite confusing to have an event on a root form to get the form state even if it's invalid but it'll only be the case for the top part of the form, not the children.

If that's really what you want, I don't think we need to include anything into ngx-sub-form itself to do that, all you have to do is add an output on your root form with the following code:

@Output() formValueNoValidityChecks = this.form.formGroup.valueChanges;

Does is seem reasonable to you? having any kind of wrapper to do that from ngx-sub-form wouldn't help much as you've have at least to declare the output yourself (otherwise Angular compiler wouldn't be happy). It isn't much longer to assign this.form.formGroup.valueChanges.

Note: I haven't checked but from memory valueChanges emits even if the form is invalid. Could be wrong

JacoboGarcesO commented 2 years ago

Hi, you gave me a great idea with this.form.formGroup.valueChanges.

Here is the implementation I did to solve my need:

export class FormUserComponent implements OnInit, OnDestroy {
  @Input() currentUser: CurrentUserModel;
  @Input() hobbies: OptionModel[];
  @Input() states: OptionModel[];
  @Input() cities: OptionModel[];
  @Output() stateSelected: EventEmitter<string> = new EventEmitter();
  private subscriptions: Subscription;

  public manualSave$: Subject<void> = new Subject();
  private input$: Subject<CurrentUserModel> = new Subject();
  @Input() set dataInput(value: CurrentUserModel) {
    this.input$.next(value);
  }

  private disabled$: Subject<boolean> = new Subject();
  @Input() set disabled(value: boolean) {
    this.disabled$.next(!!value);
  }

  @Output() dataOutput: Subject<CurrentUserModel> = new Subject();
  form = createForm<CurrentUserModel>(this, {
    formType: FormType.ROOT,
    input$: this.input$,
    disabled$: this.disabled$,
    output$: this.dataOutput,
    manualSave$: this.manualSave$,
    formControls: {
      email: new FormControl(null, [Validators.required, Validators.pattern(REGEX_RESOURCE.email)]),
      firstName: new FormControl(null, Validators.required),
      lastName: new FormControl(null, Validators.required),
      image: new FormControl(null, Validators.required),
      instagram: new FormControl(null, Validators.required),
      description: new FormControl(null, Validators.required),
      phoneNumber: new FormControl(null, Validators.required),
      hobbies: new FormControl(null, Validators.required),
      document: new FormControl(null, Validators.required),
      location: new FormControl(null),
    },
  });

  ngOnInit(): void {
    this.init();
    this.initChangesListener();
  }

  ngOnDestroy(): void {
    this.destroy();
  }

  private init(): void {
    this.subscriptions = new Subscription();
  }

  private destroy(): void {
    this.subscriptions.unsubscribe();
  }

  private initChangesListener(): void {
    this.subscriptions.add(
      this.form.formGroup.valueChanges.pipe(
        tap(this.queryStates.bind(this)),
      ).subscribe(),
    );
  }

  private queryStates(formUpdate: CurrentUserModel): void {
    if (formUpdate?.location?.state && !formUpdate?.location?.city) {
      this.stateSelected.emit(formUpdate.location.state);
    }
  }
}

Conclusion: There is no need to change anything in the library, my problem was more associated to the lack of knowledge about the latest version. Thank you very much for your cooperation.