Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/aiskillstore-delon-form-dynamic-schema-forms/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 42% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 159% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 210% | 0% |
This skill helps create dynamic forms using @delon/form's SF (Schema Form) component.
typescriptimport { 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); } }
typescript{ name: { type: 'string', title: 'Name', ui: { widget: 'string', placeholder: 'Enter name', prefix: 'User', suffix: '@', maxLength: 100 } } }
typescript{ description: { type: 'string', title: 'Description', ui: { widget: 'textarea', autosize: { minRows: 3, maxRows: 6 }, placeholder: 'Enter description' } } }
typescript{ 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, ',') } } }
typescript{ birthDate: { type: 'string', title: 'Birth Date', format: 'date', ui: { widget: 'date', mode: 'date', displayFormat: 'yyyy-MM-dd', end: 'today' // Can't select future dates } } }
typescript{ dateRange: { type: 'string', title: 'Date Range', ui: { widget: 'date', mode: 'range', displayFormat: 'yyyy-MM-dd' } } }
typescript{ 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 } } }
typescript{ tags: { type: 'array', title: 'Tags', items: { type: 'string', enum: ['angular', 'react', 'vue', 'typescript'] }, ui: { widget: 'select', mode: 'multiple', placeholder: 'Select tags' } } }
typescript{ 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' } ]; }
typescript{ 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' } } }
typescript{ agree: { type: 'boolean', title: 'Agree to terms', ui: { widget: 'checkbox' } } }
typescript{ isActive: { type: 'boolean', title: 'Active Status', ui: { widget: 'switch', checkedChildren: 'On', unCheckedChildren: 'Off' } } }
typescript{ rating: { type: 'number', title: 'Rating', minimum: 0, maximum: 100, ui: { widget: 'slider', marks: { 0: '0', 50: '50', 100: '100' } } } }
typescript{ avatar: { type: 'string', title: 'Avatar', ui: { widget: 'upload', action: '/api/upload', accept: 'image/*', limit: 1, listType: 'picture-card' } } }
typescript{ 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 })); }
typescript{ 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); }
typescriptschema: 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'] } } } } };
typescriptimport { 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 []; } } } } };
typescriptimport { 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()); } }
typescriptui: SFUISchema = { '*': { spanLabel: 4, spanControl: 20, grid: { span: 12 } // 2 columns (24 / 12 = 2) }, $description: { grid: { span: 24 } // Full width } };
typescriptui: SFUISchema = { '*': { grid: { xs: 24, // Mobile: full width sm: 12, // Tablet: 2 columns md: 8, // Desktop: 3 columns lg: 6 // Large: 4 columns } } };
typescript@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 }; }
When creating SF forms:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 22,530 | 14,394 | -36% | 1 | 1 | 0% | 4,931 | 6,982 | +42% | 0 | 0 | — |
case-02 | fail→pass | 27,856 | 25,044 | -10% | 1 | 1 | 0% | 5,665 | 9,483 | +67% | 0 | 0 | — |
case-03 | fail→pass | 22,968 | 15,877 | -31% | 1 | 1 | 0% | 4,757 | 7,046 | +48% | 0 | 0 | — |
case-04 | pass→pass | 11,317 | 9,298 | -18% | 1 | 1 | 0% | 2,385 | 5,689 | +139% | 0 | 0 | — |
case-05 | pass→pass | 14,207 | 9,073 | -36% | 1 | 1 | 0% | 2,522 | 5,600 | +122% | 0 | 0 | — |
case-06 | pass→pass | 10,029 | 7,036 | -30% | 1 | 1 | 0% | 1,727 | 5,216 | +202% | 0 | 0 | — |
case-07 | pass→pass | 7,334 | 4,768 | -35% | 1 | 1 | 0% | 1,330 | 4,652 | +250% | 0 | 0 | — |
case-08 | pass→pass | 6,511 | 4,077 | -37% | 1 | 1 | 0% | 1,124 | 4,561 | +306% | 0 | 0 | — |
case-09 | pass→pass | 10,052 | 5,339 | -47% | 1 | 1 | 0% | 1,822 | 4,763 | +161% | 0 | 0 | — |
case-10 | fail→fail | 10,888 | 9,219 | -15% | 1 | 1 | 0% | 1,839 | 5,527 | +201% | 0 | 0 | — |
case-11 | pass→pass | 12,515 | 9,160 | -27% | 1 | 1 | 0% | 1,979 | 5,449 | +175% | 0 | 0 | — |
case-12 | pass→pass | 7,392 | 5,978 | -19% | 1 | 1 | 0% | 1,289 | 4,817 | +274% | 0 | 0 | — |
case-13 | pass→pass | 10,297 | 13,746 | +33% | 1 | 1 | 0% | 1,931 | 5,302 | +175% | 0 | 0 | — |
case-14 | fail→pass | 11,049 | 6,221 | -44% | 1 | 1 | 0% | 1,911 | 4,943 | +159% | 0 | 0 | — |
case-15 | pass→pass | 9,791 | 7,406 | -24% | 1 | 1 | 0% | 1,766 | 5,058 | +186% | 0 | 0 | — |
case-16 | pass→pass | 8,615 | 4,346 | -50% | 1 | 1 | 0% | 980 | 4,662 | +376% | 0 | 0 | — |
case-17 | pass→pass | 9,335 | 6,943 | -26% | 1 | 1 | 0% | 1,758 | 5,158 | +193% | 0 | 0 | — |
case-18 | pass→pass | 4,942 | 4,671 | -5% | 1 | 1 | 0% | 826 | 4,672 | +466% | 0 | 0 | — |
case-19 | pass→pass | 25,600 | 6,423 | -75% | 1 | 1 | 0% | 2,197 | 5,023 | +129% | 0 | 0 | — |
case-20 | fail→pass | 8,847 | 5,955 | -33% | 1 | 1 | 0% | 1,629 | 5,052 | +210% | 0 | 0 | — |
case-21 | fail→pass | 11,901 | 6,408 | -46% | 1 | 1 | 0% | 2,380 | 5,157 | +117% | 0 | 0 | — |
case-22 | fail→fail | 12,444 | 12,265 | -1% | 1 | 1 | 0% | 2,386 | 6,198 | +160% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.