TransWikia.com

How to get a directive to react to an EventEmitter in a component

Stack Overflow Asked by coder101 on November 7, 2021

I have a CustomComponent which emits a value (let’s just call it "error") if a http request to the back end api returns an error. How can I get a directive (call it Form Directive), applied to this form, to recognize when the "error" value is emitted by CustomComponent?

Code for CustomComponent:

export class CustomComponent extends FormComponent<Custom> {

  constructor(
    protected fb: FormBuilder,
    private httpService: HttpService) {
    super(fb);
  }

  currentVal: string = '';
  inputType: string = 'password';
  showPasswordTitle: string = 'Show Password';
  showPasswordStatus: boolean = false;
  form: FormGroup;

  @Output() invalidOnError = new EventEmitter<string>();

  protected buildForm(): FormGroup {
    return this.form = this.fb.group({
      fieldA: ['', Validators.required],
      fieldB: ['', Validators.required],
      fieldC: [''],
      fieldD: ['', [Validators.required, Validators.pattern('[0-9]{10}')]]
    }

  protected doSubmit(): Observable<Custom> {
    return this.httpService.callDatabase<Custom>('post', '/api/users/custom', this.value);
  };

  protected get value(): Registration {
    return {
      fieldA: this.fieldA.value,
      fieldB: this.fieldB.value,
      fieldC: this.fieldC.value,
      fieldD: this.fieldD.value
    };
  }

  get fieldA() { return this.form.get('fieldA'); }
  get fieldB() { return this.form.get('fieldB'); }
  get fieldC() { return this.form.get('fieldC'); }
  get fieldD() { return this.form.get('fieldD'); }

  protected onError() {
    if (this.error.length) {//error.length indicates some of the fields in the form are already registered in the database
      Object.keys(this.error).forEach(element => {
        let formControl = this.form.get(this.error[element])
        this.currentVal = formControl.value;
        formControl.setValidators(formControl.validator ? [formControl.validator, unique(this.currentVal)] : unique(this.currentVal))
        formControl.updateValueAndValidity()
        this.invalidOnError.emit('error');
      })
    }
  }

Code for FormComponent:

export abstract class FormComponent<T> implements OnInit {
  protected form: FormGroup = null;
  submitted = false;
  completed = false;
  error: string = null;

  constructor(protected fb: FormBuilder) {}

  ngOnInit() {
    this.form = this.buildForm();
  }

  onSubmit() {
    this.submitted = true;
    if (this.form.valid) {
      this.doSubmit().subscribe(
        () => {
          this.error = null;
          this.onSuccess();
        },
        err => {
          this.error = err
          this.onError();
        },
        () => {
          this.submitted = false;
          this.completed = true;
        }
      )
    }
  }

  protected abstract get value(): T;
  protected abstract buildForm(): FormGroup;
  protected abstract doSubmit(): Observable<T>;

  protected onSuccess() {}
  protected onError() {}
}

Code for Form Directive (works well when user clicks Submit button, which triggers onSubmit event in CustomComponent):

@Directive({
  selector: 'form'
})
export class FormSubmitDirective {
  submit$ = fromEvent(this.element, 'submit').pipe(shareReplay(1));

  constructor(private host: ElementRef<HTMLFormElement>) {}

  get element() {
    return this.host.nativeElement;
  }
}

I was hoping something like this could be the solution to my question, but this for sure doesn’t work.

invalidOnError$ = fromEvent(this.element, 'error').pipe(shareReplay(1));

The idea is to use submit$ or invalidOnError$ from the directive to focus on the first invalid field in the form. Works fine for submit$, but not invalidOnError$. Appreciate some help – still fairly new to Angular.

One Answer

I got this to work in a round about manner, by using the @Input decorator in another form directive which also imports submit$ from Form Directive.

No changes to code for FormComponent and Form Directive vs. what's shown in the question.

Relevant code from Custom component:

export class CustomComponent extends FormComponent<Custom> {
  invalidOnError: string = '';
  form: FormGroup;

  protected buildForm(): FormGroup {
    return this.form = this.fb.group({
      fieldA: ['', Validators.required],
      fieldB: ['', Validators.required],
      fieldC: [''],
      fieldD: ['', [Validators.required, Validators.pattern('[0-9]{10}')]]
    }

  protected doSubmit(): Observable<Custom> {
    invalidOnError = '';
    return this.httpService.callDatabase<Custom>('post', '/api/users/custom', this.value);
  };

  protected get value(): Registration {
    return {
      fieldA: this.fieldA.value,
      fieldB: this.fieldB.value,
      fieldC: this.fieldC.value,
      fieldD: this.fieldD.value
    };
  }

  get fieldA() { return this.form.get('fieldA'); }
  get fieldB() { return this.form.get('fieldB'); }
  get fieldC() { return this.form.get('fieldC'); }
  get fieldD() { return this.form.get('fieldD'); }

  protected onError() {
    if (this.error.length) {//error.length indicates some of the fields in the form are already registered in the database
      invalidOnError = 'invalid'
      Object.keys(this.error).forEach(element => {
        let formControl = this.form.get(this.error[element])
        this.currentVal = formControl.value;
        formControl.setValidators(formControl.validator ? [formControl.validator, unique(this.currentVal)] : unique(this.currentVal))
        formControl.updateValueAndValidity()
        this.invalidOnError.emit('error');
      })
    }
  }

Relevant code from CustomComponentTemplate:

<form class="bg-light border" appFocus="FieldA" [formGroup]="CustomForm" 
[invalidOnError]="invalidOnError" (ngSubmit)="onSubmit()">

Relevant code from invalidFormControlDirective (imports submit$ from Form Directive):

@Directive({
  selector: 'form[formGroup]'
})
export class FormInvalidControlDirective {
  private form: FormGroup;
  private submit$: Observable<Event>;
  @Input() invalidOnError: string = ''; //this is the @Input variable invalidOnError

  constructor(
    @Host() private formSubmit: FormDirective,
    @Host() private formGroup: FormGroupDirective,
    @Self() private el: ElementRef<HTMLFormElement>
  ) {
    this.submit$ = this.formSubmit.submit$;
  }

  ngOnInit() {
    this.form = this.formGroup.form;
    this.submit$.pipe(untilDestroyed(this)).subscribe(() => {
      if (this.form.invalid) {
        const invalidName = this.findInvalidControlsRecursive(this.form)[0];
        this.getFormElementByControlName(invalidName).focus();
      }
    });
  }

  ngOnChanges(){
    of(this.invalidOnError).pipe(filter(val => val == 'invalid')).subscribe(() => {
        if (this.form.invalid) {
          const invalidName = this.findInvalidControlsRecursive(this.form)[0];
          this.getFormElementByControlName(invalidName).focus();
        }
      });
  }

  ngOnDestroy() { }

  // findInvalidControlsRecursive and getFormElementByControlName defined functions to get invalid controls references
}

That said, I'd be interested in 1) somehow bringing code under onChanges lifecyle into ngOnInit lifecyle in invalidFormControlDirective (couldn't get that to work), and 2) find out if there is some way to emitting an event and processing it with Rxjs fromEventPattern as opposed to passing the @Input variable invalidOnError into invalidFormControlDirective.

Answered by coder101 on November 7, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP