Overview
The CustomNode component allows you to embed your own Angular components directly into the dynamic form. This provides ultimate flexibility when you need functionality beyond the built-in components.
When to Use
Use the CustomNode component when you need to:
Integrate a third-party component library
Create complex custom input controls
Display dynamic content within the form
Implement specialized UI elements not covered by built-in components
Reuse existing custom components in your dynamic forms
Your custom component must be declared in the Angular module and designed to work within the form structure.
Basic Usage
import { CustomNode } from 'mat-dynamic-form' ;
import { MyCustomComponent } from './my-custom.component' ;
const customField = new CustomNode < MyCustomComponent >(
'customField' ,
MyCustomComponent
);
Common Patterns
Custom Component with Properties
import { RatingComponent } from './rating.component' ;
const ratingField = new CustomNode < RatingComponent >(
'rating' ,
RatingComponent ,
{
// Properties passed to the component
maxStars: 5 ,
currentRating: 3 ,
allowHalfStars: true ,
color: 'primary'
}
). apply ({
placeholder: 'Rate this product' ,
icon: 'star' ,
validator: Validators . required
});
Custom Component with Validation
import { ColorPickerComponent } from './color-picker.component' ;
const colorPicker = new CustomNode < ColorPickerComponent >(
'favoriteColor' ,
ColorPickerComponent ,
{
defaultColor: '#3f51b5' ,
palette: [ '#f44336' , '#4caf50' , '#2196f3' , '#ff9800' ]
}
). apply ({
placeholder: 'Select your favorite color' ,
validator: Validators . required ,
errorMessage: 'Please select a color'
});
Custom Component with Actions
import { LocationSelectorComponent } from './location-selector.component' ;
const locationSelector = new CustomNode < LocationSelectorComponent >(
'location' ,
LocationSelectorComponent ,
{
defaultZoom: 10 ,
enableGeolocation: true
}
). apply ({
placeholder: 'Select location' ,
action: {
type: 'valueChange' ,
onEvent : ( param ) => {
const location = param . event ;
console . log ( 'Location selected:' , location );
// Update related fields
}
}
});
Properties
Unique identifier for the custom component.
The Angular component class to embed.
Object containing properties to pass to the component as @Input() bindings.
Reference to the component instance after it’s created. Available after the component is rendered.
Label text for the custom component.
Material icon name to display.
validator
ValidatorFn | ValidatorFn[]
Angular validators to apply.
Custom error message shown when validation fails.
Whether the component is disabled.
Whether the component takes up a full row in the form grid.
Creating a Custom Component
Basic Custom Component Structure
import { Component , Input , Output , EventEmitter , forwardRef } from '@angular/core' ;
import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ;
@ Component ({
selector: 'app-rating' ,
template: `
<div class="rating-container">
<button
*ngFor="let star of stars; let i = index"
type="button"
(click)="setRating(i + 1)"
[disabled]="disabled">
<mat-icon>{{ i < rating ? 'star' : 'star_border' }}</mat-icon>
</button>
</div>
` ,
styles: [ `
.rating-container {
display: flex;
gap: 4px;
}
button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
` ],
providers: [
{
provide: NG_VALUE_ACCESSOR ,
useExisting: forwardRef (() => RatingComponent ),
multi: true
}
]
})
export class RatingComponent implements ControlValueAccessor {
@ Input () maxStars : number = 5 ;
@ Input () allowHalfStars : boolean = false ;
@ Input () color : string = 'primary' ;
rating : number = 0 ;
disabled : boolean = false ;
stars : number [] = [];
private onChange : ( value : number ) => void = () => {};
private onTouched : () => void = () => {};
ngOnInit () {
this . stars = Array ( this . maxStars ). fill ( 0 ). map (( _ , i ) => i );
}
setRating ( value : number ) {
if ( ! this . disabled ) {
this . rating = value ;
this . onChange ( this . rating );
this . onTouched ();
}
}
// ControlValueAccessor implementation
writeValue ( value : number ) : void {
this . rating = value || 0 ;
}
registerOnChange ( fn : any ) : void {
this . onChange = fn ;
}
registerOnTouched ( fn : any ) : void {
this . onTouched = fn ;
}
setDisabledState ( isDisabled : boolean ) : void {
this . disabled = isDisabled ;
}
}
import { Component } from '@angular/core' ;
import { FormStructure , Input , CustomNode , Button } from 'mat-dynamic-form' ;
import { RatingComponent } from './rating.component' ;
import { Validators } from '@angular/forms' ;
@ Component ({
selector: 'app-review-form' ,
template: '<mat-dynamic-form [structure]="formStructure"></mat-dynamic-form>'
})
export class ReviewFormComponent {
formStructure : FormStructure ;
constructor () {
this . formStructure = new FormStructure ( 'Product Review' );
this . formStructure . appearance = 'outline' ;
this . formStructure . nodeGrid = 2 ;
this . formStructure . nodes = [
new Input ( 'productName' , 'Product Name' ). apply ({
icon: 'shopping_cart' ,
validator: Validators . required ,
singleLine: true
}),
// Custom rating component
new CustomNode < RatingComponent >(
'rating' ,
RatingComponent ,
{
maxStars: 5 ,
color: 'accent' ,
allowHalfStars: false
}
). apply ({
placeholder: 'Your Rating' ,
icon: 'star' ,
validator: Validators . required ,
errorMessage: 'Please provide a rating' ,
singleLine: false
}),
new TextArea ( 'review' , 'Your Review' , '' , 500 ). apply ({
icon: 'rate_review' ,
validator: [ Validators . required , Validators . minLength ( 20 )],
singleLine: true
})
];
this . formStructure . validateActions = [
new Button ( 'submit' , 'Submit Review' , {
style: 'primary' ,
onEvent : ( param ) => this . onSubmit ( param . structure )
}). apply ({
validateForm: true ,
icon: 'send'
})
];
}
onSubmit ( structure : FormStructure ) {
const data = structure . getValue ();
console . log ( 'Review submitted:' , data );
// data.rating will contain the selected rating value
}
}
Advanced Examples
Custom Date Range Component
import { Component , forwardRef } from '@angular/core' ;
import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ;
interface DateRange {
start : Date | null ;
end : Date | null ;
}
@ Component ({
selector: 'app-date-range' ,
template: `
<div class="date-range">
<mat-form-field>
<mat-label>Start Date</mat-label>
<input
matInput
[matDatepicker]="startPicker"
[(ngModel)]="range.start"
(ngModelChange)="onRangeChange()"
[disabled]="disabled">
<mat-datepicker-toggle matSuffix [for]="startPicker"></mat-datepicker-toggle>
<mat-datepicker #startPicker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>End Date</mat-label>
<input
matInput
[matDatepicker]="endPicker"
[(ngModel)]="range.end"
(ngModelChange)="onRangeChange()"
[min]="range.start"
[disabled]="disabled">
<mat-datepicker-toggle matSuffix [for]="endPicker"></mat-datepicker-toggle>
<mat-datepicker #endPicker></mat-datepicker>
</mat-form-field>
</div>
` ,
styles: [ `
.date-range {
display: flex;
gap: 16px;
}
` ],
providers: [
{
provide: NG_VALUE_ACCESSOR ,
useExisting: forwardRef (() => DateRangeComponent ),
multi: true
}
]
})
export class DateRangeComponent implements ControlValueAccessor {
range : DateRange = { start: null , end: null };
disabled : boolean = false ;
private onChange : ( value : DateRange ) => void = () => {};
private onTouched : () => void = () => {};
onRangeChange () {
this . onChange ( this . range );
this . onTouched ();
}
writeValue ( value : DateRange ) : void {
if ( value ) {
this . range = value ;
}
}
registerOnChange ( fn : any ) : void {
this . onChange = fn ;
}
registerOnTouched ( fn : any ) : void {
this . onTouched = fn ;
}
setDisabledState ( isDisabled : boolean ) : void {
this . disabled = isDisabled ;
}
}
Using Date Range Component
const dateRangeField = new CustomNode < DateRangeComponent >(
'projectDates' ,
DateRangeComponent
). apply ({
placeholder: 'Project Duration' ,
icon: 'date_range' ,
validator: Validators . required ,
singleLine: true
});
Accessing Component Instance
After the component is rendered, you can access its instance:
const customNode = formStructure . getNodeById < CustomNode < RatingComponent >>( 'rating' );
if ( customNode && customNode . instance ) {
// Access component properties and methods
console . log ( 'Current rating:' , customNode . instance . rating );
// Call component methods
customNode . instance . setRating ( 5 );
}
Best Practices
Implement ControlValueAccessor - Your custom component should implement ControlValueAccessor to work seamlessly with Angular forms:implements ControlValueAccessor
Declare components in module - Custom components must be declared in your Angular module:@ NgModule ({
declarations: [ RatingComponent , ColorPickerComponent ]
})
Use forwardRef for providers - Provide NG_VALUE_ACCESSOR using forwardRef:providers : [
{
provide: NG_VALUE_ACCESSOR ,
useExisting: forwardRef (() => YourComponent ),
multi: true
}
]
Pass configuration via properties - Use the properties parameter to configure your component:new CustomNode ( id , Component , {
setting1: value1 ,
setting2: value2
})
Handle disabled state - Implement setDisabledState() to properly handle form control disabled state.
Emit value changes - Call onChange() whenever the component value changes to keep the form in sync.
Common Use Cases
Rich Text Editor
import { QuillEditorComponent } from './quill-editor.component' ;
const richTextEditor = new CustomNode < QuillEditorComponent >(
'description' ,
QuillEditorComponent ,
{
modules: {
toolbar: [
[ 'bold' , 'italic' , 'underline' ],
[ 'link' , 'image' ]
]
}
}
). apply ({
placeholder: 'Product Description' ,
singleLine: true
});
Image Cropper
import { ImageCropperComponent } from './image-cropper.component' ;
const imageCropper = new CustomNode < ImageCropperComponent >(
'avatar' ,
ImageCropperComponent ,
{
aspectRatio: 1 , // Square
maxSize: 500 // 500x500px
}
). apply ({
placeholder: 'Profile Picture' ,
singleLine: true
});
Signature Pad
import { SignaturePadComponent } from './signature-pad.component' ;
const signaturePad = new CustomNode < SignaturePadComponent >(
'signature' ,
SignaturePadComponent ,
{
width: 400 ,
height: 200 ,
backgroundColor: '#ffffff'
}
). apply ({
placeholder: 'Signature' ,
validator: Validators . required ,
singleLine: true
});
CustomNode allows you to integrate any component when built-in components don’t meet your needs.
Form Structure Learn about integrating components in forms
Validators Validating custom component values
See Also