Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Touched): Add ability for sub form components to mark sub control as touched with an observable #206

Merged
merged 1 commit into from
Mar 17, 2021

Conversation

zakhenry
Copy link
Contributor

No description provided.

@zakhenry zakhenry requested a review from maxime1992 March 16, 2021 23:38
@@ -75,6 +75,8 @@ export type NgxSubFormOptions<ControlInterface, FormInterface = ControlInterface
formGroupOptions?: FormGroupOptions<FormInterface>;
emitNullOnDestroy?: boolean;
componentHooks?: ComponentHooks;
// emit on this observable to mark the control as touched
touched$?: Observable<void>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should help a bit with #155 :)

@zakhenry zakhenry merged commit 543357e into cloudnc:feat-rewrite Mar 17, 2021
@zakhenry zakhenry deleted the feat/optional-on-touched branch March 17, 2021 19:18
@github-actions
Copy link

🎉 This PR is included in version 6.0.0-feat-rewrite.10 🎉

The release is available on:

Your semantic-release bot 📦🚀

@elvispdosreis
Copy link

could you give me an example of how this functionality works

@maxime1992
Copy link
Contributor

@elvispdosreis the change on the public API is the one pointed out in this comment above: #206 (review)

So what you can do is create a subject for example and provide in the createForm config the following key touched$ and pass your subject for the value. Then each time you emit in the subject, it'll mark them as touched

@rlzdesenv
Copy link

sorry but I don't understand this implementation, I'm looking for a way for an event to propagate from the parent form to the children

@188599
Copy link

188599 commented Dec 13, 2022

sorry but I don't understand this implementation, I'm looking for a way for an event to propagate from the parent form to the children

Same boat here.

Is this meant specifically for subforms and their own controls? Would be nice to have some example use cases.

@maxime1992
Copy link
Contributor

Here's what happens behind the scenes:

    bindTouched$: combineLatest([componentHooks.registerOnTouched$, options.touched$ ?? EMPTY]).pipe(
      delay(0),
      tap(([onTouched]) => onTouched()),
    ),

Meaning, we call the onTouched callback of a ControlValueAccessor every time the options touched$ is emitting a value.

So when you create your sub forms, what you can do is pass the options touched$ like this:

  private touched$$ = new Subject<void>();

  public form = createForm<...>(this, {
    formType: FormType.SUB,
    touched$: this.touched$$,
    ...
  }

And any time you emit in touched$$, it'll mark the form as touched.

@lgaida
Copy link

lgaida commented Dec 14, 2022

I think what most people want (including me) is to be able to trigger something like a 'touchedEvent' on the root-form which will automatically propagate down to all sub/childforms.
This is actually what AbstractControl.markAllAsTouched() is supposed to do: 'Marks the control and all its descendant controls as touched'). However markAllAsTouched does not work with ngx-sub-form, see #155. Even the workarounds provided in #155 are only applicable to the old api.

