Marketplace 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
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/7spade/delon-form-dynamic-schema-forms" ~/.claude/skills/aiskillstore-marketplace-delon-form-dynamic-schema-forms && rm -rf "$T"
manifest:
skills/7spade/delon-form-dynamic-schema-forms/SKILL.mdsource content
@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)