cloudnc / ngx-sub-form

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

markAllAsTouched() not propegated to subforms #155

Closed ntziolis closed 3 years ago

ntziolis commented 4 years ago

Repro:

Expected:

Actual:

Note:

The intent of this issue is to try and come up with a best practice to handle this and if possible to extend the library to assist in these cases.

ntziolis commented 4 years ago

Was thinking about this issue, but also more generally that there are various others arising from not being able to have access to the parent formGroup of the sub-form.

They all stem from the limited interface of the ControlValueAccessor, which wasn't intended to handle sub forms. Hence I was thinking something along the lines of:

Goal:

Note: Im still looking into if there is a way to hook into things markAllAsTouched etc. on the now available parent formGroup. Will report back here when I had time to look at this.

ntziolis commented 4 years ago

Results of further investigation for how to hook into calls on the parent, sample markAllAsTouched():

I'm aware that especially the second approach would require significant changes in how the lib works today. Right now this is meant merely to spark a discussion of what you guys think of this.

ntziolis commented 4 years ago

So I took a swing at trying to see how exzessive it would be todo this:

I was slightly worried that the injection of the derived directive would get messed up, but no need it worked like a charm!

Below is the directive, there is an equivalent subFormdirective which takes a control instead of a name:

const controlNameBinding: any = {
  provide: NgControl,
  useExisting: forwardRef(() => SubFormNameDirective)
};

@Directive({ selector: '[subFormName]', providers: [controlNameBinding] })
export class SubFormNameDirective extends FormControlName implements OnInit {
  @Input('subFormName') name!: string;

  // this is copied from angular FormControlName directive
  // https://github.com/angular/angular/blob/master/packages/forms/src/directives/reactive_directives/form_control_name.ts
  constructor(
    @Optional() @Host() @SkipSelf() parent: ControlContainer,
    @Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator | ValidatorFn>,
    @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator | AsyncValidatorFn>,
    @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]
  ) {
    super(parent, validators, asyncValidators, valueAccessors, null);
  }

  ngOnInit(): void {
    const self = (this as unknown) as FormControlName;

    if (self.valueAccessor instanceof NgxSubFormComponent) {
      const subForm = self.valueAccessor;

      const control = self.control;

      const markAsTouched = control.markAsTouched.bind(
        control
      ) as AbstractControl['markAsTouched'];
      control.markAsTouched = (
        opts: Parameters<AbstractControl['markAsTouched']>[0]
      ) => {
        markAsTouched(opts);
        subForm.formGroup.markAllAsTouched();
      };

      const markAsUntouched = control.markAsUntouched.bind(
        control
      ) as AbstractControl['markAsUntouched'];
      control.markAsUntouched = (
        opts: Parameters<AbstractControl['markAsUntouched']>[0]
      ) => {
        markAsUntouched(opts);
        subForm.formGroup.markAsUntouched();
      };
    }
  }
}

What do you guys think?

RicardoArdito commented 4 years ago

Hi @ntziolis ,

First of all congrats for the great job you guys are doing with ngx-sub-form. I started using it a few days ago, with some very complex forms where we were struggling to handle validation. Using ngx-sub-form was pretty straight forward and did solved the enormous boilerplate we had due to our deep component tree on those forms.

But we soon noticed this problem with markAllAsTouched(): We use Angular Material, and although all fields were being correctly validated, it was not being reflected in the interface (since they were not being marked as touched on form submit). That was when we found this thread.

We implemented your SubFormNameDirective, and noticed a few points that I would like to share with you:

Please let me know if there is any thing we can do to help with those issues, or if we are missing something.

Thanks, guys!

ntziolis commented 4 years ago

@RicardoArdito First off, me and my team really are just heavy users of ngx-sub-form as well, credits for the lib should go to the creators ;)

In regards to your comments:

In general the markAllAsTouched is only the most prominent of various problems that arise with using FormControl's to hide complex forms underneath it as it severs the connection between main form and controls on the sub form.

Initially we thought we can get around this by using custom directives but that is not the case as we quickly ran into issues as you encountered as well. We need both a better way to intercept as well as propagate events / calls to the form controls on the sub form.

Here are some requirements we have for a solutions:

With that in mind we are thinking this could be achieved by:

Right now I believe such solution would not require custom directives, but merely the use of SubFormGroup instead for FormControl inside the getControls call.

What do you think?

maxime1992 commented 4 years ago

Hey guys, small update. We've had to focus on our app in the last few weeks but we will try to get back on ngx-sub-form a bit next week.

Thanks for keeping up great discussions in the meantime, we'll have a look asap :) !

PS: Thanks for the kind words Ricardo, feels free to share more about your experience here: https://github.com/cloudnc/ngx-sub-form/issues/112 !

MonsieurMan commented 4 years ago

We have the same use case as @RicardoArdito.

I prefer my submit buttons to never be disabled and show errors only when the user tries to submit the form instead. But I can't as there is no way to access child controls from root form for now.

