| name | @delon/form Dynamic Schema Forms |
| description | Create dynamic schema-based forms using @delon/form (SF component). Use this skill when building complex forms with validation, conditional rendering, async data loading, custom widgets, and multi-step workflows. Ensures forms follow JSON Schema standards, integrate with Angular reactive forms, support internationalization, and maintain consistent validation patterns across the application. |
| license | MIT |
@delon/form Dynamic Schema Forms Skill
This skill helps create dynamic forms using @delon/form's SF (Schema Form) component.
Core Principles
Schema-Driven Forms
- JSON Schema: Define forms declaratively with JSON Schema
- Type Safety: Full TypeScript support for schema definitions
- Validation: Built-in validation with custom validators
- Dynamic Rendering: Conditional fields based on form state
Key Features
- Automatic form generation from schema
- Custom widgets for specialized inputs
- Async data loading (dropdowns, autocomplete)
- Multi-step forms (wizards)
- Responsive grid layouts
- Internationalization support
Basic Schema Form
import { Component, signal, output } from '@angular/core';
import { SFSchema, SFComponent, SFUISchema } from '@delon/form';
import { SHARED_IMPORTS } from '@shared';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [SHARED_IMPORTS, SFComponent],
template: `
<sf
[schema]="schema"
[ui]="ui"
[formData]="initialData()"
[loading]="loading()"
(formSubmit)="handleSubmit($event)"
(formChange)="handleChange($event)"
(formError)="handleError($event)"
/>
`
})
export class UserFormComponent {
loading = signal(false);
initialData = signal<any>({});
formSubmit = output<any>();
schema: SFSchema = {
properties: {
name: {
type: 'string',
title: 'Full Name',
maxLength: 100
},
email: {
type: 'string',
title: 'Email',
format: 'email'
},
age: {
type: 'number',
title: 'Age',
minimum: 18,
maximum: 120
},
role: {
type: 'string',
title: 'Role',
enum: ['admin', 'member', 'viewer'],
default: 'member'
}
},
required: ['name', 'email', 'role']
};
ui: SFUISchema = {
'*': {
spanLabel: 6,
spanControl: 18,
grid: { span: 24 }
},
$name: {
placeholder: 'Enter full name',
widget: 'string'
},
$email: {
placeholder: 'user@example.com',
widget: 'string'
},
$age: {
widget: 'number'
},
$role: {
widget: 'select',
placeholder: 'Select role'
}
};
handleSubmit(value: any): void {
console.log('Form submitted:', value);
this.formSubmit.emit(value);
}
handleChange(value: any): void {
console.log('Form changed:', value);
}
handleError(errors: any): void {
console.error('Form errors:', errors);
}
}
Common Widgets
String Input
{
name: {
type: 'string',
title: 'Name',
ui: {
widget: 'string',
placeholder: 'Enter name',
prefix: 'User',
suffix: '@',
maxLength: 100
}
}
}
Textarea
{
description: {
type: 'string',
title: 'Description',
ui: {
widget: 'textarea',
autosize: { minRows: 3, maxRows: 6 },
placeholder: 'Enter description'
}
}
}
Number Input
{
amount: {
type: 'number',
title: 'Amount',
minimum: 0,
maximum: 1000000,
ui: {
widget: 'number',
precision: 2,
prefix: '$',
formatter: (value: number) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
}
}
Date Picker
{
birthDate: {
type: 'string',
title: 'Birth Date',
format: 'date',
ui: {
widget: 'date',
mode: 'date',
displayFormat: 'yyyy-MM-dd',
end: 'today' // Can't select future dates
}
}
}
Date Range
{
dateRange: {
type: 'string',
title: 'Date Range',
ui: {
widget: 'date',
mode: 'range',
displayFormat: 'yyyy-MM-dd'
}
}
}
Select Dropdown
{
category: {
type: 'string',
title: 'Category',
enum: [
{ label: 'Technology', value: 'tech' },
{ label: 'Business', value: 'business' },
{ label: 'Science', value: 'science' }
],
ui: {
widget: 'select',
placeholder: 'Select category',
allowClear: true,
showSearch: true
}
}
}
Multi-Select
{
tags: {
type: 'array',
title: 'Tags',
items: {
type: 'string',
enum: ['angular', 'react', 'vue', 'typescript']
},
ui: {
widget: 'select',
mode: 'multiple',
placeholder: 'Select tags'
}
}
}
Autocomplete
{
city: {
type: 'string',
title: 'City',
ui: {
widget: 'autocomplete',
asyncData: () => this.loadCities(),
debounceTime: 300,
placeholder: 'Search city'
}
}
}
private async loadCities(): Promise<any[]> {
return [
{ label: 'New York', value: 'ny' },
{ label: 'Los Angeles', value: 'la' },
{ label: 'Chicago', value: 'chi' }
];
}
Radio Buttons
{
priority: {
type: 'string',
title: 'Priority',
enum: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' }
],
default: 'medium',
ui: {
widget: 'radio',
styleType: 'button' // or 'default'
}
}
}
Checkbox
{
agree: {
type: 'boolean',
title: 'Agree to terms',
ui: {
widget: 'checkbox'
}
}
}
Switch
{
isActive: {
type: 'boolean',
title: 'Active Status',
ui: {
widget: 'switch',
checkedChildren: 'On',
unCheckedChildren: 'Off'
}
}
}
Slider
{
rating: {
type: 'number',
title: 'Rating',
minimum: 0,
maximum: 100,
ui: {
widget: 'slider',
marks: {
0: '0',
50: '50',
100: '100'
}
}
}
}
File Upload
{
avatar: {
type: 'string',
title: 'Avatar',
ui: {
widget: 'upload',
action: '/api/upload',
accept: 'image/*',
limit: 1,
listType: 'picture-card'
}
}
}
Async Data Loading
Dynamic Dropdown Options
{
assignee: {
type: 'string',
title: 'Assignee',
ui: {
widget: 'select',
asyncData: () => this.loadUsers(),
placeholder: 'Select user'
}
}
}
private async loadUsers(): Promise<any[]> {
const users = await this.userService.getUsers();
return users.map(u => ({
label: u.name,
value: u.id
}));
}
Cascading Selects
{
country: {
type: 'string',
title: 'Country',
ui: {
widget: 'select',
asyncData: () => this.loadCountries(),
change: (value: string) => this.onCountryChange(value)
}
},
city: {
type: 'string',
title: 'City',
ui: {
widget: 'select',
asyncData: () => this.loadCities(this.selectedCountry())
}
}
}
private selectedCountry = signal<string>('');
onCountryChange(value: string): void {
this.selectedCountry.set(value);
}
Conditional Fields
Show/Hide Based on Value
schema: SFSchema = {
properties: {
userType: {
type: 'string',
title: 'User Type',
enum: ['individual', 'company']
},
// Show only for companies
companyName: {
type: 'string',
title: 'Company Name',
ui: {
visibleIf: {
userType: ['company']
}
}
},
// Show only for individuals
firstName: {
type: 'string',
title: 'First Name',
ui: {
visibleIf: {
userType: ['individual']
}
}
}
}
};
Custom Validators
import { SFSchema } from '@delon/form';
schema: SFSchema = {
properties: {
password: {
type: 'string',
title: 'Password',
ui: {
type: 'password',
validator: (value: string, formProperty: any, form: any) => {
if (value.length < 8) {
return [{ keyword: 'minLength', message: 'Password must be at least 8 characters' }];
}
if (!/[A-Z]/.test(value)) {
return [{ keyword: 'uppercase', message: 'Password must contain uppercase letter' }];
}
return [];
}
}
},
confirmPassword: {
type: 'string',
title: 'Confirm Password',
ui: {
type: 'password',
validator: (value: string, formProperty: any, form: any) => {
if (value !== form.value.password) {
return [{ keyword: 'match', message: 'Passwords must match' }];
}
return [];
}
}
}
}
};
Multi-Step Forms (Wizards)
import { Component, signal } from '@angular/core';
import { SFSchema } from '@delon/form';
@Component({
selector: 'app-wizard-form',
template: `
<nz-steps [nzCurrent]="currentStep()">
<nz-step nzTitle="Basic Info" />
<nz-step nzTitle="Address" />
<nz-step nzTitle="Confirmation" />
</nz-steps>
@switch (currentStep()) {
@case (0) {
<sf [schema]="basicInfoSchema" (formSubmit)="nextStep($event)" />
}
@case (1) {
<sf [schema]="addressSchema" (formSubmit)="nextStep($event)" />
}
@case (2) {
<div class="confirmation">
<h3>Review Your Information</h3>
<pre>{{ formData() | json }}</pre>
<button nz-button nzType="primary" (click)="submit()">Submit</button>
</div>
}
}
`
})
export class WizardFormComponent {
currentStep = signal(0);
formData = signal<any>({});
basicInfoSchema: SFSchema = {
properties: {
name: { type: 'string', title: 'Name' },
email: { type: 'string', title: 'Email', format: 'email' }
},
required: ['name', 'email']
};
addressSchema: SFSchema = {
properties: {
street: { type: 'string', title: 'Street' },
city: { type: 'string', title: 'City' },
zipCode: { type: 'string', title: 'Zip Code' }
},
required: ['street', 'city']
};
nextStep(value: any): void {
this.formData.update(data => ({ ...data, ...value }));
this.currentStep.update(step => step + 1);
}
submit(): void {
console.log('Final data:', this.formData());
}
}
Grid Layout
ui: SFUISchema = {
'*': {
spanLabel: 4,
spanControl: 20,
grid: { span: 12 } // 2 columns (24 / 12 = 2)
},
$description: {
grid: { span: 24 } // Full width
}
};
Responsive Layout
ui: SFUISchema = {
'*': {
grid: {
xs: 24, // Mobile: full width
sm: 12, // Tablet: 2 columns
md: 8, // Desktop: 3 columns
lg: 6 // Large: 4 columns
}
}
};
Form Actions
@Component({
template: `
<sf [schema]="schema" [button]="button">
<ng-template sf-template="button" let-btn>
<button
nz-button
[nzType]="btn.submit ? 'primary' : 'default'"
(click)="btn.submit ? handleSubmit() : handleReset()"
>
{{ btn.submit ? 'Submit' : 'Reset' }}
</button>
</ng-template>
</sf>
`
})
export class CustomButtonFormComponent {
button = {
submit_text: 'Submit',
reset_text: 'Reset',
submit_type: 'primary' as const,
reset_type: 'default' as const
};
}
Checklist
When creating SF forms:
- Use proper JSON Schema types
- Define required fields
- Set validation rules (min/max, format, pattern)
- Use appropriate widgets for data types
- Handle async data loading
- Implement conditional field visibility
- Add custom validators when needed
- Configure responsive grid layout
- Handle form submission and errors
- Provide loading states
- Test form validation
- Ensure accessibility (labels, ARIA)