Przed przeczytaniem artykułu, polecam zapoznać się podstawami Reactive Forms z poprzednich artykułów.
W tym artykule przedstawię, jak tworzyć własne kontrolki (form Control). Oprócz kontrolek, które Angular obsługuje od ręki, tzn:
- selecty, inputy, textarea, radio buttony, checkboxy
posiadamy również możliwość stworzenia własnych, unikalnych kontrolek. Obecnie istnieje ich sporo na rynku, dużą popularnością cieszą się date-pickery i multiselecty. Pokażę jak stworzyć własny Custom Form Control – przygotujemy rozbudowany dropdown.
Poniżej link do przykładu, który omówię:
Przygotowujemy komponent
Standardowy select składa się z opcji, które zawierają tylko nazwy i posiada pojedynczą wartość dla danej opcji. Natomiast przykład, który rozpatrzymy, dotyczy dropdowna, który zawiera wiele informacji, a wartością dla danej opcji, jest pełny obiekt. Wygląda to następująco (wybaczcie design ; )):
Skoro już wiemy co chcemy osiągnąć, to czas rozpocząć pracę od przygotowania templatki i klasy komponentu.
Klasa:
@Component({
selector: 'product-select',
templateUrl: './product-select.component.html',
styleUrls: ['./product-select.component.css']
})
export class ProductSelectComponent {
@Input() products : any[];
isListVisible : boolean = false;
value : any;
toggleList() : void {
this.isListVisible = !this.isListVisible;
}
selectProduct(product) : void {
this.value = product;
}
}
Bez magii:
- Input() dla tablicy produktów
- boolean isListVisible, który posłuży nam do pokazania listy po kliknięciu buttona
- i metoda selectProduct(), która przypisze wybrany produkt do pola value
Templatka:
<div class="select-product-container">
<button(click)="toggleList()">Choose product</button>
<ul [ngClass]="{'show-list': isListVisible}">
<li *ngFor="let product of products" (click)="selectProduct(product)">
<span>NAME: {{ product.name }}</span>
<span>PRICE: {{ product.price }}</span>
<span>STOCK: {{ product.stock }}</span>
</li>
</ul>
</div>
Powyższy kod również powinien być dla Ciebie jasny. Puściliśmy *ngFor po tablicy produktów, aby wygenerować je w liście, oraz na każdy element listy przypisaliśmy na clicka metodę, która wybiera dany produkt.
Na ten moment mamy prosty, głupiutki komponent. Próba użycia komponentu jako FormControla w postaci:
<select-product formControlName="product"></select-product> // reactive forms
<select-product ngModel></select-product> // template driven forms
zakończy się niepowodzeniem. No to czas nauczyć nasz komponent rozmawiać z formularzami.
ControlValueAccessor
ControlValueAccessor możesz traktować jako klej pomiędzy elementem DOM (nasz komponent) a modelem reprezentującym formularz (np. modelem stworzonym przez FormBuildera). Po bardziej „programistycznemu” jest to interfejs:
interface ControlValueAccessor {
writeValue(obj: any) : void
registerOnChange(fn: any) : void
registerOnTouched(fn: any) : void
setDisabledState(isDisabled: boolean) : void
}
ControlValueAccessor – link do dokumentacji
Jeśli spojrzysz do API Angulara, to zobaczysz, że istnieje parę typów ValueAccesor:
- DefaultValueAccessor – dla inputa i textarea
- CheckboxControlValueAccessor / RadioControlValueAccessor– dla checkboxa i radio buttona
- SelectControlValueAccessor / SelectMultipleControlValueAccessor– dla selectów
Zatem każdy ValueAccessor mówi danemu elementowi, jak zapisywać wartość i jak nasłuchiwać na zmiany.
——
Do naszego custom controla wykorzystamy ControlValueAccessor. Posiada on następujące metody:
- writeValue(obj : any) : void – zapisuje nową wartość z modelu formularza do widoku
- registerOnChange(fn: any) : void – rejestruje listenera na Change Events, metoda używana wewnętrznie przez Angulara. Nie powinniśmy jej usuwać, bez tej metody próba użycia control.valueChanges.subscribe(…) w Reactive Forms, w celu nasłuchiwania na zmiany wartości, zakończony się niepowodzeniem
- registerOnTouched(fn: any) : void – działa na podobnej zasadzie co registerOnChange, z tą różnicą, że na słuchuje na Touch Events.
- setDisabledState(isDisabled: boolean) : void – funkcja jest wywołana, jeśli kontrolka zmienia stan z enabled na disabled lub odwrotnie (control.enable() i control.disable()). Możemy przechwycić wartość isDisabled z parametru i użyć jej na dyrektywie [disabled] w celu zablokowania elementu w DOM, który może posiadać taki atrybut.
Skoro już znamy szczegóły, czas dodać nasz interfejs.
Implementujemy interfejs ControlValueAccessor
Wracamy do klasy:
@Component({
...
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ProductSelectComponent), multi: true }
]
})
export class ProductSelectComponent implements ControlValueAccessor {
...
value : any;
getCssForSelectedProduct(product) {
return {
selected: this.value && this.value.name === product.name
}
}
writeValue(value) {
if (value) {
this.value = value;
}
}
registerOnChange(fn) : void {
this.propagateChange = fn;
}
registerOnTouched() : void {}
setDisabledState(isDisabled: boolean) : void {}
}
Co się wydarzyło:
- do dekoratora @Component dodaliśmy nowy provider, pod tokenem NG_VALUE_ACCESSOR, rejestrujemy nasz nowy ValueAccessor, którym jest nasz komponent. Czym jest dokładnie useExisiting oraz forwardRef tłumaczyłem w artykule o własnych walidatorach: Angular : Custom validators
- zaimplementowaliśmy interfejs ControlValueAccessor
- dodaliśmy metody z interfejsu
- pod polem value, trzymana jest aktualna wartość kontrolki (w naszym przypadku będzie to obiekt, reprezentujący produkt z listy)
- dodaliśmy metodę getCssForSelectedProduct(product), która doda nam odpowiednią klasę CSS dla wybranego elementu z listy
W tej chwili nasz Custom Form Control potrafi już się porozumieć z formularzami. Zwróć uwagę, że nigdzie nie wywołujemy metod z interfejsu. Metoda writeValue(value) sama wie, że jej parametr value to jest wartość przekazana z modelu formularza (stworzonego np. przez FormBuildera). Teraz możemy już użyć naszego selecta w obu typach formularzy:
<select-product [prodcuts]="products" formControlName="product"></select-product> // reactive forms
<select-product [products]="products" name="product" ngModel></select-product> // template driven forms
Link do omówionego przykładu:
LIVE EXAMPLE
Podsumowanie
Generalnie genialna sprawa. Możemy stworzyć dowolony komponent, który będzie potrafił przekazać swoją wartość do modelu reprezentującego formularz.Wartością może być cokolwiek, np. skomplikowany obiekt.