1.0.0 • Published 11 months ago

subform-control-value-accessor v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

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

Methods

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

Methods

Only new or overided methods are described here

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!