Angular - Create a dynamic form input using attribute selector for components

May 17, 2023

I recently read an article about using components in Angular as the attribute selector. That is an interesting idea about how flexible the component can be with its selector.

👉 Here is the article; you can find out more about it first.

This approach allows for styling the container without needing a custom Angular tag. Instead, the component's template is rendered within the native tag that the component is used on.

Thanks to this great idea, we are going to create a dynamic form input that is highly customizable and easy-to-use.

Here is the source code.

Result expectation

Expect that this is how order components will use the FormFieldComponent .

<form [formGroup]="form"> <section app-form-field [label]="{ value: 'Name', markAsRequired: true }" formControlName="name"> </section> <form/>

Let's jump into it!

Customise form control

The reactive form in Angular is a powerful feature that allows us to create complex forms with a variety of supportive interfaces. In this article, we are going to customise a form control by inheriting the ControlValueAccessor interface.

For reuse purposes, we first create a standalone CustomFormControlBaseComponent looking like this:

@Component({ standalone: true, imports: [FormsModule, CommonModule], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FormFieldComponent), multi: true, }, ], template: '', }) export abstract class CustomFormControlBase implements ControlValueAccessor { protected _value: any; onChange: (_: any) => {}; onTouched: (_: any) => {}; writeValue(value: any) { this._value = value; } registerOnChange(fn: (_: any) => {}) { this.onChange = fn; } registerOnTouched(fn: (_: any) => {}) { this.onTouched = fn; } }

In order to refer to the component while it is not yet defined, we provide the NG_VALUE_TOKEN with the forwardRef function. Then we implement the required ControlValueAccessor methods. For the base component, we do not need to layout the template as well.

Dynamic form input used as attribute selector

We create the FormFieldComponent providing the reference to NG_VALUE_TOKEN and extending the above base component. The FormFieldComponent also has its own inputs, such as label and type.

@Component({ standalone: true, imports: [FormsModule, CommonModule], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FormFieldComponent), multi: true, }, ], selector: '[app-form-field]', templateUrl: './form-field.component.html', styleUrls: ['./form-field.component.css'], }) export class FormFieldComponent extends CustomFormControlBase { @Input() label: | string | { value: string; markAsRequired?: boolean; hidden?: boolean } = ''; @Input() type: 'checkbox' | 'text' | 'number' | 'textarea' = 'text'; get value(): any { return this._value; } getLabel() { return typeof this.label === 'string' ? this.label : this.label.value; } }

Here, to invoke the component as an attribute selector, we define the selector of the component by having its name within square brackets.

In the template of the component, just define some common input types that are attached to its events with methods of the ControlValueAccessor interface.

<label *ngIf="!$any(label).hidden" >{{ getLabel() }} <span *ngIf="$any(label).markAsRequired">*</span></label > <ng-container [ngSwitch]="type"> <ng-container *ngSwitchCase="'textarea'"> <textarea class=" border border-gray-300 rounded-md shadow-sm active:border-gray-300 " [value]="value" (input)="onChange($any($event.target).value)" (blur)="onTouched($any($event.target).value)" (keyup)="writeValue($any($event.target).value)" ></textarea> </ng-container> <input *ngSwitchCase="'checkbox'" class="border border-gray-300 rounded-md shadow-sm active:border-gray-300" type="checkbox" [checked]="value" (input)="onChange($any($event.target).checked)" (blur)="onTouched($any($event.target).checked)" (change)="writeValue($any($event.target).checked)" /> <input *ngSwitchDefault class="border border-gray-300 rounded-md shadow-sm active:border-gray-300" [type]="type" [value]="value" (input)="onChange($any($event.target).value)" (blur)="onTouched($any($event.target).value)" (keyup)="writeValue($any($event.target).value)" /> </ng-container>

Allow customising input with a dynamic template

Next, we are going to make the component more customizable by adding a dynamic input template as the TemplateRef passed through an input.

Let’s start by adding an @Input for the custom input

export class FormFieldComponent extends CustomFormControlBase { @Input() label: | string | { value: string; markAsRequired?: boolean; hidden?: boolean } = ''; @Input() type: 'checkbox' | 'text' | 'number' | 'textarea' = 'text'; @Input() inputTemplate: TemplateRef<any>; get value(): any { return this._value; } getLabel() { return typeof this.label === 'string' ? this.label : this.label.value; } }

In addition, we use ngTemplateOutlet to render the custom input template if it is provided

<label *ngIf="!$any(label).hidden" for="" class="" >{{ getLabel() }} <span *ngIf="$any(label).markAsRequired">*</span></label > <ng-container *ngTemplateOutlet="inputTemplate ? inputTemplate : defaultInput" ></ng-container> <ng-template #defaultInput ><ng-container [ngSwitch]="type"> <ng-container *ngSwitchCase="'textarea'"> <textarea class=" border border-gray-300 rounded-md shadow-sm active:border-gray-300 " [value]="value" (input)="onChange($any($event.target).value)" (blur)="onTouched($any($event.target).value)" (keyup)="writeValue($any($event.target).value)" ></textarea> </ng-container> <input *ngSwitchCase="'checkbox'" class="border border-gray-300 rounded-md shadow-sm active:border-gray-300" type="checkbox" [checked]="value" (input)="onChange($any($event.target).checked)" (blur)="onTouched($any($event.target).checked)" (change)="writeValue($any($event.target).checked)" /> <input *ngSwitchDefault class="border border-gray-300 rounded-md shadow-sm active:border-gray-300" [type]="type" [value]="value" (input)="onChange($any($event.target).value)" (blur)="onTouched($any($event.target).value)" (keyup)="writeValue($any($event.target).value)" /> </ng-container> </ng-template>

That is all, you can check out the demonstration on stackblizt here. Futhermore, you can enhance the FormFieldComponent by adding validations, hints, etc.

References