@ -0,0 +1,242 @@ |
|||
# ABP Platform 10.2 RC Has Been Released |
|||
|
|||
We are happy to release [ABP](https://abp.io) version **10.2 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. |
|||
|
|||
Try this version and provide feedback for a more stable version of ABP v10.2! Thanks to you in advance. |
|||
|
|||
## Get Started with the 10.2 RC |
|||
|
|||
You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). |
|||
|
|||
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: |
|||
|
|||
 |
|||
|
|||
## Migration Guide |
|||
|
|||
There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v10.1 or earlier: [ABP Version 10.2 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/abp-10-2). |
|||
|
|||
## What's New with ABP v10.2? |
|||
|
|||
In this section, I will introduce some major features released in this version. |
|||
Here is a brief list of titles explained in the next sections: |
|||
|
|||
- Multi-Tenant Account Usage: Shared User Accounts |
|||
- Prevent Privilege Escalation: Assignment Restrictions for Roles and Permissions |
|||
- `ClientResourcePermissionValueProvider` for OAuth/OpenIddict |
|||
- Angular: Hybrid Localization Support |
|||
- Angular: Extensible Table Row Detail |
|||
- Angular: CMS Kit Module Features |
|||
- Blazor: Upgrade to Blazorise 2.0 |
|||
- Identity: Single Active Token Providers |
|||
- TickerQ Package Upgrade to 10.1.1 |
|||
- AI Management: MCP (Model Context Protocol) Support |
|||
- AI Management: RAG with File Upload |
|||
- AI Management: OpenAI-Compatible Chat Endpoint |
|||
- File Management: Resource-Based Authorization |
|||
|
|||
### Multi-Tenant Account Usage: Shared User Accounts |
|||
|
|||
ABP v10.2 introduces **Shared User Accounts**: a single user account can belong to multiple tenants, and the user can choose or switch the active tenant when signing in. This enables a "one account, multiple tenants" experience — for example, inviting the same email address into multiple tenants. |
|||
|
|||
When you use Shared User Accounts: |
|||
|
|||
- Username/email uniqueness becomes **global** (Host + all tenants) |
|||
- Users are prompted to select the tenant at login if they belong to multiple tenants |
|||
- Users can switch between tenants using the tenant switcher in the user menu |
|||
- Tenant administrators can invite existing or new users to join a tenant |
|||
|
|||
Enable shared accounts by configuring `UserSharingStrategy`: |
|||
|
|||
```csharp |
|||
Configure<AbpMultiTenancyOptions>(options => |
|||
{ |
|||
options.IsEnabled = true; |
|||
options.UserSharingStrategy = TenantUserSharingStrategy.Shared; |
|||
}); |
|||
``` |
|||
|
|||
> See the [Shared User Accounts](https://abp.io/docs/10.2/modules/account/shared-user-accounts) documentation for details. |
|||
|
|||
### Prevent Privilege Escalation: Assignment Restrictions for Roles and Permissions |
|||
|
|||
ABP v10.2 implements a unified **privilege escalation prevention** model to address security vulnerabilities where users could assign themselves or others roles or permissions they do not possess. |
|||
|
|||
**Role Assignment Restriction:** Users can only assign or remove roles they currently have. Users cannot add new roles to themselves (removal only) and cannot assign or remove roles they do not possess. |
|||
|
|||
**Permission Grant/Revoke Authorization:** Users can only grant or revoke permissions they currently have. Validation applies to both grant and revoke operations. |
|||
|
|||
**Incremental Permission Protection:** When updating user or role permissions, permissions the current user does not have are treated as non-editable and are preserved as-is during updates. |
|||
|
|||
Users with the `admin` role can assign any role and grant/revoke any permission. All validations are enforced on the backend — the UI is not a security boundary. |
|||
|
|||
> See [#24775](https://github.com/abpframework/abp/pull/24775) for more details. |
|||
|
|||
### `ClientResourcePermissionValueProvider` for OAuth/OpenIddict |
|||
|
|||
ABP v10.2 adds **ClientResourcePermissionValueProvider**, extending resource-based authorization to OAuth clients. When using IdentityServer or OpenIddict, clients can now have resource permissions aligned with the standard user and role permission model. |
|||
|
|||
This allows you to control which OAuth clients can access which resources, providing fine-grained authorization for API consumers. The implementation integrates with ABP's existing resource permission infrastructure. |
|||
|
|||
> See [#24515](https://github.com/abpframework/abp/pull/24515) for more details. |
|||
|
|||
### Angular: Hybrid Localization Support |
|||
|
|||
ABP v10.2 introduces **Hybrid Localization** for Angular applications, combining server-side and client-side localization strategies. This gives you flexibility in how translations are loaded and resolved — you can use server-provided localization, client-side fallbacks, or a mix of both. |
|||
|
|||
This feature is useful when you want to reduce initial load time, support offline scenarios, or have environment-specific localization behavior. The Angular packages have been updated to support the hybrid approach seamlessly. |
|||
|
|||
> See the [Hybrid Localization](https://abp.io/docs/10.2/framework/ui/angular/hybrid-localization) documentation and [#24731](https://github.com/abpframework/abp/pull/24731). |
|||
|
|||
### Angular: Extensible Table Row Detail |
|||
|
|||
ABP v10.2 adds the **ExtensibleTableRowDetailComponent** for expandable row details in extensible tables. You can now display additional information for each row in a collapsible detail section. |
|||
|
|||
The feature supports row detail templates via both direct input and content child component. It adds toggle logic and emits `rowDetailToggle` events, making it easy to customize the behavior and appearance of expandable rows in your data tables. |
|||
|
|||
> See [#24636](https://github.com/abpframework/abp/pull/24636) for more details. |
|||
|
|||
### Angular: CMS Kit Module Features |
|||
|
|||
ABP v10.2 brings **CMS Kit features to Angular**, completing the cross-platform UI coverage for the CMS Kit module. The Angular implementation includes: Blogs, Blog Posts, Comments, Menus, Pages, Tags, Global Resources, and CMS Settings. |
|||
|
|||
Together with the CMS Kit Pro Angular implementation (FAQ, Newsletters, Page Feedbacks, Polls, Url forwarding), ABP now provides full Angular UI coverage for both the open-source CMS Kit and CMS Kit Pro modules. |
|||
|
|||
> See [#24234](https://github.com/abpframework/abp/pull/24234) for more details. |
|||
|
|||
### Blazor: Upgrade to Blazorise 2.0 |
|||
|
|||
ABP v10.2 upgrades the [Blazorise](https://blazorise.com/) library to **version 2.0** for Blazor UI. If you are upgrading your project to v10.2 RC, please ensure that all Blazorise-related packages are updated to v2.0 in your application. |
|||
|
|||
Blazorise 2.0 includes various improvements and changes. Please refer to the [Blazorise 2.0 Release Notes](https://blazorise.com/news/release-notes/200) and the [ABP Blazorise 2.0 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/blazorise-2-0-migration) for upgrade instructions. |
|||
|
|||
> See [#24906](https://github.com/abpframework/abp/pull/24906) for more details. |
|||
|
|||
### Identity: Single Active Token Providers |
|||
|
|||
ABP v10.2 introduces a **single active token** policy for password reset, email confirmation, and change-email flows. Three new token providers are available: `AbpPasswordResetTokenProvider`, `AbpEmailConfirmationTokenProvider`, and `AbpChangeEmailTokenProvider`. |
|||
|
|||
When a new token is generated, it invalidates any previously issued tokens for that purpose. This improves security by ensuring that only the most recently issued token is valid. Token lifespan can be customized via the respective options classes for each provider. |
|||
|
|||
> See [#24926](https://github.com/abpframework/abp/pull/24926) for more details. |
|||
|
|||
### TickerQ Package Upgrade to 10.1.1 |
|||
|
|||
**If you are using the TickerQ integration packages** (`Volo.Abp.TickerQ`, `Volo.Abp.BackgroundJobs.TickerQ`, or `Volo.Abp.BackgroundWorkers.TickerQ`), you need to apply breaking changes when upgrading to ABP 10.2. TickerQ has been upgraded from 2.5.3 to 10.1.1, which only targets .NET 10.0 and contains several API changes. |
|||
|
|||
Key changes include: |
|||
|
|||
- `UseAbpTickerQ` moved from `IApplicationBuilder` to `IHost` — use `context.GetHost().UseAbpTickerQ()` in your module |
|||
- Entity types renamed: `TimeTicker` → `TimeTickerEntity`, `CronTicker` → `CronTickerEntity` |
|||
- Scheduler and dashboard configuration APIs have changed |
|||
- New helpers: `context.GetHost()`, `GetWebApplication()`, `GetEndpointRouteBuilder()` |
|||
|
|||
> **Important:** Do **not** resolve `IHost` from `context.ServiceProvider.GetRequiredService<IHost>()`. Always use `context.GetHost()`. See the [ABP Version 10.2 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/abp-10-2) for the complete list of changes. |
|||
|
|||
### AI Management: MCP (Model Context Protocol) Support |
|||
|
|||
_This is a **PRO** feature available for ABP Commercial customers._ |
|||
|
|||
The [AI Management Module](https://abp.io/docs/10.2/modules/ai-management) now supports [MCP (Model Context Protocol)](https://modelcontextprotocol.io/), enabling AI workspaces to use external MCP servers as tools. MCP allows AI models to interact with external services, databases, APIs, and more through a standardized protocol. |
|||
|
|||
 |
|||
|
|||
You can create and manage MCP servers via the AI Management UI. Each MCP server supports one of the following transport types: **Stdio** (runs a local command), **SSE** (Server-Sent Events), or **StreamableHttp**. For HTTP-based transports, you can configure authentication (API Key, Bearer token, or custom headers). Once MCP servers are defined, you can associate them with workspaces. When a workspace has MCP servers associated, the AI model can invoke tools from those servers during chat conversations — tool calls and results are displayed in the chat interface. |
|||
|
|||
You can test the connection to an MCP server after creating it to verify connectivity and list available tools before use: |
|||
|
|||
 |
|||
|
|||
When a workspace has MCP servers associated, the AI model can invoke tools from those servers during chat conversations. Tool calls and results are displayed in the chat interface. |
|||
|
|||
 |
|||
|
|||
> See the [AI Management documentation](https://abp.io/docs/10.2/modules/ai-management#mcp-servers) for details. |
|||
|
|||
### AI Management: RAG with File Upload |
|||
|
|||
_This is a **PRO** feature available for ABP Commercial customers._ |
|||
|
|||
The AI Management module supports **RAG (Retrieval-Augmented Generation)** with file upload, which enables workspaces to answer questions based on the content of uploaded documents. When RAG is configured, the AI model searches the uploaded documents for relevant information before generating a response. |
|||
|
|||
To enable RAG, configure an **embedder** (e.g., OpenAI, Ollama) and a **vector store** (e.g., PgVector) on the workspace: |
|||
|
|||
| Embedder | Vector Store | |
|||
| --- | --- | |
|||
|  |  | |
|||
|
|||
You can then upload documents (PDF, Markdown, or text files, max 10 MB) through the workspace management UI. Uploaded documents are automatically processed — their content is chunked, embedded, and stored in the configured vector store: |
|||
|
|||
 |
|||
|
|||
When you ask questions in the chat interface, the AI model uses the uploaded documents as context for accurate, grounded responses. |
|||
|
|||
> See the [AI Management — RAG with File Upload](https://abp.io/docs/10.2/modules/ai-management#rag-with-file-upload) documentation for configuration details. |
|||
|
|||
### AI Management: OpenAI-Compatible Chat Endpoint |
|||
|
|||
_This is a **PRO** feature available for ABP Commercial customers._ |
|||
|
|||
The AI Management module exposes an **OpenAI-compatible REST API** at the `/v1` path. This allows any application or tool that supports the OpenAI API format — such as [AnythingLLM](https://anythingllm.com/), [Open WebUI](https://openwebui.com/), [Dify](https://dify.ai/), or custom scripts using the OpenAI SDK — to connect directly to your AI Management instance. |
|||
|
|||
**Example configuration from AnythingLLM**: |
|||
|
|||
 |
|||
|
|||
Each AI Management **workspace** appears as a selectable model in the client application. The workspace's configured AI provider handles the actual inference transparently. Available endpoints include `/v1/chat/completions`, `/v1/models`, `/v1/embeddings`, `/v1/files`, and more. All endpoints require authentication via a Bearer token in the `Authorization` header. |
|||
|
|||
> See the [AI Management — OpenAI-Compatible API](https://abp.io/docs/10.2/modules/ai-management#openai-compatible-api) documentation for usage examples. |
|||
|
|||
### File Management: Resource-Based Authorization |
|||
|
|||
_This is a **PRO** feature available for ABP Commercial customers._ |
|||
|
|||
The **File Management Module** now supports **resource-based authorization**. You can control access to individual files and folders per user, role, or client. Permissions can be granted at the resource level via the UI, and the feature integrates with ABP's resource permission infrastructure. |
|||
|
|||
 |
|||
|
|||
This feature is **implemented for all three supported UIs: MVC/Razor Pages, Blazor, and Angular**, providing a consistent experience across your application regardless of the UI framework you use. |
|||
|
|||
### Other Improvements and Enhancements |
|||
|
|||
- **Angular signal APIs**: ABP Angular packages migrated to signal queries, output functions, and signal input functions for alignment with Angular 21 ([#24765](https://github.com/abpframework/abp/pull/24765), [#24766](https://github.com/abpframework/abp/pull/24766), [#24777](https://github.com/abpframework/abp/pull/24777)). |
|||
- **Angular Vitest**: ABP Angular templates now use Vitest as the default testing framework instead of Karma/Jasmine ([#24725](https://github.com/abpframework/abp/pull/24725)). |
|||
- **Ambient auditing**: Programmatic disable/enable of auditing via `IAuditingHelper.DisableAuditing()` and `IsAuditingEnabled()` ([#24718](https://github.com/abpframework/abp/pull/24718)). |
|||
- **Complex property auditing**: Entity History and ModifierId now support EF Core complex properties ([#24767](https://github.com/abpframework/abp/pull/24767)). |
|||
- **RabbitMQ correlation ID**: Correlation ID support added to RabbitMQ JobQueue for distributed tracing ([#24755](https://github.com/abpframework/abp/pull/24755)). |
|||
- **Concurrent config retrieval**: `MvcCachedApplicationConfigurationClient` now fetches configuration and localization concurrently for faster startup ([#24838](https://github.com/abpframework/abp/pull/24838)). |
|||
- **Environment localization fallback**: Angular can use `environment.defaultResourceName` when the backend does not provide it ([#24589](https://github.com/abpframework/abp/pull/24589)). |
|||
- **JS proxy namespace fix**: Resolved namespace mismatch for multi-segment company names in generated proxies ([#24877](https://github.com/abpframework/abp/pull/24877)). |
|||
- **Audit Logging max length**: Entity/property type full names increased to 512 characters to reduce truncation ([#24846](https://github.com/abpframework/abp/pull/24846)). |
|||
- **AI guidelines**: Cursor and Copilot AI guideline documents added for ABP development ([#24563](https://github.com/abpframework/abp/pull/24563), [#24593](https://github.com/abpframework/abp/pull/24593)). |
|||
|
|||
## Community News |
|||
|
|||
### New ABP Community Articles |
|||
|
|||
As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: |
|||
|
|||
- [Enis Necipoğlu](https://abp.io/community/members/enisn) has published 2 new posts: |
|||
- [ABP Framework's Hidden Magic: Things That Just Work Without You Knowing](https://abp.io/community/articles/hidden-magic-things-that-just-work-without-you-knowing-vw6osmyt) |
|||
- [Implementing Multiple Global Query Filters with Entity Framework Core](https://abp.io/community/articles/implementing-multiple-global-query-filters-with-entity-ugnsmf6i) |
|||
- [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa) has published 2 new posts: |
|||
- [.NET 11 Preview 1 Highlights: Faster Runtime, Smarter JIT, and AI-Ready Improvements](https://abp.io/community/articles/dotnet-11-preview-1-highlights-hspp3o5x) |
|||
- [TOON vs JSON for LLM Prompts in ABP: Token-Efficient Structured Context](https://abp.io/community/articles/toon-vs-json-b4rn2avd) |
|||
- [Fahri Gedik](https://abp.io/community/members/fahrigedik) has published 2 new posts: |
|||
- [Building a Multi-Agent AI System with A2A, MCP, and ADK in .NET](https://abp.io/community/articles/building-a-multiagent-ai-system-with-a2a-mcp-iefdehyx) |
|||
- [Async Chain of Persistence Pattern: Designing for Failure in Event-Driven Systems](https://abp.io/community/articles/async-chain-of-persistence-pattern-wzjuy4gl) |
|||
- [Alper Ebiçoğlu](https://abp.io/community/members/alper) has published 2 new posts: |
|||
- [NDC London 2026: From a Developer's Perspective and My Personal Notes about AI](https://abp.io/community/articles/ndc-london-2026-a-.net-conf-from-a-developers-perspective-07wp50yl) |
|||
- [Which Open-Source PDF Libraries Are Recently Popular? A Data-Driven Look At PDF Topic](https://abp.io/community/articles/which-opensource-pdf-libraries-are-recently-popular-a-g68q78it) |
|||
- [Stop Spam and Toxic Users in Your App with AI](https://abp.io/community/articles/stop-spam-and-toxic-users-in-your-app-with-ai-3i0xxh0y) by [Engincan Veske](https://abp.io/community/members/EngincanV) |
|||
- [How AI Is Changing Developers](https://abp.io/community/articles/how-ai-is-changing-developers-e8y4a85f) by [Liming Ma](https://abp.io/community/members/maliming) |
|||
- [JetBrains State of Developer Ecosystem Report 2025 — Key Insights](https://abp.io/community/articles/jetbrains-state-of-developer-ecosystem-report-2025-key-z0638q5e) by [Tarık Özdemir](https://abp.io/community/members/mtozdemir) |
|||
- [Integrating AI into ABP.IO Applications: The Complete Guide to Volo.Abp.AI and AI Management Module](https://abp.io/community/articles/integrating-ai-into-abp.io-applications-the-complete-guide-jc9fbjq0) by [Adnan Ali](https://abp.io/community/members/adnanaldaim) |
|||
|
|||
Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. |
|||
|
|||
## Conclusion |
|||
|
|||
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/10.2/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.2 RC and provide feedback to help us release a more stable version. |
|||
|
|||
Thanks for being a part of this community! |
|||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 56 KiB |
@ -0,0 +1,930 @@ |
|||
```json |
|||
//[doc-seo] |
|||
{ |
|||
"Description": "Learn how to use the ABP Dynamic Form Module to create dynamic, configurable forms with validation, conditional logic, nested groups and arrays, many input types, 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: ` |
|||
<abp-dynamic-form |
|||
[fields]="formFields" |
|||
[submitButtonText]="'Submit'" |
|||
[showCancelButton]="true" |
|||
(onSubmit)="handleSubmit($event)" |
|||
(formCancel)="handleCancel()" |
|||
/> |
|||
`, |
|||
}) |
|||
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<string, any>` | `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<any>` | Emitted when the form is submitted with valid data | |
|||
| `formCancel` | `EventEmitter<void>` | 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/radio (static or API) |
|||
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<ControlValueAccessor>; // Custom component |
|||
|
|||
// Type-specific attributes |
|||
min?: number | string; // number, date, time, range |
|||
max?: number | string; // number, date, time, range |
|||
step?: number | string; // number, time, range |
|||
minLength?: number; // text, password |
|||
maxLength?: number; // text, password |
|||
pattern?: string; // tel, text (regex) |
|||
accept?: string; // file (e.g. "image/*") |
|||
multiple?: boolean; // file |
|||
|
|||
// Nested forms (group / array) |
|||
children?: FormFieldConfig[]; // Child fields for group/array |
|||
minItems?: number; // array: minimum items (default 0) |
|||
maxItems?: number; // array: maximum items |
|||
} |
|||
``` |
|||
|
|||
### Field Types |
|||
|
|||
The following field types are supported: |
|||
|
|||
| Type | Description | |
|||
|------|-------------| |
|||
| `text` | Text input | |
|||
| `email` | Email input | |
|||
| `number` | Number input (supports `min`, `max`, `step`) | |
|||
| `select` | Dropdown select (static or API-driven options) | |
|||
| `checkbox` | Checkbox | |
|||
| `date` | Date picker (supports `min`, `max`) | |
|||
| `datetime-local` | Date and time picker | |
|||
| `time` | Time picker (supports `min`, `max`, `step`) | |
|||
| `textarea` | Multi-line text | |
|||
| `password` | Password input (`minLength`, `maxLength`) | |
|||
| `tel` | Telephone input (`pattern`) | |
|||
| `url` | URL input | |
|||
| `radio` | Radio group (uses `options`) | |
|||
| `file` | File upload (`accept`, `multiple`) | |
|||
| `range` | Range slider (`min`, `max`, `step`) | |
|||
| `color` | Color picker | |
|||
| `group` | Nested group of fields (uses `children`) | |
|||
| `array` | Dynamic list with add/remove (uses `children`, `minItems`, `maxItems`) | |
|||
|
|||
**Notes:** |
|||
- `file`: form value is `File` or `File[]` when `multiple` is true. Use `accept` (e.g. `"image/*"`) to limit types. |
|||
- `range`: defaults `min` 0, `max` 100, `step` 1 if omitted. |
|||
- `radio`: requires `options` (static `defaultValues` or `url`). |
|||
|
|||
## 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 and Radio Fields with Options |
|||
|
|||
You can create `select` dropdowns or `radio` groups with static or dynamic options. Both use the `options` property (`OptionProps`). |
|||
|
|||
### 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 |
|||
|
|||
Used for `select` and `radio` fields. Provide either static `defaultValues` or `url` for API-driven options: |
|||
|
|||
```ts |
|||
interface OptionProps<T = any> { |
|||
defaultValues?: T[]; // Static array of options |
|||
url?: string; // API endpoint URL (fetched via RestService) |
|||
disabled?: (option: T) => boolean; // Function to disable specific options |
|||
labelProp?: string; // Property name for label (default 'value') |
|||
valueProp?: string; // Property name for value (default 'key') |
|||
apiName?: string; // API name for RestService when using url |
|||
} |
|||
``` |
|||
|
|||
When using `url`, the response array is mapped with `valueProp` / `labelProp` to build options. Localization is applied to labels via `abpLocalization` where applicable. |
|||
|
|||
## 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). |
|||
|
|||
## Nested Forms |
|||
|
|||
The Dynamic Form supports **nested structures** via two field types: |
|||
|
|||
### Group Type |
|||
|
|||
Use `type: 'group'` to group related fields (e.g. address, contact info). Define child fields in `children`: |
|||
|
|||
```ts |
|||
{ |
|||
key: 'address', |
|||
type: 'group', |
|||
label: 'Address Information', |
|||
gridSize: 12, |
|||
children: [ |
|||
{ key: 'street', type: 'text', label: 'Street', gridSize: 8 }, |
|||
{ key: 'city', type: 'text', label: 'City', gridSize: 4 }, |
|||
{ key: 'zipCode', type: 'text', label: 'ZIP Code', gridSize: 6 }, |
|||
], |
|||
} |
|||
``` |
|||
|
|||
**Output:** `{ "address": { "street": "...", "city": "...", "zipCode": "..." } }` |
|||
|
|||
Groups use `<fieldset>` / `<legend>` for semantics and accessibility. Nesting is recursive (groups inside groups). |
|||
|
|||
### Array Type |
|||
|
|||
Use `type: 'array'` for dynamic lists with add/remove (e.g. phone numbers, work experience). Set `children` for each item schema, and optionally `minItems` / `maxItems`: |
|||
|
|||
```ts |
|||
{ |
|||
key: 'phoneNumbers', |
|||
type: 'array', |
|||
label: 'Phone Numbers', |
|||
minItems: 1, |
|||
maxItems: 5, |
|||
gridSize: 12, |
|||
children: [ |
|||
{ |
|||
key: 'type', |
|||
type: 'select', |
|||
label: 'Type', |
|||
gridSize: 4, |
|||
options: { |
|||
defaultValues: [ |
|||
{ key: 'mobile', value: 'Mobile' }, |
|||
{ key: 'home', value: 'Home' }, |
|||
{ key: 'work', value: 'Work' }, |
|||
], |
|||
}, |
|||
}, |
|||
{ key: 'number', type: 'tel', label: 'Number', gridSize: 8 }, |
|||
], |
|||
} |
|||
``` |
|||
|
|||
**Output:** `{ "phoneNumbers": [ { "type": "mobile", "number": "..." }, ... ] }` |
|||
|
|||
Arrays render add/remove buttons, item labels (e.g. "Phone Number #1"), and respect `minItems` / `maxItems`. You can nest groups inside arrays and arrays inside groups. |
|||
|
|||
See `NESTED-FORMS.md` in the package and `apps/dev-app/src/app/dynamic-form-page` for more examples. |
|||
|
|||
## 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: ` |
|||
<div class="rating"> |
|||
@for (star of [1,2,3,4,5]; track star) { |
|||
<span |
|||
class="star" |
|||
[class.filled]="star <= value" |
|||
(click)="setValue(star)"> |
|||
★ |
|||
</span> |
|||
} |
|||
</div> |
|||
`, |
|||
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: ` |
|||
<abp-dynamic-form |
|||
[fields]="formFields" |
|||
[values]="initialValues" |
|||
(onSubmit)="handleSubmit($event)" |
|||
/> |
|||
`, |
|||
}) |
|||
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: ` |
|||
<abp-dynamic-form |
|||
#myForm="abpDynamicForm" |
|||
[fields]="formFields" |
|||
(onSubmit)="handleSubmit($event)" |
|||
/> |
|||
|
|||
<button (click)="myForm.resetForm()">Reset Form</button> |
|||
`, |
|||
}) |
|||
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: ` |
|||
<abp-dynamic-form |
|||
[fields]="formFields" |
|||
(onSubmit)="handleSubmit($event)"> |
|||
|
|||
<div actions class="form-actions"> |
|||
<button type="button" class="btn btn-secondary" (click)="handleCancel()"> |
|||
Cancel |
|||
</button> |
|||
<button type="submit" class="btn btn-success"> |
|||
Save Changes |
|||
</button> |
|||
<button type="button" class="btn btn-info" (click)="handleDraft()"> |
|||
Save as Draft |
|||
</button> |
|||
</div> |
|||
</abp-dynamic-form> |
|||
`, |
|||
}) |
|||
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'); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Accessibility |
|||
|
|||
The Dynamic Form includes built-in accessibility support: |
|||
|
|||
- **ARIA attributes**: `aria-label`, `aria-required`, `aria-invalid`, `aria-describedby`, `aria-busy` on inputs and actions; `role="form"`, `role="group"`, `role="radiogroup"`, `role="alert"` where appropriate. |
|||
- **Semantic HTML**: `<fieldset>` / `<legend>` for groups; proper `<label>` / `for` associations. |
|||
- **Error handling**: Validation errors are exposed via `aria-describedby` and `aria-live="polite"` so screen readers announce them. |
|||
- **Focus management**: On submit when the form is invalid, focus moves to the first invalid field and it scrolls into view. |
|||
- **Keyboard navigation**: All controls are keyboard-accessible; range and color inputs use appropriate ARIA value attributes. |
|||
|
|||
When using custom components or projected actions, keep labels, error associations, and focus behavior consistent for a good experience. |
|||
|
|||
## 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: ` |
|||
<div class="container"> |
|||
<h2>Employee Registration</h2> |
|||
<abp-dynamic-form |
|||
#employeeForm="abpDynamicForm" |
|||
[fields]="formFields" |
|||
[submitButtonText]="'Register Employee'" |
|||
[showCancelButton]="true" |
|||
[submitInProgress]="isSubmitting" |
|||
(onSubmit)="handleSubmit($event)" |
|||
(formCancel)="handleCancel()" |
|||
/> |
|||
</div> |
|||
`, |
|||
}) |
|||
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. It is `providedIn: 'root'`. |
|||
|
|||
#### Methods |
|||
|
|||
| Method | Parameters | Returns | Description | |
|||
|--------|-----------|---------|-------------| |
|||
| `createFormGroup(fields)` | `FormFieldConfig[]` | `FormGroup` | Creates a FormGroup from field configurations (handles `group` / `array` recursively) | |
|||
| `getInitialValues(fields)` | `FormFieldConfig[]` | `any` | Extracts initial values from field configurations | |
|||
| `getOptions(url, apiName?)` | `string, string?` | `Observable<any[]>` | Fetches options from an API via `RestService`; used for `select` / `radio` when `options.url` is set | |
|||
|
|||
Nested forms use `DynamicFormGroupComponent` and `DynamicFormArrayComponent` internally. You configure them via `type: 'group'` / `type: 'array'` and `children`; you do not need to use these components directly. |
|||
|
|||
## See Also |
|||
|
|||
- [Form Validation](./form-validation.md) |
|||
- [Form Input Component](./form-input-component.md) |
|||
- [Dynamic Form Extensions](./dynamic-form-extensions.md) |
|||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@ -0,0 +1,160 @@ |
|||
using System; |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class ABP10_2 : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropIndex( |
|||
name: "IX_AbpPermissions_Name", |
|||
table: "AbpPermissions"); |
|||
|
|||
migrationBuilder.AddColumn<DateTimeOffset>( |
|||
name: "LastSignInTime", |
|||
table: "AbpUsers", |
|||
type: "datetimeoffset", |
|||
nullable: true); |
|||
|
|||
migrationBuilder.AddColumn<bool>( |
|||
name: "Leaved", |
|||
table: "AbpUsers", |
|||
type: "bit", |
|||
nullable: false, |
|||
defaultValue: false); |
|||
|
|||
migrationBuilder.AlterColumn<string>( |
|||
name: "GroupName", |
|||
table: "AbpPermissions", |
|||
type: "nvarchar(128)", |
|||
maxLength: 128, |
|||
nullable: true, |
|||
oldClrType: typeof(string), |
|||
oldType: "nvarchar(128)", |
|||
oldMaxLength: 128); |
|||
|
|||
migrationBuilder.AddColumn<string>( |
|||
name: "ManagementPermissionName", |
|||
table: "AbpPermissions", |
|||
type: "nvarchar(128)", |
|||
maxLength: 128, |
|||
nullable: true); |
|||
|
|||
migrationBuilder.AddColumn<string>( |
|||
name: "ResourceName", |
|||
table: "AbpPermissions", |
|||
type: "nvarchar(256)", |
|||
maxLength: 256, |
|||
nullable: true); |
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "AbpResourcePermissionGrants", |
|||
columns: table => new |
|||
{ |
|||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), |
|||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), |
|||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false), |
|||
ProviderName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false), |
|||
ProviderKey = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false), |
|||
ResourceName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false), |
|||
ResourceKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_AbpResourcePermissionGrants", x => x.Id); |
|||
}); |
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "AbpUserPasskeys", |
|||
columns: table => new |
|||
{ |
|||
CredentialId = table.Column<byte[]>(type: "varbinary(1024)", maxLength: 1024, nullable: false), |
|||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), |
|||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), |
|||
Data = table.Column<string>(type: "nvarchar(max)", nullable: true) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_AbpUserPasskeys", x => x.CredentialId); |
|||
table.ForeignKey( |
|||
name: "FK_AbpUserPasskeys_AbpUsers_UserId", |
|||
column: x => x.UserId, |
|||
principalTable: "AbpUsers", |
|||
principalColumn: "Id", |
|||
onDelete: ReferentialAction.Cascade); |
|||
}); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_AbpPermissions_ResourceName_Name", |
|||
table: "AbpPermissions", |
|||
columns: new[] { "ResourceName", "Name" }, |
|||
unique: true, |
|||
filter: "[ResourceName] IS NOT NULL"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_AbpResourcePermissionGrants_TenantId_Name_ResourceName_ResourceKey_ProviderName_ProviderKey", |
|||
table: "AbpResourcePermissionGrants", |
|||
columns: new[] { "TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey" }, |
|||
unique: true, |
|||
filter: "[TenantId] IS NOT NULL"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_AbpUserPasskeys_UserId", |
|||
table: "AbpUserPasskeys", |
|||
column: "UserId"); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropTable( |
|||
name: "AbpResourcePermissionGrants"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "AbpUserPasskeys"); |
|||
|
|||
migrationBuilder.DropIndex( |
|||
name: "IX_AbpPermissions_ResourceName_Name", |
|||
table: "AbpPermissions"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "LastSignInTime", |
|||
table: "AbpUsers"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "Leaved", |
|||
table: "AbpUsers"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "ManagementPermissionName", |
|||
table: "AbpPermissions"); |
|||
|
|||
migrationBuilder.DropColumn( |
|||
name: "ResourceName", |
|||
table: "AbpPermissions"); |
|||
|
|||
migrationBuilder.AlterColumn<string>( |
|||
name: "GroupName", |
|||
table: "AbpPermissions", |
|||
type: "nvarchar(128)", |
|||
maxLength: 128, |
|||
nullable: false, |
|||
defaultValue: "", |
|||
oldClrType: typeof(string), |
|||
oldType: "nvarchar(128)", |
|||
oldMaxLength: 128, |
|||
oldNullable: true); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_AbpPermissions_Name", |
|||
table: "AbpPermissions", |
|||
column: "Name", |
|||
unique: true); |
|||
} |
|||
} |
|||
} |
|||
@ -1,3 +1,3 @@ |
|||
{ |
|||
"ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True", |
|||
"ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True" |
|||
} |
|||
@ -1,3 +1,4 @@ |
|||
{ |
|||
"role": "host.mvc" |
|||
"role": "host.mvc", |
|||
"projectId": "47142bf8-4bb8-41c5-9900-990a97a67e8a" |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Guids; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Volo.Abp.PermissionManagement; |
|||
|
|||
public class TestUnavailableResourcePermissionManagementProvider : ResourcePermissionManagementProvider |
|||
{ |
|||
public override string Name => "TestUnavailable"; |
|||
|
|||
public TestUnavailableResourcePermissionManagementProvider( |
|||
IResourcePermissionGrantRepository resourcePermissionGrantRepository, |
|||
IGuidGenerator guidGenerator, |
|||
ICurrentTenant currentTenant) |
|||
: base( |
|||
resourcePermissionGrantRepository, |
|||
guidGenerator, |
|||
currentTenant) |
|||
{ |
|||
} |
|||
|
|||
public override Task<bool> IsAvailableAsync() |
|||
{ |
|||
return Task.FromResult(false); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Volo.Abp.PermissionManagement; |
|||
|
|||
public class TestUnavailableResourcePermissionProviderKeyLookupService : IResourcePermissionProviderKeyLookupService, ITransientDependency |
|||
{ |
|||
public string Name => "TestUnavailable"; |
|||
|
|||
public ILocalizableString DisplayName => new LocalizableString("TestUnavailable", "TestResource"); |
|||
|
|||
public Task<bool> IsAvailableAsync() |
|||
{ |
|||
return Task.FromResult(false); |
|||
} |
|||
|
|||
public Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default) |
|||
{ |
|||
throw new System.NotImplementedException(); |
|||
} |
|||
|
|||
public Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string[] keys, CancellationToken cancellationToken = default) |
|||
{ |
|||
throw new System.NotImplementedException(); |
|||
} |
|||
} |
|||