<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Wed, 11 Mar 2026 11:44:31 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=erdem.caygor" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/from-server-to-browser-angular-transferstate-explained-m99zf8oh</guid>
      <link>https://abp.io/community/posts/from-server-to-browser-angular-transferstate-explained-m99zf8oh</link>
      <a10:author>
        <a10:name>erdem.caygor</a10:name>
        <a10:uri>https://abp.io/community/members/erdem.caygor</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>prerendering</category>
      <category>http-api</category>
      <category>interceptors</category>
      <category>state-management</category>
      <title>From Server to Browser: Angular TransferState Explained</title>
      <description>Angular’s **TransferState** helps prevent duplicate HTTP requests during SSR hydration by sharing server-fetched data with the browser.  
It’s lightweight, secure for serializable data, and easy to use — whether manually, via an interceptor, or with Angular’s built-in `withHttpTransferCacheOptions`.  
By avoiding redundant API calls, it significantly improves SSR performance and speeds up page hydration.</description>
      <pubDate>Thu, 16 Oct 2025 08:26:25 Z</pubDate>
      <a10:updated>2026-03-11T11:20:13Z</a10:updated>
      <content:encoded><![CDATA[<h1>From Server to Browser — the Elegant Way: Angular TransferState Explained</h1>
<h2>Introduction</h2>
<p>When building Angular applications with Server‑Side Rendering (SSR), a common performance pitfall is duplicated data fetching: the server loads data to render HTML, then the browser bootstraps Angular and fetches the same data again. That’s wasteful, increases Time‑to‑Interactive, and can hammer your APIs.</p>
<p>Angular’s built‑in <strong>TransferState</strong> lets you transfer the data fetched on the server to the browser during hydration so the client can reuse it instead of calling the API again. It’s simple, safe for serializable data, and makes SSR feel instant for users.</p>
<p>This article explains what TransferState is, and how to implement it in your Angular SSR app.</p>
<hr />
<h2>What Is TransferState?</h2>
<p>TransferState is a key–value store that exists for a single SSR render. On the server, you put serializable data into the store. Angular serializes it into the HTML as a small script tag. When the browser hydrates, Angular reads that payload back and makes it available to your app. You can then consume it and skip duplicate HTTP calls.</p>
<p>Key points:</p>
<ul>
<li>Works only across the SSR → browser hydration boundary (not a general cache).</li>
<li>Data is cleaned up after bootstrapping (no stale data).</li>
<li>Stores JSON‑serializable data only (if you need to use Date/Functions/Map; serialize it).</li>
<li>Data is set on the server and read on the client.</li>
</ul>
<hr />
<h2>When Should You Use It?</h2>
<ul>
<li>Data fetched during SSR that is also be needed on the client.</li>
<li>Data that doesn’t change between server render and immediate client hydration.</li>
<li>Expensive or slow API endpoints where a second request is visibly costly.</li>
</ul>
<p>Avoid using it for:</p>
<ul>
<li>Highly dynamic data that changes frequently.</li>
<li>Sensitive data (never put secrets/tokens in TransferState).</li>
<li>Large payloads (keep the serialized state small to avoid bloating HTML).</li>
</ul>
<hr />
<h2>Prerequisites</h2>
<ul>
<li>An Angular app with SSR enabled (Angular ≥16: <code>ng add @angular/ssr</code>).</li>
<li><code>HttpClient</code> configured. The examples below show both manual TransferState use and the build in solutions.</li>
</ul>
<hr />
<h2>Option A — Using TransferState Manually</h2>
<p>This approach gives you full control over what to cache and when. It's straightforward and works in both module‑based and standalone‑based apps.</p>
<p>Service example that fetches books and uses TransferState:</p>
<pre><code class="language-ts">// books.service.ts
import {
    Injectable,
    PLATFORM_ID,
    makeStateKey,
    TransferState,
    inject,
} from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

export interface Book {
    id: number;
    name: string;
    price: number;
}

@Injectable({ providedIn: 'root' })
export class BooksService {
    BOOKS_KEY = makeStateKey&lt;Book[]&gt;('books:list');
    readonly httpClient = inject(HttpClient);
    readonly transferState = inject(TransferState);
    readonly platformId = inject(PLATFORM_ID);

    getBooks(): Observable&lt;Book[]&gt; {
        // If browser and we have the data that already fetched on the server, use it and remove from TransferState
        if (this.transferState.hasKey(this.BOOKS_KEY)) {
            const cached = this.transferState.get&lt;Book[]&gt;(this.BOOKS_KEY, []);
            this.transferState.remove(this.BOOKS_KEY); // remove to avoid stale reads
            return of(cached);
        }

        // Otherwise fetch data. If running on the server, write into TransferState
        return this.httpClient.get&lt;Book[]&gt;('/api/books').pipe(
            tap(list =&gt; {
                if (isPlatformServer(this.platformId)) {
                    this.transferState.set(this.BOOKS_KEY, list);
                }
            })
        );
    }
}

</code></pre>
<p>Use it in a component:</p>
<pre><code class="language-ts">// books.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BooksService, Book } from './books.service';

@Component({
    selector: 'app-books',
    imports: [CommonModule],
    template: `
    &lt;h1&gt;Books&lt;/h1&gt;
    &lt;ul&gt;
      @for (book of books; track book.id) {
        &lt;li&gt;{{ book.name }} — {{ book.price | currency }}&lt;/li&gt;
      }
    &lt;/ul&gt;
  `,
})
export class BooksComponent implements OnInit {
    private booksService = inject(BooksService);
    books: Book[] = [];

    ngOnInit() {
        this.booksService.getBooks().subscribe(data =&gt; (this.books = data));
    }
}

</code></pre>
<p>Route resolver variant (keeps templates simple and aligns with SSR prefetching):</p>
<pre><code class="language-ts">// src/app/routes.ts

export const routes: Routes = [
  {
    path: 'books',
    component: BooksComponent,
    resolve: {
      books: () =&gt; inject(BooksService).getBooks(),
    },
  },
];
</code></pre>
<p>Then read <code>books</code> from the <code>ActivatedRoute</code> data in your component.</p>
<hr />
<h2>Option B — Using HttpInterceptor to Automate TransferState</h2>
<p>Like Option A, but less boilerplate. This approach uses an <strong>HttpInterceptor</strong> to automatically cache HTTP GET (also POST/PUT request but not recommended) responses in TransferState. You can determine which requests to cache based on URL patterns.</p>
<p>Example interceptor that caches GET requests:</p>
<pre><code class="language-ts">import { inject, makeStateKey, PLATFORM_ID, TransferState } from '@angular/core';
import {
    HttpEvent,
    HttpHandlerFn,
    HttpInterceptorFn,
    HttpRequest,
    HttpResponse,
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { tap } from 'rxjs/operators';

export const transferStateInterceptor: HttpInterceptorFn = (
    req: HttpRequest&lt;any&gt;,
    next: HttpHandlerFn,
): Observable&lt;HttpEvent&lt;any&gt;&gt; =&gt; {
    const transferState = inject(TransferState);
    const platformId = inject(PLATFORM_ID);

    // Only cache GET requests. You can customize this to match specific URLs if needed.
    if (req.method !== 'GET') {
        return next(req);
    }

    // Create a unique key for this request
    const stateKey = makeStateKey&lt;HttpResponse&lt;any&gt;&gt;(req.urlWithParams);

    // If browser, check if we have the response in TransferState
    if (isPlatformBrowser(platformId)) {
        const storedResponse = transferState.get&lt;HttpResponse&lt;any&gt;&gt;(stateKey, null);
        if (storedResponse) {
            transferState.remove(stateKey); // remove to avoid stale reads
            return of(new HttpResponse&lt;any&gt;({ body: storedResponse, status: 200 }));
        }
    }

    return next(req).pipe(
        tap(event =&gt; {
            // If server, store the response in TransferState
            if (isPlatformServer(platformId) &amp;&amp; event instanceof HttpResponse) {
                transferState.set(stateKey, event.body);
            }
        }),
    );
};

</code></pre>
<p>Add the interceptor to your app module or bootstrap function:</p>
<pre><code class="language-ts">        provideHttpClient(withFetch(), withInterceptors([transferStateInterceptor]))
</code></pre>
<hr />
<h2>Option C — Using Angular's Built-in HTTP Transfer Cache</h2>
<p>This is the simplest option if you want to HTTP requests that without custom logic.</p>
<p>Angular docs: https://angular.dev/api/platform-browser/withHttpTransferCacheOptions</p>
<p>Usage examples:</p>
<pre><code class="language-ts">   // Only cache GET requests that have no headers
   provideClientHydration(withHttpTransferCacheOptions({}))

    // Also cache POST requests (not recommended for most cases)
    provideClientHydration(withHttpTransferCacheOptions({
        includePostRequests: true
    }))

    // Cache requests that have auth headers (e.g., JWT tokens)
    provideClientHydration(withHttpTransferCacheOptions({
        includeRequestsWithAuthHeaders: true
    }))
</code></pre>
<p>To see all options, check the Angular docs: https://angular.dev/api/common/http/HttpTransferCacheOptions</p>
<h2>Best Practices and Pitfalls</h2>
<ul>
<li>Keep payloads small: only put what’s needed for initial paint.</li>
<li>Serialize explicitly if needed: for Dates or complex types, convert to strings and reconstruct on the client.</li>
<li>Don’t transfer secrets: never place tokens or sensitive user data in TransferState.</li>
<li>Per‑request isolation: state is scoped to a single SSR request; it is not a global cache.</li>
</ul>
<hr />
<h2>Debugging Tips</h2>
<ul>
<li>Log on server vs browser: use <code>isPlatformServer</code> and <code>isPlatformBrowser</code> checks to confirm where code runs.</li>
<li>DevTools inspection: view the page source after SSR; you’ll see a small script tag that embeds the transfer state.</li>
<li>Count requests: put a console log in your service to verify the second HTTP call is gone on the client.</li>
</ul>
<hr />
<h2>Measurable Impact</h2>
<p>On content‑heavy pages, TransferState typically removes 1–3 duplicate API calls during hydration, shaving 100–500 ms from the critical path on average networks. It’s a low‑effort, high‑impact win for SSR apps.</p>
<hr />
<h2>Conclusion</h2>
<p>If you already have SSR, enabling TransferState is one of the easiest ways to make hydration feel instant. You can use it built‑in HTTP caching or manually control what to cache. Either way, it eliminates redundant data fetching, speeds up Time‑to‑Interactive, and improves user experience with minimal effort.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1cfe4d-7aea-ccea-d71a-9bc3dab5d350" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1cfe4d-7aea-ccea-d71a-9bc3dab5d350" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/building-dynamic-forms-in-angular-for-enterprise-applications-6r3ewpxt</guid>
      <link>https://abp.io/community/posts/building-dynamic-forms-in-angular-for-enterprise-applications-6r3ewpxt</link>
      <a10:author>
        <a10:name>erdem.caygor</a10:name>
        <a10:uri>https://abp.io/community/members/erdem.caygor</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>application-development</category>
      <category>dynamic-form-extensions</category>
      <category>customization</category>
      <title>Building Dynamic Forms in Angular for Enterprise Applications</title>
      <description>This article explains how to create and use dynamic forms in Angular applications. It covers how to define configuration models and build reusable components that render and manage forms at runtime.

By using a configuration-based approach, each form field is defined through simple metadata such as type, label, and validation rules. This removes the need for hardcoded forms and makes the system more flexible, reusable, and easier to maintain.</description>
      <pubDate>Wed, 08 Oct 2025 06:31:30 Z</pubDate>
      <a10:updated>2026-03-11T08:35:27Z</a10:updated>
      <content:encoded><![CDATA[<h1>Building Dynamic Forms in Angular for Enterprise Applications</h1>
<h2>Introduction</h2>
<p>Dynamic forms are useful for enterprise applications where form structures need to be flexible, configurable, and generated at runtime based on business requirements. This approach allows developers to create forms from configuration objects rather than hardcoding them, enabling greater flexibility and maintainability.</p>
<h2>Benefits</h2>
<ol>
<li><strong>Flexibility</strong>: Forms can be easily modified without changing the code.</li>
<li><strong>Reusability</strong>: Form components can be shared across components.</li>
<li><strong>Maintainability</strong>: Changes to form structures can be managed through configuration files or databases.</li>
<li><strong>Scalability</strong>: New form fields and types can be added without significant code changes.</li>
<li><strong>User Experience</strong>: Dynamic forms can adapt to user roles and permissions, providing a tailored experience.</li>
</ol>
<h2>Architecture</h2>
<h3>1. Defining Form Configuration Models</h3>
<p>We will define form configuration model as a first step. This models stores field types, labels, validation rules, and other metadata.</p>
<h4>1.1. Form Field Configuration</h4>
<p>Form field configuration interface represents individual form fields and contains properties like type, label, validation rules and conditional logic.</p>
<pre><code class="language-typescript">export interface FormFieldConfig {
    key: string;
    value?: any;
    type: 'text' | 'email' | 'number' | 'select' | 'checkbox' | 'date' | 'textarea';
    label: string;
    placeholder?: string;
    required?: boolean;
    disabled?: boolean;
    options?: { key: string; value: any }[]; 
    validators?: ValidatorConfig[]; // Custom validators
    conditionalLogic?: ConditionalRule[]; // For showing/hiding fields based on other field values
    order?: number; // For ordering fields in the form
    gridSize?: number; // For layout purposes, e.g., Bootstrap grid size (1-12)
}
</code></pre>
<h4>1.2. Validator Configuration</h4>
<p>Validator configuration interface defines validation rules for form fields.</p>
<pre><code class="language-typescript">export interface ValidatorConfig {
    type: 'required' | 'email' | 'minLength' | 'maxLength' | 'pattern' | 'custom';
    value?: any;
    message: string;
}
</code></pre>
<h4>1.3. Conditional Logic</h4>
<p>Conditional logic interface defines rules for showing/hiding or enabling/disabling fields based on other field values.</p>
<pre><code class="language-typescript">export interface ConditionalRule {
    dependsOn: string;
    condition: 'equals' | 'notEquals' | 'contains' | 'greaterThan' | 'lessThan';
    value: any;
    action: 'show' | 'hide' | 'enable' | 'disable';
}
</code></pre>
<h3>2. Dynamic Form Service</h3>
<p>We will create dynamic form service to handle form creation and validation processes.</p>
<pre><code class="language-typescript">@Injectable({
    providedIn: 'root'
})
export class DynamicFormService {

    // Create form group based on fields
    createFormGroup(fields: FormFieldConfig[]): FormGroup {
        const group: any = {};

        fields.forEach(field =&gt; {
            const validators = this.buildValidators(field.validators || []);
            const initialValue = this.getInitialValue(field);

            group[field.key] = new FormControl({
                value: initialValue,
                disabled: field.disabled || false
            }, validators);
        });

        return new FormGroup(group);
    }

    // Returns an array of form field validators based on the validator configurations
    private buildValidators(validatorConfigs: ValidatorConfig[]): ValidatorFn[] {
        return validatorConfigs.map(config =&gt; {
            switch (config.type) {
                case 'required':
                    return Validators.required;
                case 'email':
                    return Validators.email;
                case 'minLength':
                    return Validators.minLength(config.value);
                case 'maxLength':
                    return Validators.maxLength(config.value);
                case 'pattern':
                    return Validators.pattern(config.value);
                default:
                    return Validators.nullValidator;
            }
        });
    }

    private getInitialValue(field: FormFieldConfig): any {
        switch (field.type) {
            case 'checkbox':
                return false;
            case 'number':
                return 0;
            default:
                return '';
        }
    }
}

</code></pre>
<h3>3. Dynamic Form Component</h3>
<p>The main component that renders the form based on the configuration it receives as input.</p>
<pre><code class="language-typescript">@Component({
    selector: 'app-dynamic-form',
    template: `
    &lt;form [formGroup]=&quot;dynamicForm&quot; (ngSubmit)=&quot;onSubmit()&quot; class=&quot;dynamic-form&quot;&gt;
      @for (field of sortedFields; track field.key) {
        &lt;div class=&quot;row&quot;&gt;
          &lt;div [ngClass]=&quot;'col-md-' + (field.gridSize || 12)&quot;&gt;
            &lt;app-dynamic-form-field
              [field]=&quot;field&quot;
              [form]=&quot;dynamicForm&quot;
              [isVisible]=&quot;isFieldVisible(field)&quot;
              (fieldChange)=&quot;onFieldChange($event)&quot;&gt;
            &lt;/app-dynamic-form-field&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      }
      &lt;div class=&quot;form-actions&quot;&gt;
        &lt;button
          type=&quot;button&quot;
          class=&quot;btn btn-secondary&quot;
          (click)=&quot;onCancel()&quot;&gt;
          Cancel
        &lt;/button&gt;
        &lt;button
          type=&quot;submit&quot;
          class=&quot;btn btn-primary&quot;
          [disabled]=&quot;!dynamicForm.valid || isSubmitting&quot;&gt;
          {{ submitButtonText() }}
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  `,
    styles: [`
    .dynamic-form {
      display: flex;
      gap: 0.5rem;
      flex-direction: column;
    }
    .form-actions {
      display: flex;
      justify-content: flex-end;
      gap: 0.5rem;
    }
  `],
    imports: [ReactiveFormsModule, CommonModule, DynamicFormFieldComponent],
})
export class DynamicFormComponent implements OnInit {
    fields = input&lt;FormFieldConfig[]&gt;([]);
    submitButtonText = input&lt;string&gt;('Submit');
    formSubmit = output&lt;any&gt;();
    formCancel = output&lt;void&gt;();
    private dynamicFormService = inject(DynamicFormService);

    dynamicForm!: FormGroup;
    isSubmitting = false;
    fieldVisibility: { [key: string]: boolean } = {};

    ngOnInit() {
        this.dynamicForm = this.dynamicFormService.createFormGroup(this.fields());
        this.initializeFieldVisibility();
        this.setupConditionalLogic();
    }

    get sortedFields(): FormFieldConfig[] {
        return this.fields().sort((a, b) =&gt; (a.order || 0) - (b.order || 0));
    }

    onSubmit() {
        if (this.dynamicForm.valid) {
            this.isSubmitting = true;
            this.formSubmit.emit(this.dynamicForm.value);
        } else {
            this.markAllFieldsAsTouched();
        }
    }

    onCancel() {
        this.formCancel.emit();
    }

    onFieldChange(event: { fieldKey: string; value: any }) {
        this.evaluateConditionalLogic(event.fieldKey);
    }

    isFieldVisible(field: FormFieldConfig): boolean {
        return this.fieldVisibility[field.key] !== false;
    }

    private initializeFieldVisibility() {
        this.fields().forEach(field =&gt; {
            this.fieldVisibility[field.key] = !field.conditionalLogic?.length;
        });
    }

    private setupConditionalLogic() {
        this.fields().forEach(field =&gt; {
            if (field.conditionalLogic) {
                field.conditionalLogic.forEach(rule =&gt; {
                    const dependentControl = this.dynamicForm.get(rule.dependsOn);
                    if (dependentControl) {
                        dependentControl.valueChanges.subscribe(() =&gt; {
                            this.evaluateConditionalLogic(field.key);
                        });
                    }
                });
            }
        });
    }

    private evaluateConditionalLogic(fieldKey: string) {
        const field = this.fields().find(f =&gt; f.key === fieldKey);
        if (!field?.conditionalLogic) return;

        field.conditionalLogic.forEach(rule =&gt; {
            const dependentValue = this.dynamicForm.get(rule.dependsOn)?.value;
            const conditionMet = this.evaluateCondition(dependentValue, rule.condition, rule.value);

            this.applyConditionalAction(fieldKey, rule.action, conditionMet);
        });
    }

    private evaluateCondition(fieldValue: any, condition: string, ruleValue: any): boolean {
        switch (condition) {
            case 'equals':
                return fieldValue === ruleValue;
            case 'notEquals':
                return fieldValue !== ruleValue;
            case 'contains':
                return fieldValue &amp;&amp; fieldValue.includes &amp;&amp; fieldValue.includes(ruleValue);
            case 'greaterThan':
                return Number(fieldValue) &gt; Number(ruleValue);
            case 'lessThan':
                return Number(fieldValue) &lt; Number(ruleValue);
            default:
                return false;
        }
    }

    private applyConditionalAction(fieldKey: string, action: string, shouldApply: boolean) {
        const control = this.dynamicForm.get(fieldKey);

        switch (action) {
            case 'show':
                this.fieldVisibility[fieldKey] = shouldApply;
                break;
            case 'hide':
                this.fieldVisibility[fieldKey] = !shouldApply;
                break;
            case 'enable':
                if (control) {
                    shouldApply ? control.enable() : control.disable();
                }
                break;
            case 'disable':
                if (control) {
                    shouldApply ? control.disable() : control.enable();
                }
                break;
        }
    }

    private markAllFieldsAsTouched() {
        Object.keys(this.dynamicForm.controls).forEach(key =&gt; {
            this.dynamicForm.get(key)?.markAsTouched();
        });
    }
}
</code></pre>
<h3>4. Dynamic Form Field Component</h3>
<p>This component renders individual form fields, handling different types and validation messages based on the configuration.</p>
<pre><code class="language-typescript">@Component({
    selector: 'app-dynamic-form-field',
    template: `
    @if (isVisible) {
      &lt;div class=&quot;field-container&quot; [formGroup]=&quot;form&quot;&gt;

        @if (field.type === 'text') {
          &lt;!-- Text Input --&gt;
          &lt;div class=&quot;form-group&quot;&gt;
            &lt;label [for]=&quot;field.key&quot;&gt;{{ field.label }}&lt;/label&gt;
            &lt;input
              [id]=&quot;field.key&quot;
              [formControlName]=&quot;field.key&quot;
              [placeholder]=&quot;field.placeholder || ''&quot;
              class=&quot;form-control&quot;
              [class.is-invalid]=&quot;isFieldInvalid()&quot;&gt;
            @if (isFieldInvalid()) {
              &lt;div class=&quot;invalid-feedback&quot;&gt;
                {{ getErrorMessage() }}
              &lt;/div&gt;
            }
          &lt;/div&gt;
        } @else if (field.type === 'select') {
          &lt;!-- Select Dropdown --&gt;
          &lt;div class=&quot;form-group&quot;&gt;
            &lt;label [for]=&quot;field.key&quot;&gt;{{ field.label }}&lt;/label&gt;
            &lt;select
              [id]=&quot;field.key&quot;
              [formControlName]=&quot;field.key&quot;
              class=&quot;form-control&quot;
              [class.is-invalid]=&quot;isFieldInvalid()&quot;&gt;
              &lt;option value=&quot;&quot;&gt;Please select...&lt;/option&gt;
              @for (option of field.options; track option.key) {
                &lt;option
                  [value]=&quot;option.key&quot;&gt;
                  {{ option.value }}
                &lt;/option&gt;
              }
            &lt;/select&gt;
            @if (isFieldInvalid()) {
              &lt;div class=&quot;invalid-feedback&quot;&gt;
                {{ getErrorMessage() }}
              &lt;/div&gt;
            }
          &lt;/div&gt;
        } @else if (field.type === 'checkbox') {
          &lt;!-- Checkbox --&gt;
          &lt;div class=&quot;form-group form-check&quot;&gt;
            &lt;input
              type=&quot;checkbox&quot;
              [id]=&quot;field.key&quot;
              [formControlName]=&quot;field.key&quot;
              class=&quot;form-check-input&quot;
              [class.is-invalid]=&quot;isFieldInvalid()&quot;&gt;
            &lt;label class=&quot;form-check-label&quot; [for]=&quot;field.key&quot;&gt;
              {{ field.label }}
            &lt;/label&gt;
            @if (isFieldInvalid()) {
              &lt;div class=&quot;invalid-feedback&quot;&gt;
                {{ getErrorMessage() }}
              &lt;/div&gt;
            }
          &lt;/div&gt;
        } @else if (field.type === 'email') {
          &lt;!-- Email Input --&gt;
          &lt;div class=&quot;form-group&quot;&gt;
            &lt;label [for]=&quot;field.key&quot;&gt;{{ field.label }}&lt;/label&gt;
            &lt;input
              type=&quot;email&quot;
              [id]=&quot;field.key&quot;
              [formControlName]=&quot;field.key&quot;
              [placeholder]=&quot;field.placeholder || ''&quot;
              class=&quot;form-control&quot;
              [class.is-invalid]=&quot;isFieldInvalid()&quot;&gt;
          @if (isFieldInvalid()) {
            &lt;div class=&quot;invalid-feedback&quot;&gt;
              {{ getErrorMessage() }}
            &lt;/div&gt;
          }
          &lt;/div&gt;
        } @else if (field.type === 'textarea') {
          &lt;!-- Textarea --&gt;
          &lt;div class=&quot;form-group&quot;&gt;
            &lt;label [for]=&quot;field.key&quot;&gt;{{ field.label }}&lt;/label&gt;
            &lt;textarea
              [id]=&quot;field.key&quot;
              [formControlName]=&quot;field.key&quot;
              [placeholder]=&quot;field.placeholder || ''&quot;
              rows=&quot;4&quot;
              class=&quot;form-control&quot;
              [class.is-invalid]=&quot;isFieldInvalid()&quot;&gt;
        &lt;/textarea&gt;
          @if (isFieldInvalid()) {
            &lt;div class=&quot;invalid-feedback&quot;&gt;
              {{ getErrorMessage() }}
            &lt;/div&gt;
          }
          &lt;/div&gt;
        }
      &lt;/div&gt;
&lt;!--      Add more field types as needed--&gt;
    }
  `,
    imports: [ReactiveFormsModule],
})
export class DynamicFormFieldComponent implements OnInit {
    @Input() field!: FormFieldConfig;
    @Input() form!: FormGroup;
    @Input() isVisible: boolean = true;
    @Output() fieldChange = new EventEmitter&lt;{ fieldKey: string; value: any }&gt;();

    ngOnInit() {
        const control = this.form.get(this.field.key);
        if (control) {
            control.valueChanges.subscribe(value =&gt; {
                this.fieldChange.emit({ fieldKey: this.field.key, value });
            });
        }
    }

    isFieldInvalid(): boolean {
        const control = this.form.get(this.field.key);
        return !!(control &amp;&amp; control.invalid &amp;&amp; (control.dirty || control.touched));
    }

    getErrorMessage(): string {
        const control = this.form.get(this.field.key);
        if (!control || !control.errors) return '';

        const validators = this.field.validators || [];

        for (const validator of validators) {
            if (control.errors[validator.type]) {
                return validator.message;
            }
        }

        // Fallback error messages
        if (control.errors['required']) return `${this.field.label} is required`;
        if (control.errors['email']) return 'Please enter a valid email address';
        if (control.errors['minlength']) return `Minimum length is ${control.errors['minlength'].requiredLength}`;
        if (control.errors['maxlength']) return `Maximum length is ${control.errors['maxlength'].requiredLength}`;

        return 'Invalid input';
    }
}

</code></pre>
<h3>5. Usage Example</h3>
<pre><code class="language-typescript">
@Component({
    selector: 'app-home',
    template: `
    &lt;div class=&quot;row&quot;&gt;
      &lt;div class=&quot;col-4 offset-4&quot;&gt;
        &lt;app-dynamic-form
          [fields]=&quot;formFields&quot;
          submitButtonText=&quot;Save User&quot;
          (formSubmit)=&quot;onSubmit($event)&quot;
          (formCancel)=&quot;onCancel()&quot;&gt;
        &lt;/app-dynamic-form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  `,
    imports: [DynamicFormComponent]
})
export class HomeComponent {
    @Input() title: string = 'Home Component';
    formFields: FormFieldConfig[] = [
        {
            key: 'firstName',
            type: 'text',
            label: 'First Name',
            placeholder: 'Enter first name',
            required: true,
            validators: [
                { type: 'required', message: 'First name is required' },
                { type: 'minLength', value: 2, message: 'Minimum 2 characters required' }
            ],
            gridSize: 12,
            order: 1
        },
        {
            key: 'lastName',
            type: 'text',
            label: 'Last Name',
            placeholder: 'Enter last name',
            required: true,
            validators: [
                { type: 'required', message: 'Last name is required' }
            ],
            gridSize: 12,
            order: 2
        },
        {
            key: 'email',
            type: 'email',
            label: 'Email Address',
            placeholder: 'Enter email',
            required: true,
            validators: [
                { type: 'required', message: 'Email is required' },
                { type: 'email', message: 'Please enter a valid email' }
            ],
            order: 3
        },
        {
            key: 'userType',
            type: 'select',
            label: 'User Type',
            required: true,
            options: [
                { key: 'admin', value: 'Administrator' },
                { key: 'user', value: 'Regular User' },
                { key: 'guest', value: 'Guest User' }
            ],
            validators: [
                { type: 'required', message: 'Please select user type' }
            ],
            order: 4
        },
        {
            key: 'adminNotes',
            type: 'textarea',
            label: 'Admin Notes',
            placeholder: 'Enter admin-specific notes',
            conditionalLogic: [
                {
                    dependsOn: 'userType',
                    condition: 'equals',
                    value: 'admin',
                    action: 'show'
                }
            ],
            order: 5
        }
    ];

    onSubmit(formData: any) {
        console.log('Form submitted:', formData);
        // Handle form submission
    }

    onCancel() {
        console.log('Form cancelled');
        // Handle form cancellation
    }
}


</code></pre>
<h2>Result</h2>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-10-06-Building-Dynamic-Forms-in-Angular-for-Enterprise-Applications/form.png" alt="example_form" /></p>
<h2>Conclusion</h2>
<p>These kinds of components are essential for large applications because they allow for rapid development and easy maintenance. By defining forms through configuration, developers can quickly adapt to changing requirements without extensive code changes. This approach also promotes consistency across the application, as the same form components can be reused in different contexts.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1cd4b1-63ad-b534-e0a1-098f672f60ce" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1cd4b1-63ad-b534-e0a1-098f672f60ce" medium="image" />
    </item>
  </channel>
</rss>