When somebody sees the touched$ of createForm, they will probably think of something like this (doesn't work):
stackblitz-example-editormode

  private touched$: Subject<void> = new Subject();

  public form = createForm<Client>(this, {
    formType: FormType.ROOT,
    input$: this.input$,
    output$: this.clientUpdated,
    touched$: this.touched$,
    formControls: {
      name: new UntypedFormControl(null, Validators.required),
      configuration: new UntypedFormControl(null),
    },
  });

  touch() {
    console.log('touch button was clicked');
    this.touched$.next();
  }

So when you create your sub forms, what you can do is pass the options touched$ like this:

  private touched$$ = new Subject<void>();

  public form = createForm<...>(this, {
    formType: FormType.SUB,
    touched$: this.touched$$,
    ...
  }

And any time you emit in touched$$, it'll mark the form as touched.

I don't see where this will help propagating onTouched from root to sub-forms (or even sub-forms to sub-forms) without having to declare & bind a 'touched$' property on each component down the whole "form-tree".

Would love to see someone forking my stackblitz and wiring it up as intended 👍

@188599
Copy link

188599 commented Dec 14, 2022

I think what most people want (including me) is to be able to trigger something like a 'touchedEvent' on the root-form which will automatically propagate down to all sub/childforms. This is actually what AbstractControl.markAllAsTouched() is supposed to do: 'Marks the control and all its descendant controls as touched'). However markAllAsTouched does not work with ngx-sub-form, see #155. Even the workarounds provided in #155 are only applicable to the old api.

When somebody sees the touched$ of createForm, they will probably think of something like this (doesn't work): stackblitz-example-editormode

  private touched$: Subject<void> = new Subject();

  public form = createForm<Client>(this, {
    formType: FormType.ROOT,
    input$: this.input$,
    output$: this.clientUpdated,
    touched$: this.touched$,
    formControls: {
      name: new UntypedFormControl(null, Validators.required),
      configuration: new UntypedFormControl(null),
    },
  });

  touch() {
    console.log('touch button was clicked');
    this.touched$.next();
  }

So when you create your sub forms, what you can do is pass the options touched$ like this:

  private touched$$ = new Subject<void>();

  public form = createForm<...>(this, {
    formType: FormType.SUB,
    touched$: this.touched$$,
    ...
  }

And any time you emit in touched$$, it'll mark the form as touched.

I don't see where this will help propagating onTouched from root to sub-forms (or even sub-forms to sub-forms) without having to declare & bind a 'touched$' property on each component down the whole "form-tree".

Would love to see someone forking my stackblitz and wiring it up as intended 👍

I tried to use the solution provided here, but it wasn't taking into account the use of formControlName directive, so I rewrote it into this:

import {
  ChangeDetectorRef,
  Directive,
  Inject,
  OnInit,
  Optional,
  Self,
} from '@angular/core';
import {
  AbstractControl,
  NG_VALUE_ACCESSOR,
  ControlValueAccessor,
  FormControlName,
} from '@angular/forms';
import { NgxSubForm } from 'ngx-sub-form';

@Directive({
  standalone: true,
  selector: '[formControl],[formControlName],[formGroup]',
})
export class NgxSubFormMarkAllAsTouchedFixDirective implements OnInit {
  public control!: AbstractControl;

  constructor(
    private cdr: ChangeDetectorRef,
    @Optional()
    @Self()
    @Inject(NG_VALUE_ACCESSOR)
    private valueAccessors?: ControlValueAccessor[],
    @Optional()
    private abstractControl?: AbstractControl,
    @Optional()
    private formControlName?: FormControlName
  ) {}

  public ngOnInit(): void {
    this.control ??= (this.formControlName?.control ?? this.abstractControl)!;

    if (this.control != null && this.valueAccessors !== null)
      this.applyMiddlewaresToMethods();
  }

  private implementsTKeys<T>(obj: any, keys: (keyof T)[]): obj is T {
    if (!obj || typeof obj !== 'object' || !Array.isArray(keys)) {
      return false;
    }

    const implementKeys = keys.reduce((impl, key) => impl && key in obj, true);

    return implementKeys;
  }

  private getNgxSubForm(
    valueAccessor: ControlValueAccessor
  ): NgxSubForm<unknown, {}> {
    const values = Array.from(Object.values(valueAccessor));

    const ngxSubForm: NgxSubForm<unknown, {}> = values.find((value: any) =>
      this.implementsTKeys<NgxSubForm<unknown, {}>>(value, [
        'formGroup',
        'controlValue$',
        'formControlNames',
      ])
    );

    return ngxSubForm;
  }

  private overrideMarkAsTouched(subForm: NgxSubForm<unknown, {}>): void {
    const markAsTouched = this.control.markAsTouched.bind(
      this.control
    ) as AbstractControl['markAsTouched'];
    this.control.markAsTouched = (
      opts: Parameters<AbstractControl['markAsTouched']>[0]
    ) => {
      markAsTouched(opts);
      subForm.formGroup.markAllAsTouched();
      this.cdr.markForCheck();
    };
  }

  private overrideMarkAsUnTouched(subForm: NgxSubForm<unknown, {}>): void {
    const markAsUntouched = this.control.markAsUntouched.bind(
      this.control
    ) as AbstractControl['markAsUntouched'];
    this.control.markAsUntouched = (
      opts: Parameters<AbstractControl['markAsUntouched']>[0]
    ) => {
      markAsUntouched(opts);

      subForm.formGroup.markAsUntouched();

      this.cdr.markForCheck();
    };
  }

  private applyMiddlewaresToMethods() {
    this.valueAccessors?.forEach((valueAccessor) => {
      const ngxSubForm = this.getNgxSubForm(valueAccessor);

      if (!ngxSubForm) return;

      this.overrideMarkAsTouched(ngxSubForm);
      this.overrideMarkAsUnTouched(ngxSubForm);
    });
  }
}

It seems to work properly, even on the new API.

Still, it's quite problematic if you are using the new standalone components on angular 15, like myself, since you then need to import this directive on every single form and sub-form component to make it work.

I tried to make a stackblitz to show it working using the code sample from this repo as a base, but I couldn't get it to work (missing types for the module 'uuid', even if add the types as dependency). If you anyone wants to mess around with it, feel free to check it here.

Edit: Also, I removed the provider from my current solution, but I'm not entirely sure of the purpose of it. It seems to work fine on my end, with my current form though.

@elvispdosreis
Copy link

I couldn't make it work, it doesn't propagate to the subforms

@elvispdosreis
Copy link

elvispdosreis commented Mar 28, 2023

I am still having difficulty using the
Ngxsubformmarkallascouchedfixdirective, works only 1 level of subform, in aligned subforms does not work, and does not work in subform morph.

I don't see a problem with having to declare touch $ in all form and subform, the problem and that I can't make the events to propagate

https://stackblitz.com/edit/angular-ivy-kzcv41

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants