diff --git a/docs/en/framework/ui/angular/dynamic-form-module.md b/docs/en/framework/ui/angular/dynamic-form-module.md new file mode 100644 index 0000000000..c3a307bc56 --- /dev/null +++ b/docs/en/framework/ui/angular/dynamic-form-module.md @@ -0,0 +1,816 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to use the ABP Dynamic Form Module to create dynamic, configurable forms with validation, conditional logic, and custom components in Angular applications." +} +``` + +# Dynamic Form Module + +The ABP Dynamic Form Module is a powerful component that allows you to create dynamic, configurable forms without writing extensive HTML templates. It provides a declarative way to define form fields with validation, conditional logic, grid layout, and custom components. + +## Installation + +The Dynamic Form Module is part of the `@abp/ng.components` package. If you haven't installed it yet, install it via npm: + +```bash +npm install @abp/ng.components +``` + +## Usage + +Import the `DynamicFormComponent` in your component: + +```ts +import { DynamicFormComponent } from '@abp/ng.components/dynamic-form'; + +@Component({ + selector: 'app-my-component', + imports: [DynamicFormComponent], + templateUrl: './my-component.component.html', +}) +export class MyComponent {} +``` + +## Basic Example + +Here's a simple example of how to use the dynamic form: + +```ts +import { Component } from '@angular/core'; +import { DynamicFormComponent } from '@abp/ng.components/dynamic-form'; +import { FormFieldConfig } from '@abp/ng.components/dynamic-form'; + +@Component({ + selector: 'app-user-form', + imports: [DynamicFormComponent], + template: ` + + + `, +}) +export class UserFormComponent { + formFields: FormFieldConfig[] = [ + { + key: 'firstName', + type: 'text', + label: 'First Name', + placeholder: 'Enter your first name', + required: true, + order: 1, + }, + { + key: 'lastName', + type: 'text', + label: 'Last Name', + placeholder: 'Enter your last name', + required: true, + order: 2, + }, + { + key: 'email', + type: 'email', + label: 'Email', + placeholder: 'Enter your email', + required: true, + order: 3, + }, + ]; + + handleSubmit(formValue: any) { + console.log('Form submitted:', formValue); + // Handle form submission + } + + handleCancel() { + console.log('Form cancelled'); + // Handle form cancellation + } +} +``` + +## Component Inputs + +The `DynamicFormComponent` accepts the following inputs: + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `fields` | `FormFieldConfig[]` | `[]` | Array of field configurations | +| `values` | `Record` | `undefined` | Initial values for the form | +| `submitButtonText` | `string` | `'Submit'` | Text for the submit button | +| `submitInProgress` | `boolean` | `false` | Whether form submission is in progress | +| `showCancelButton` | `boolean` | `false` | Whether to show the cancel button | + +## Component Outputs + +| Output | Type | Description | +|--------|------|-------------| +| `onSubmit` | `EventEmitter` | Emitted when the form is submitted with valid data | +| `formCancel` | `EventEmitter` | Emitted when the cancel button is clicked | + +## FormFieldConfig Properties + +The `FormFieldConfig` interface defines the structure of each field in the form: + +```ts +interface FormFieldConfig { + key: string; // Unique identifier for the field + type: FieldType; // Type of the field + label: string; // Label text for the field + value?: any; // Initial value + placeholder?: string; // Placeholder text + required?: boolean; // Whether the field is required + disabled?: boolean; // Whether the field is disabled + options?: OptionProps; // Options for select fields + validators?: ValidatorConfig[]; // Array of validator configurations + conditionalLogic?: ConditionalRule[]; // Array of conditional rules + order?: number; // Display order (ascending) + gridSize?: number; // Bootstrap grid size (1-12) + component?: Type; // Custom component +} +``` + +### Field Types + +The following field types are supported: + +- `text` - Text input +- `email` - Email input +- `number` - Number input +- `select` - Dropdown select +- `checkbox` - Checkbox +- `date` - Date picker +- `textarea` - Textarea + +## Validators + +You can add validators to your form fields using the `validators` property: + +```ts +const formFields: FormFieldConfig[] = [ + { + key: 'username', + type: 'text', + label: 'Username', + validators: [ + { + type: 'required', + message: 'Username is required', + }, + { + type: 'minLength', + value: 3, + message: 'Username must be at least 3 characters', + }, + { + type: 'maxLength', + value: 20, + message: 'Username must not exceed 20 characters', + }, + ], + }, + { + key: 'age', + type: 'number', + label: 'Age', + validators: [ + { + type: 'min', + value: 18, + message: 'You must be at least 18 years old', + }, + { + type: 'max', + value: 100, + message: 'Age must not exceed 100', + }, + ], + }, +]; +``` + +### Available Validator Types + +| Type | Description | Requires Value | +|------|-------------|----------------| +| `required` | Field is required | No | +| `email` | Must be a valid email | No | +| `minLength` | Minimum string length | Yes | +| `maxLength` | Maximum string length | Yes | +| `min` | Minimum numeric value | Yes | +| `max` | Maximum numeric value | Yes | +| `pattern` | Regular expression pattern | Yes | +| `requiredTrue` | Must be true (for checkboxes) | No | + +## Select Fields with Options + +You can create dropdown select fields with static or dynamic options: + +### Static Options + +```ts +const formFields: FormFieldConfig[] = [ + { + key: 'country', + type: 'select', + label: 'Country', + options: { + defaultValues: [ + { key: 'us', value: 'United States' }, + { key: 'uk', value: 'United Kingdom' }, + { key: 'ca', value: 'Canada' }, + ], + valueProp: 'key', + labelProp: 'value', + }, + }, +]; +``` + +### Dynamic Options from API + +```ts +const formFields: FormFieldConfig[] = [ + { + key: 'department', + type: 'select', + label: 'Department', + options: { + url: '/api/departments', + apiName: 'MyApi', + valueProp: 'id', + labelProp: 'name', + }, + }, +]; +``` + +### OptionProps Interface + +```ts +interface OptionProps { + defaultValues?: T[]; // Static array of options + url?: string; // API endpoint URL + disabled?: (option: T) => boolean; // Function to disable specific options + labelProp?: string; // Property name for label + valueProp?: string; // Property name for value + apiName?: string; // API name for RestService +} +``` + +## Conditional Logic + +The Dynamic Form Module supports conditional logic to show/hide or enable/disable fields based on other field values: + +```ts +const formFields: FormFieldConfig[] = [ + { + key: 'hasLicense', + type: 'checkbox', + label: 'Do you have a driver\'s license?', + order: 1, + }, + { + key: 'licenseNumber', + type: 'text', + label: 'License Number', + placeholder: 'Enter your license number', + order: 2, + conditionalLogic: [ + { + dependsOn: 'hasLicense', + condition: 'equals', + value: true, + action: 'show', + }, + ], + }, + { + key: 'age', + type: 'number', + label: 'Age', + order: 3, + }, + { + key: 'parentConsent', + type: 'checkbox', + label: 'Parent Consent Required', + order: 4, + conditionalLogic: [ + { + dependsOn: 'age', + condition: 'lessThan', + value: 18, + action: 'show', + }, + ], + }, +]; +``` + +### Conditional Rule Interface + +```ts +interface ConditionalRule { + dependsOn: string; // Key of the field to watch + condition: string; // Condition type + value: any; // Value to compare against + action: string; // Action to perform +} +``` + +### Available Conditions + +- `equals` - Field value equals the specified value +- `notEquals` - Field value does not equal the specified value +- `contains` - Field value contains the specified value (for strings/arrays) +- `greaterThan` - Field value is greater than the specified value (for numbers) +- `lessThan` - Field value is less than the specified value (for numbers) + +### Available Actions + +- `show` - Show the field when condition is met +- `hide` - Hide the field when condition is met +- `enable` - Enable the field when condition is met +- `disable` - Disable the field when condition is met + +## Grid Layout + +You can use the `gridSize` property to control the Bootstrap grid layout: + +```ts +const formFields: FormFieldConfig[] = [ + { + key: 'firstName', + type: 'text', + label: 'First Name', + gridSize: 6, // Half width + order: 1, + }, + { + key: 'lastName', + type: 'text', + label: 'Last Name', + gridSize: 6, // Half width + order: 2, + }, + { + key: 'address', + type: 'textarea', + label: 'Address', + gridSize: 12, // Full width + order: 3, + }, +]; +``` + +The `gridSize` property uses Bootstrap's 12-column grid system. If not specified, it defaults to 12 (full width). + +## Custom Components + +You can use custom components for specific fields by providing a component that implements `ControlValueAccessor`: + +```ts +// custom-rating.component.ts +import { Component, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'app-custom-rating', + template: ` +
+ @for (star of [1,2,3,4,5]; track star) { + + ★ + + } +
+ `, + styles: [` + .star { cursor: pointer; font-size: 24px; color: #ccc; } + .star.filled { color: #ffc107; } + `], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CustomRatingComponent), + multi: true + }] +}) +export class CustomRatingComponent implements ControlValueAccessor { + value = 0; + onChange: any = () => {}; + onTouched: any = () => {}; + + setValue(rating: number) { + this.value = rating; + this.onChange(rating); + this.onTouched(); + } + + writeValue(value: any): void { + this.value = value || 0; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } +} +``` + +Then use it in your form configuration: + +```ts +import { CustomRatingComponent } from './custom-rating.component'; + +const formFields: FormFieldConfig[] = [ + { + key: 'rating', + type: 'text', // Type is ignored when using custom component + label: 'Rating', + component: CustomRatingComponent, + value: 3, + }, +]; +``` + +## Setting Initial Values + +You can set initial values for the form fields in two ways: + +### 1. Using the `value` property in FormFieldConfig + +```ts +const formFields: FormFieldConfig[] = [ + { + key: 'firstName', + type: 'text', + label: 'First Name', + value: 'John', + }, +]; +``` + +### 2. Using the `values` input + +```ts +@Component({ + template: ` + + + `, +}) +export class MyComponent { + formFields: FormFieldConfig[] = [ + { + key: 'firstName', + type: 'text', + label: 'First Name', + }, + { + key: 'lastName', + type: 'text', + label: 'Last Name', + }, + ]; + + initialValues = { + firstName: 'John', + lastName: 'Doe', + }; + + handleSubmit(formValue: any) { + console.log(formValue); + } +} +``` + +## Programmatic Form Control + +You can access the form instance using the `exportAs` property and template reference variable: + +```ts +@Component({ + template: ` + + + + + `, +}) +export class MyComponent { + formFields: FormFieldConfig[] = [ + // ... field configurations + ]; + + handleSubmit(formValue: any) { + console.log(formValue); + } +} +``` + +### Available Methods + +- `resetForm()` - Resets the form to its initial state +- `submit()` - Programmatically submit the form + +## Custom Action Buttons + +You can customize the action buttons by projecting your own content: + +```ts +@Component({ + template: ` + + +
+ + + +
+
+ `, +}) +export class MyComponent { + formFields: FormFieldConfig[] = [ + // ... field configurations + ]; + + handleSubmit(formValue: any) { + console.log('Form submitted:', formValue); + } + + handleCancel() { + console.log('Cancelled'); + } + + handleDraft() { + console.log('Saved as draft'); + } +} +``` + +## Complete Example + +Here's a complete example demonstrating various features: + +```ts +import { Component } from '@angular/core'; +import { DynamicFormComponent } from '@abp/ng.components/dynamic-form'; +import { FormFieldConfig } from '@abp/ng.components/dynamic-form'; + +@Component({ + selector: 'app-employee-form', + imports: [DynamicFormComponent], + template: ` +
+

Employee Registration

+ + +
+ `, +}) +export class EmployeeFormComponent { + isSubmitting = false; + + formFields: FormFieldConfig[] = [ + // Personal Information + { + key: 'firstName', + type: 'text', + label: 'First Name', + placeholder: 'Enter first name', + required: true, + gridSize: 6, + order: 1, + validators: [ + { type: 'required', message: 'First name is required' }, + { type: 'minLength', value: 2, message: 'First name must be at least 2 characters' }, + ], + }, + { + key: 'lastName', + type: 'text', + label: 'Last Name', + placeholder: 'Enter last name', + required: true, + gridSize: 6, + order: 2, + validators: [ + { type: 'required', message: 'Last name is required' }, + { type: 'minLength', value: 2, message: 'Last name must be at least 2 characters' }, + ], + }, + { + key: 'email', + type: 'email', + label: 'Email', + placeholder: 'Enter email address', + required: true, + gridSize: 6, + order: 3, + validators: [ + { type: 'required', message: 'Email is required' }, + { type: 'email', message: 'Please enter a valid email address' }, + ], + }, + { + key: 'phoneNumber', + type: 'text', + label: 'Phone Number', + placeholder: 'Enter phone number', + gridSize: 6, + order: 4, + }, + + // Employment Details + { + key: 'department', + type: 'select', + label: 'Department', + required: true, + gridSize: 6, + order: 5, + options: { + defaultValues: [ + { id: 1, name: 'Engineering' }, + { id: 2, name: 'Marketing' }, + { id: 3, name: 'Sales' }, + { id: 4, name: 'Human Resources' }, + ], + valueProp: 'id', + labelProp: 'name', + }, + validators: [ + { type: 'required', message: 'Department is required' }, + ], + }, + { + key: 'position', + type: 'text', + label: 'Position', + placeholder: 'Enter position', + required: true, + gridSize: 6, + order: 6, + validators: [ + { type: 'required', message: 'Position is required' }, + ], + }, + { + key: 'startDate', + type: 'date', + label: 'Start Date', + required: true, + gridSize: 6, + order: 7, + validators: [ + { type: 'required', message: 'Start date is required' }, + ], + }, + + // Conditional Fields + { + key: 'isManager', + type: 'checkbox', + label: 'Is this person a manager?', + gridSize: 12, + order: 8, + }, + { + key: 'teamSize', + type: 'number', + label: 'Team Size', + placeholder: 'Number of team members', + gridSize: 6, + order: 9, + conditionalLogic: [ + { + dependsOn: 'isManager', + condition: 'equals', + value: true, + action: 'show', + }, + ], + validators: [ + { type: 'min', value: 1, message: 'Team size must be at least 1' }, + ], + }, + { + key: 'managementExperience', + type: 'textarea', + label: 'Management Experience', + placeholder: 'Describe your management experience', + gridSize: 12, + order: 10, + conditionalLogic: [ + { + dependsOn: 'isManager', + condition: 'equals', + value: true, + action: 'show', + }, + ], + }, + + // Additional Information + { + key: 'notes', + type: 'textarea', + label: 'Additional Notes', + placeholder: 'Any additional information', + gridSize: 12, + order: 11, + }, + ]; + + handleSubmit(formValue: any) { + this.isSubmitting = true; + + console.log('Employee Data:', formValue); + + // Simulate API call + setTimeout(() => { + this.isSubmitting = false; + alert('Employee registered successfully!'); + }, 2000); + } + + handleCancel() { + if (confirm('Are you sure you want to cancel?')) { + // Navigate back or reset form + console.log('Form cancelled'); + } + } +} +``` + +## API Reference + +### DynamicFormComponent + +#### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `dynamicForm` | `FormGroup` | The underlying Angular FormGroup instance | +| `fieldVisibility` | `{ [key: string]: boolean }` | Object tracking field visibility state | + +#### Methods + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `submit()` | - | `void` | Submits the form if valid | +| `onCancel()` | - | `void` | Emits the formCancel event | +| `resetForm()` | - | `void` | Resets the form to initial values | +| `isFieldVisible(field)` | `FormFieldConfig` | `boolean` | Checks if a field is currently visible | + +### DynamicFormService + +The `DynamicFormService` provides utility methods for form management: + +#### Methods + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `createFormGroup(fields)` | `FormFieldConfig[]` | `FormGroup` | Creates a FormGroup from field configurations | +| `getInitialValues(fields)` | `FormFieldConfig[]` | `any` | Extracts initial values from field configurations | +| `getOptions(url, apiName?)` | `string, string?` | `Observable` | Fetches options from an API endpoint | + +## See Also + +- [Form Validation](./form-validation.md) +- [Form Input Component](./form-input-component.md) +- [Dynamic Form Extensions](./dynamic-form-extensions.md) \ No newline at end of file