Search code examples
angulartypescriptangular-reactive-formsform-control

Angular get Input Elements of Form, native & custom formcontrol


Preface: I used this posts answer Change behaviour of enter key in a phone - Angular 5 to creat my own Enter-To-Tab directive, which works without using Ids (as angular deletes Ids on matInput elements)

@Directive({
  selector: '[appEnterTab]'
})
export class EnterTabDirective implements AfterContentInit{

  @ContentChildren(MatFormFieldControl, {descendants: true, read: ElementRef}) controls: QueryList<ElementRef>;

  constructor(private renderer: Renderer2, private el: ElementRef) {
  }
  ngAfterContentInit(): void {
    this.controls.changes.subscribe(controls => {
      this.createKeydownEnter(controls);
    })
    if (this.controls.length) {
      this.createKeydownEnter(this.controls);
    }
  }
  private createKeydownEnter(querycontrols: QueryList<any>) {
    querycontrols.forEach((c: ElementRef) => {
      this.renderer.listen(c.nativeElement, 'keydown.enter', (event) => {
        if (this.controls.last !== c) {
          let controls = querycontrols.toArray();
          let index = controls.findIndex(d => d === c);
          if (index >= 0) {
            let nextControl: ElementRef = controls.find((n, i) => n && i > index && !n.nativeElement.disabled)
            if (nextControl) {
              event.preventDefault();
                nextControl.nativeElement.select();
            }
          }
        }
      })
    })

  }


}

This works fine for forms with nativ matInput fields such as

<form [formGroup]="aGroup" appEnterTab>
<input formControlName="someInput" matInput/>
<input formControlName="someOtherInput" matInput/>
</form>

but when using a Custom formControl such as

@Component({
  selector: 'app-employee-id-input',
  templateUrl: './employee-id-input.component.html',
  styleUrls: ['./employee-id-input.component.scss'],
  providers: [
    { provide: MatFormFieldControl, useExisting: ArticleInformationComponent },
  ],
})
export class EmployeeIdInputComponent implements ControlValueAccessor,
MatFormFieldControl<string>,
OnDestroy {
  public static nextId = 0;

  private _disabled = false;
  private _required = false;
  private _placeholder: string;

  public form: FormGroup<EmployeeIdForm> = this.formbuilder.group({
    EmployeeId: new FormControl('', { validators: [Validators.required] }),
  });

  public stateChanges: Subject<void> = new Subject();
  public focused: boolean = false;
  public touched: boolean = false;
  public controlType?: string | undefined = 'app-employee-id-input';
  public id: string = `app-employee-id-input${EmployeeIdInputComponent.nextId++}`;
  public onChange: any = () => { };
  public onTouched: any = () => { };
  public shouldLabelFloat: boolean;

  constructor(private formbuilder: FormBuilder,
    private personnelService: PersonnelService,
    private _elementRef: ElementRef<HTMLElement>,
    @Optional() @Self() public ngControl: NgControl,
    @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField) {
      if (this.ngControl != null) {
        this.ngControl.valueAccessor = this;
      } 
    }
    public get empty() {
      return !this.form.value;
    }
  
    // eslint-disable-next-line @angular-eslint/no-input-rename
    @Input('aria-describedby') userAriaDescribedBy: string;
  
    @Input()
    get placeholder(): string {
      return this._placeholder;
    }
    set placeholder(value: string) {
      this._placeholder = value;
      this.stateChanges.next();
    }
  
    @Input()
    get required(): boolean {
      return this._required;
    }
    set required(value: BooleanInput) {
      this._required = coerceBooleanProperty(value);
      this.stateChanges.next();
    }
  
    @Input()
    get disabled(): boolean {
      return this._disabled;
    }
    set disabled(value: BooleanInput) {
      this._disabled = coerceBooleanProperty(value);
      this._disabled ? this.form.disable() : this.form.enable();
      this.stateChanges.next();
    }
  
    @Input()
    get value(): string | null {
      if (this.form.valid) {
        return this.form.controls.EmployeeId.value;
      }
      return null;
    }
    set value(employeeId: string | null) {
      this.form.controls.EmployeeId.setValue(employeeId);
      this.stateChanges.next();
    }
    get errorState(): boolean {
      return this.form.invalid && this.touched;
    }
  
    ngOnDestroy(): void {
      this.stateChanges.complete();
    }
  
    onFocusIn(event: FocusEvent) {
      if (!this.focused) {
        this.focused = true;
        this.stateChanges.next();
      }
    }
  
    onFocusOut(event: FocusEvent) {
      if (!this._elementRef.nativeElement.contains(event.relatedTarget as Element)) {
        this.touched = true;
        this.focused = false;
        this.onTouched();
        this.stateChanges.next();
      }
    }
  
    setDescribedByIds(ids: string[]) {
      const controlElement = this._elementRef.nativeElement.querySelector(
        '.example-tel-input-container',
      )!;
      controlElement.setAttribute('aria-describedby', ids.join(' '));
    }
  
  
    onContainerClick(event: MouseEvent): void {
      this.onTouched()
    }
  
  writeValue(employeeId: string): void {
    this.form.controls.EmployeeId.setValue(employeeId);
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
  public change(value: Event)
  {
    this.form.controls.EmployeeId.setValue((value.target as HTMLInputElement).value);
    this.onChange(value)
  }
}
<mat-form-field [formGroup]="form">
    <mat-label dataTid="Personalnummer">Personalnummer</mat-label>
    <input formControlName="EmployeeId" matInput (change)="change($event)"(focusin)="onFocusIn($event)" 
    (focusout)="onFocusOut($event)" (input)="onChange(form.controls.EmployeeId.value)"/>    
</mat-form-field>

and using it like this

<form [formGroup]="aGroup" appEnterTab>
<input formControlName="someInput" matInput/>
<app-employee-id-input formControlName="someOtherInput"> </app-employee-id-input>
</form>

I get an error, that "Select()" is not a function, when trying to call it on the nativeElement of my custom control. Which makes sense, since the nativeElement is the whole control, not the input field.

Does anyone know how to refactor my enter-tab directive so that it works on both custom Controls and nativ input fields?


Solution

  • I ended up editing the diretive as follows:

    private createKeydownEnter(querycontrols: QueryList<any>) {
     [...]
      event.preventDefault();
      if (nextControl.nativeElement.select) {
        let element: HTMLInputElement = nextControl.nativeElement;
        this.focus(element);
      } else { //If element does not have a "select" method, aka is a custom CVA
        let element: HTMLInputElement =
          nextControl.nativeElement.querySelector('input'); //Search for first Input in the control
        if (element) {
          this.focus(element);
        } 
    
    }