npm.io
1.0.0 • Published 3 years ago

subform-control-value-accessor

Licence
MIT
Version
1.0.0
Deps
1
Size
81 kB
Vulns
0
Weekly
0

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
Methods

Only new or overided methods are described here

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!

Keywords