SubformControlValueAccessor
SubformControlValueAccessor is ControlValueAccessor interface implementation project for Angular Framework. Current project contains realized 2 abstract classes: ControlValueAccessorComponent for simple custom controls and FormControlValueAccessorComponent for subforms
Install
You can run this command in your terminal
$ npm install subform-control-value-accessor
Usage
ControlValueAccessorComponent
ControlValueAccessorComponent is abstract class for simple (not subform) custom form controls. This implementation support not only ControlValueAccessor interface functions but error managment too
Properties
| Property | Decorator, Modificator | Type | Default Value | Example | Description |
|---|---|---|---|---|---|
| errorMessages | @Input() public | [key: string]: string | ((control: AbstractControl) => string) | {} | { 'required': 'Field is required', 'minlength': (control: AbstractControl) => 'Min length is 4 characters' } | Error messages object. ControlValueAccessorComponent support displaying errors managment. Current realization check all active errors and sort all messages to showing it in component html. key is error message |
| formControlName | @Input() public | string | null | null | "login" | This property is used in form for getting correct control object |
| disabled | @Input() public | boolean | false | false | This property is part of ControlValueAccessor implementation. Developer should realize this functionallity in custom control html for correct realization, but this property doesn't affect the basic functionality |
| value | @Input() public set/get | T | null | This property is PUBLIC getters and setter for linked data. Setter sets value to data property and triggers onChange event | ||
| isValid | public get | booleen | true | Returns valid status of control. Works only inside form | |
| activeErrorMessages | public get | string[] | ['Field is required', 'Min length is 4 characters'] | Returns array of messages for active errors | |
| control | protected | AbstractControl | null | null | Internal property. This is set in the ngOnInit method if custom control is used in form. Property is used in the error messages manangment | |
| data | protected abstract set/get | T | null | This property is INTERNAL getters and setter for linked data. Setter doesn't trigger onChange event | ||
| onChangeCallback | protected | (_: T | null) => void | () => {} | This property is part of ControlValueAccessor implementation. This is set in registerOnChange method. Calling this method trigger onChange event | |
| onTouchedCallback | protected | () => void | () => {} | This property is part of ControlValueAccessor implementation. This is set in registerOnTouched method. Calling of this method trigger blur event |
Methods
| Method | Modificator | Arguments | Return type | Description |
|---|---|---|---|---|
| ngOnInit | public | void | OnInit interface implementation. This method set control object to control property if current control inside form | |
| writeValue | public | value: T (generic) | void | ControlValueAccessor interface implementation. This method set value to data setter |
| registerOnChange | public | callback: (_: T | null) => void | void | ControlValueAccessor interface implementation. This method set callback to onChangeCallback property |
| registerOnTouched | public | callback: () => void | void | ControlValueAccessor interface implementation. This method set callback to onTouchedCallback property |
| setDisabledState | public | isDisabled: boolean | void | ControlValueAccessor interface implementation. This method set isDisabled to disabled property |
| getErrorsMessages | protected | string[] | This method filters error messages from errorMessages property by active control errors and returns this array |
Creating simple custom control
Module
Add FormModule to your module for ngModel
import { FormsModule } from '@angular/forms'; // add FormsModule for ngModel
@NgModule({
declarations: [
CustomComponent
],
imports: [
CommonModule,
FormsModule // add FormsModule for ngModel
],
exports: [
CustomComponent
]
})
export class CustomModule { }
Component class
Extend your class component from ControlValueAccessorComponent and set your control data type
export class CustomComponent extends ControlValueAccessorComponent<string>
Provide ControlContainer object to constructor
constructor(@Optional() @Host() @SkipSelf() controlContainer: ControlContainer){
super(controlContainer);
}
Implement abstract property data. ControlValueAccessorComponent has 2 getters and setters for linked data: "value" and "data" "data" is internal property without "onChange" event. This property is used by "writeValue" method ("ControlValueAccessor" interface) "value" is public property with "onChange" event. "value" === "data" + "onChange" event
private _data: string | null = null;
protected get data(): string | null { // data: T | null
return this._data;
}
protected set data(value : string | null){ // data: T | null
this._data = value;
}
Set provider
const CUSTOM_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomComponent),
multi: true
};
@Component({
selector: 'custom-component',
templateUrl: './custom.component.html',
providers: [CUSTOM_VALUE_ACCESSOR] //set provider
})
Full component code. You can find this code in examples.
import { Component, Host, Optional, SkipSelf, forwardRef } from '@angular/core';
import { ControlContainer, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessorComponent } from 'subform-control-value-accessor'; //import abstract class
//create provider for ControlValueAccessor
const CUSTOM_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomComponent),
multi: true
};
@Component({
selector: 'custom-component',
templateUrl: './custom.component.html',
providers: [CUSTOM_VALUE_ACCESSOR] //set provider
})
export class CustomComponent extends ControlValueAccessorComponent<string> { // extend class with ControlValueAccessorComponent<T>.
//ControlValueAccessorComponent<T> is generic class. Set your parameter type.
/**
* ControlValueAccessorComponent<T> uses ControlContainer parameter in constructor
*/
constructor(@Optional() @Host() @SkipSelf() controlContainer: ControlContainer){
super(controlContainer);
}
/**
* implement abstraction properties
*
* ControlValueAccessorComponent<T> has 2 getters and setters for linked data: "value" and "data"
* "data" is internal property without "onChange" event. This property is used by "writeValue" method ("ControlValueAccessor" interface)
* "value" is public property with "onChange" event.
* "value" === "data" + "onChange" event
*/
private _data: string | null = null;
protected get data(): string | null { //data: T | null
return this._data;
}
protected set data(value : string | null){ //data: T | null
this._data = value;
}
}
Component html
Create component template and link ngModel to PUBLIC "value" property
<input [(ngModel)]="value">
<p *ngFor="let error of activeErrorMessages">{{ error }}</p>
Using custom control in parent component with ngModel without form
Component class
Set data property for ngModel linking
export class ParentComponent {
public data: string = 'non-form data';
}
Component html
Create component template and link data property to ngModel
<custom-component [(ngModel)]="data"></custom-component>
<p>{{ data }}</p>
Using custom control in parent component with form
Component class
Set and initalize form property and object with custom error messages
export class ParentComponent {
constructor(private formBuilder: FormBuilder){}
public form!: FormGroup;
public errorMessages = {
'required': 'Field is required',
'minlength': (control: AbstractControl) => 'Min length is 4 characters'
};
public ngOnInit(): void {
this.form = this.formBuilder.group({
custom: ['custom control value', [Validators.required, Validators.minLength(4)]]
});
}
}
Component html
Create component template and link control to form
<form [formGroup]="form">
<label for="custom">Custom Control</label>
<custom-component
formControlName="custom"
name="custom"
style="display: block;"
[errorMessages]="errorMessages">
</custom-component>
</form>
<p>Form valid: {{ form.valid }}</p>
<p>Form value: {{ form.value | json }}</p>
FormControlValueAccessorComponent
FormControlValueAccessorComponent is abstract class for custom subform controls. This class extends ControlValueAccessorComponent and adds new functionality for subform controls.
Properties
Only new or overided properties are described here
| Property | Decorator, Modificator | Type | Default Value | Example | Description |
|---|---|---|---|---|---|
| componentSubscriptions | protected | Subscription | Object contains all component subscriptions | ||
| form | public abstract get | FormGroup | Component form getter | ||
| data | protected set/get | T | null | ControlValueAccessorComponent data property implementation. Setter calls patchForm method. Getter returns form.value |
| Method | Modificator | Arguments | Return type | Description |
|---|---|---|---|---|
| ngOnInit | public override | void | This method extends ControlValueAccessorComponent ngOnInit method. Calls initForm and addSubscriptions methods | |
| ngOnDestroy | public | void | OnDestroy interface implementation. This method calls removeSubscriptions method | |
| ngAfterViewInit | public | void | AfterViewInit interface implementation. This method realize parent form value update hook for control default values | |
| validate | public | control?: AbstractControl | ValidationErrors | null | Validator interface implementation. This method returns form error to parent form when component has internal validation errors |
| initForm | protected abstract | value: T | null | void | Abstract form initialization method |
| patchForm | protected abstract | value: T | null | void | Abstract form patching method |
| onFormValueChangedHandler | protected | value: T | null | void | form.valueChanges event handler. Triggers component onChangeCallback event |
| addSubscriptions | protected | void | Method adds component subscriptions to componentSubscriptions property | |
| removeSubscriptions | protected | void | Method removes component subscriptions from componentSubscriptions property |
Creating simple custom form control
Module
Add ReactiveFormsModule to your module
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
CustomFormComponent
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule
]
})
export class CustomFormModule { }
Component class
Extend your class component from FormControlValueAccessorComponent and set your control data type
export class CustomFormComponent extends FormControlValueAccessorComponent<FormModel>
Provide ControlContainer and FormBuilder objects to constructor
constructor(
@Optional() @Host() @SkipSelf() controlContainer: ControlContainer,
private formBuilder: FormBuilder) {
super(controlContainer);
}
Implement form abstract getter
private _form!: FormGroup;
public get form(): FormGroup {
return this._form;
}
Implement initForm and patchForm methods
protected initForm(): void {
this._form = this.formBuilder.group({
firstName: ['John', [Validators.required]], //default form data
lastName: ['Doe', [Validators.required]] //default form data
});
}
protected patchForm(value: FormModel | null): void {
value && this.form.patchValue(value);
}
Create providers for NG_VALUE_ACCESSOR and NG_VALIDATORS and your component
const CUSTOM_FORM_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomFormComponent),
multi: true
};
const CUSTOM_FORM_VALIDATORS = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CustomFormComponent),
multi: true
};
@Component({
selector: 'custom-form',
templateUrl: './custom-form.component.html',
providers: [ CUSTOM_FORM_VALUE_ACCESSOR, CUSTOM_FORM_VALIDATORS ] // Add providers to component decorator
})
Full component code. You can find this code in examples.
import { Component, Host, Optional, SkipSelf, forwardRef } from '@angular/core';
import { ControlContainer, FormBuilder, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { FormControlValueAccessorComponent } from 'subform-control-value-accessor';
/**
* Component data structure
*/
interface FormModel {
firstName: string;
lastName: string
}
/**
* NG_VALUE_ACCESSOR provider for custom control
*/
const CUSTOM_FORM_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomFormComponent),
multi: true
};
/**
* NG_VALIDATORS provider for custom control
*/
const CUSTOM_FORM_VALIDATORS = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CustomFormComponent),
multi: true
};
@Component({
selector: 'custom-form',
templateUrl: './custom-form.component.html',
providers: [ CUSTOM_FORM_VALUE_ACCESSOR, CUSTOM_FORM_VALIDATORS ] // Add providers to component decorator
})
export class CustomFormComponent extends FormControlValueAccessorComponent<FormModel> { // extend FormControlValueAccessorComponent and set your data type
private _form!: FormGroup;
constructor(
@Optional() @Host() @SkipSelf() controlContainer: ControlContainer,
private formBuilder: FormBuilder) {
super(controlContainer);
}
/**
* Implememt form getter
*/
public get form(): FormGroup {
return this._form;
}
/**
* Implement form initialization method
*/
protected initForm(): void {
this._form = this.formBuilder.group({
firstName: ['John', [Validators.required]], //default form data
lastName: ['Doe', [Validators.required]] //default form data
});
}
/**
* Implement for patching method
*/
protected patchForm(value: FormModel | null): void {
value && this.form.patchValue(value);
}
}
Component html
Create component template and link formGroup to "form" property
<form [formGroup]="form">
<label for="firstName">First Name</label>
<input formControlName="firstName" name="default" style="display: block;"/>
<ng-container *ngIf="this.form.controls['firstName'].invalid">
<p style="color:red" *ngIf="this.form.controls['firstName'].errors?.['required']">Field is required</p>
</ng-container>
<label for="lastName">Last Name</label>
<input formControlName="lastName" name="default" style="display: block;"/>
<ng-container *ngIf="this.form.controls['lastName'].invalid">
<p style="color:red" *ngIf="this.form.controls['lastName'].errors?.['required']">Field is required</p>
</ng-container>
</form>
Using custom control in parent component
Parent component class
Create and initialize FormGroup object
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'parent',
templateUrl: './parent.component.html'
})
export class ParentComponent implements OnInit {
constructor(private formBuilder: FormBuilder){}
public form!: FormGroup;
/**
* Initialize form. You can set default value to custom component. If you set null then component uses component default value
*/
public ngOnInit(): void {
this.form = this.formBuilder.group({
custom: [{firstName: 'Jane', lastName: 'Doe'}],
});
}
}
Parent component html
Create form template and provide custom component as form control
<form [formGroup]="form">
<custom-form formControlName="custom"></custom-form>
</form>
<p>Form Valid: {{ form.valid }}</p>
<p>Form Value: {{ form.value | json }}</p>
Summary
All examples you can find in git project page Good luck!