@ntziolis, I follow you on the requirements. About the solution I don't know much about the code base but it seems right at a glance.

MiroslavKral commented 4 years ago

Hey, we have the same problem. Our solution is simple marker directive for sub forms components.

@Directive({
  selector: '[rbSubForm]',
})
export class RbSubFormDirective implements OnInit, OnDestroy {
  private destroy = new Subject<void>();

  constructor(private ngControl: NgControl, private rbForm: RbFormDirective) {}

  ngOnInit(): void {
    this.rbForm
      .onSubmit()
      .pipe(takeUntil(this.destroy))
      .subscribe(() => {
        const valueAccessor = this.ngControl
          .valueAccessor as NgxSubFormComponent<unknown>;
        valueAccessor.formGroup.markAllAsTouched();
      });
  }

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

Then we use it as follows:

<rb-address-form
  rbSubForm
  id="permanent-address"
  [formControlName]="formControlNames.permanentAddress"
></rb-address-form>

Yes it is manual solution, but it serves us well.

ntziolis commented 4 years ago

@MiroslavKral This (additional directive) was our initial approach as well and then we simply moved the logic into the subFormGroup to avoid having 2 directives. This approach works great for the markAllAsTouched use case.

However it does not work when wanting todo other things like reset the form (both data as well as touched state). While directive approach does make sure all formControls on all subForms are initially marked untouched, this gets reverted by the way the OnTouched is being called in the root NgxSubFromComponent. So one ends up with a partial reset form (data is reset, formControls are marked as touched).

ntziolis commented 4 years ago

The more we we make use of the sub-form library it becomes clear that we need access to the actual form controls on a sub form to enable full control from the outside if needed (while we actively trying to avoid it where possible)

I’m currently playing with creating a class derived from form group based on the requirements outlined above. Since this approach would allow to use formGroup of the sub form directly we wouldn’t need the sub-form-component class to handle things like on touched anymore. Trying to figure out if a change in the lib is required to make this work or if it’s possible to simply ignore what the lib does in regards to touched.

Will report back here soon.

maxime1992 commented 4 years ago

Can we all please give a :+1: to the following issue: https://github.com/angular/angular/issues/27315 ?

If that was solved upstream we could implement this feature really easily :heart:

elvispdosreis commented 4 years ago

please share RbFormDirective

Hey, we have the same problem. Our solution is simple marker directive for sub forms components.

@Directive({
  selector: '[rbSubForm]',
})
export class RbSubFormDirective implements OnInit, OnDestroy {
  private destroy = new Subject<void>();

  constructor(private ngControl: NgControl, private rbForm: RbFormDirective) {}

  ngOnInit(): void {
    this.rbForm
      .onSubmit()
      .pipe(takeUntil(this.destroy))
      .subscribe(() => {
        const valueAccessor = this.ngControl
          .valueAccessor as NgxSubFormComponent<unknown>;
        valueAccessor.formGroup.markAllAsTouched();
      });
  }

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

Then we use it as follows:

<rb-address-form
  rbSubForm
  id="permanent-address"
  [formControlName]="formControlNames.permanentAddress"
></rb-address-form>

Yes it is manual solution, but it serves us well.

agallardol commented 4 years ago

Hi everyone, today I was dealing with this problem and trying to use your ideas as inspiration I created the following gist that let me workaround the markAsTouched/markAllAsTouched problem.

NgxSubForm markAllAsTouched fix

All of your feedback is very preciated and I hope this could help someone of you.

elvispdosreis commented 4 years ago

could you please provide a usage example

<form [formGroup]="form" (ngSubmit)="onSubmit()">

because it already has a directive with the same name [formGroup]

agallardol commented 4 years ago

Hi @elvispdosreis I hope you will be fine. I created this example to demonstrate how works the proposed workaround. I think It could be easily extended and generalized.

NgxSubForm markAllAsTouched fix example

Tell me if I can help with another example or case 🤟

agallardol commented 4 years ago

Hi, I added minor changes to the example to add ngSubmit usage.

elvispdosreis commented 4 years ago

Thanks

is that correct? line 47

if (isSubForm) {
    subForm = value;
    return;
}
agallardol commented 4 years ago

Hi, It's ok, but I was reading again my code and I applied small refactorizations to improve the code readability. Basically I'm looking for a NgxSubForm instance inside any ControlValueAccessor (asuming this is in the root) of the FormGroup/FormControl.

maxime1992 commented 3 years ago

Hello :wave:

I'm glad we've got some work around explained above above but as explained here, this would really need to be fixed upstream so I've marked this issue as needs fix upstream and won't solve here I'm afraid.

I'll close this issue as there's nothing we can do more than offer work arounds, which has been done :)

Thanks everyone for the brainstorming around the work arounds!

elvispdosreis commented 3 years ago

I need help, I'm using your fix but it only marks the first form

https://stackblitz.com/edit/ngx-sub-form-basics-patchvalue?file=src%2Fapp%2Fngx-sub-form-mark-all-as-touched-fix.ts