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:
1 2 3 4 5 |
@Component({ selector: 'product-select', templateUrl: './product-select.component.html', styleUrls: ['./product-select.component.css'] }) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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:
1 2 3 4 5 6 7 8 9 10 |
<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:
1 2 |
<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:
1 2 3 4 5 6 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@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:
1 |
<select-product [prodcuts]="products" formControlName="product"></select-product> // reactive forms |
1 |
<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.
Siemka,
W przykładach kodu zapomniałeś o
propagateChange : any = () => {};
,W Live Example już jest
Czy z klasy implementującej Customvalueaccessor da się łatwo dostać do AbstractControl / FormControl lub FormGroup żeby zresetować value?
Przykład przestał działać. 404 na resource’ach
Jak się ma sprawa z walidacją?