Agent skill
@delon/form Dynamic Schema Forms
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/design/delonform-dynamic-schema-forms-tw-lin-ng-lin
SKILL.md
@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)
References
- @delon/form Documentation
- JSON Schema Specification
- SF Component API
- ng-alain Component Skill
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?