Delon form dynamic schema forms
Skill aiskillstore/marketplace/skills/7spade/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.From its SKILL.md
npx -y skills add aiskillstore/marketplace --skill delon-form-dynamic-schema-formsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
12.3 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
@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: '[email protected]',
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
What ships with it: 1 file
53.4 KB alongside SKILL.md
- skill-report.json53.4 KB