Angular Custom Form Control erstellen

Tutorial Angular

von Ardian Shala, 09.06.2018
Oft sind normale HTML Inputs für komplexere oder umfangreichere Formulare nicht ausreichend.
Mittels ControlValueAccessor und NG_VALUE_ACCESSOR lassen sich Custom Form Controls realisieren und bei Wertänderungen diese übermitteln.
Zur Demonstration wird ein simpler Zahlen Spinner realisiert, der das normale HTML5 Spinner Verhalten erweitert und individuell gestalten lässt.
Vorraussetzung:
  • Angular - link
  • Verständnis Angular Form Builder - link

Downloads:

Stackblitz Demo

Screenshot

tutorial-angular-custom-form-control-preview

Bootstrap installieren

Zur Demonstation wird das Control für das CSS Framework Twitter Bootstrap v. 4 realisiert.
Dieses lässt sich mittels foglenden Befehl installieren:

npm install --save bootstrap

Native Spinner ausblenden

Die folgende Component soll das vorhandene native HTML Input vom Type number erweitern.
Deshalb werden mittels folgender CSS Class die Spinner ausgeblendet.

.no-spinners {
  -moz-appearance:textfield;
}

.no-spinners::-webkit-outer-spin-button,
.no-spinners::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

Optional FontAwesome integrieren

Für die Buttons „Hochzählen“ und „Runterzählen“ werden nachfolgend Icons eingesetzt.
Diese werden mittels FontAwesome integriert.
Natürlich kann hier auch aus Performance-Gründen einfach ASCII Code genutzt werden.
Andernfalls kann FontAwesome via CDN im globalen style integriert werden.

@import url(https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css);

Form initialisieren

Das Tutorial setzt ein Grundverständnis des Angular FormBuilder Service vorraus.
In der AppComponent wird nun eine FormGroup mittels Angular FormBuilder erstellt.
Der FormGroup wird ein neues Control spinnerValue mit dem Initialwert 0 hinzugefügt.
Das neu zugewiesene Control wird später an das Custom Form Control gebunden.

import { Component } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  myForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.myForm = this.fb.group({
      spinnerValue: 0
    });
  }

}

Component erstellen

Im folgenden Abschnitt ist das Grundgerüst des Spinner FormControl.
Hierfür wird eine neue Component mit dem Namen spinner.component.ts erstellt.
Vordefiniert ist das Template, welches eine FormGroup beinhaltet indem ein Input und 2 Buttons angzeigt werden.
Die Buttons dienen dem Hoch- und Runterzählen des Input-Values.

Durch Implementieren des Angular Interface ControlValueAccessor müssen folgende Funkionen integriert und ausimplementiert werden:

  • writeValue
  • registerOnChange
  • registerOnTouched
import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';

@Component({
  selector: 'spinner',
  template: `
    <div class="input-group">
      <input type="number" class="form-control no-spinners" />
      <div class="input-group-append">
        <button class="btn btn-default" type="button" (click)="decrement()"><i class="fa fa-minus"></i></button>
        <button class="btn btn-default" type="button" (click)="increment()"><i class="fa fa-plus"></i></button>
      </div>
    </div>
  `
})
export class SpinnerComponent implements ControlValueAccessor {
    propagateChange: any = () => { };

    increment() { }

    decrement() { }

    writeValue(value) { }

    // required by ControlValueAccessor
    registerOnChange(fn) { this.propagateChange = fn; }
    registerOnTouched() { }
}

Wertzuweisung

Das Zuweisen des Wertes lässt sich leicht über Get Set Properties realisieren, die eine private Property herausreichen oder deren Wert überschreiben.
Die private Property lautet im weiteren Verlauf _model

_model: number = 0;

get value(): number {
    return this._model;
}

set value(value: number) {
    this._model = value;
    this.propagateChange(value); // ChangeDetection von Angular informieren
}

Wert über Buttons ändern

Im Template der Component sind bereits 2 Funktionen den Buttons zugewiesen.
Wenn einer der Buttons geklickt wird, soll der Wert der Component entsprechend abgeändert werden und später mittels formControl oder ngModel gebunden werden.

increment() {
    this.value = this.value + 1;
}

decrement() {
    this.value = this.value - 1;
}

ControlValueAccessor implementieren

Damit die Wertänderungen initial auch richtig übergeben werden, muss die Funktion writeValue minimal wie folgt implementiert werden.

changeValue(value: number) {
    this.value = value;
}

writeValue(value) {
    if (value) {
        this.value = value;
    }

    this.changeValue(this._model); // initiale Wertzuweisung
}

Kompletter Source

import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'spinner',
  template: `
    <div class="input-group">
      <input type="number" class="form-control no-spinners" [(ngModel)]="value" />
      <div class="input-group-append">
        <button class="btn btn-default" type="button" (click)="decrement()"><i class="fa fa-minus"></i></button>
        <button class="btn btn-default" type="button" (click)="increment()"><i class="fa fa-plus"></i></button>
      </div>
    </div>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SpinnerComponent),
      multi: true
    }
  ],
})
export class SpinnerComponent implements ControlValueAccessor {

  propagateChange: any = () => { };
  _model: number = 0;

  increment() {
    this.value = this.value + 1;
  }

  decrement() {
    this.value = this.value - 1;
  }

  changeValue(value: number) {
    this.value = value;
  }

  writeValue(value) {
    if (value) {
      this.value = value;
    }
    this.changeValue(+this._model); // intiale Wertzuweisung
  }

  // required by ControlValueAccessor
  registerOnChange(fn) { this.propagateChange = fn; }
  registerOnTouched() { }


  get value(): number {
    return this._model;
  }

  set value(value: number) {
    this._model = value;
    this.propagateChange(value);
  }
}

Custom Form Control benutzen

Zuvor muss die Component einem Angular Module – hier app.moduel.ts – integriert und den declarations hinzugefügt werden.
Nun kann die Component im Template integriert werden und dem zu Beginn erstellten FormControl zugewiesen werden.
Hierfür wird in app.component.html die Component wie folgt integriert:

<spinner [formControl]="myForm.get('spinnerValue')"></spinner>

Formular Änderungen überprüfen

Um zu prüfen, ob die Wertänderungen auch im Formular ankommen, kann dieses schnell ausgegeben werden:
Hierfür nimmt man den value der zu Beginn erstellten FormGroup und nutzt das JSON Pipe von Angular:

<pre>{{myForm.value | json }}</pre>

Platz für Erweiterung

Dieses Angular Custom Form Control kann nun über weitere Inputs ergänzt werden.
Eine Idee wäre das Integrieren von Wertintervallen, die beim Hoch- oder Runterzählen übersprungen werden sollen.
z.B.: 10er Schritte

#angular #controlvalueaccessor #formcontrol #Forms #spinner

Autor: Ardian Shala

Ersteller der Webseite MuchaDev. Selbstständiger IT Constultant für Frontend Technologien.