FormGroup in Angular Reactive Forms

FormGroup in Angular Reactive Forms with a Real-time Application

A FormGroup is the best way to build real-world Reactive Forms because it lets you split a large form into clean, meaningful sections (like Business, KYC, Bank, Address). Each section behaves like a mini-form, so you can validate, reset, enable/disable, and show errors section-wise, which keeps your code readable and your UI easy to manage as the form grows. In real-time applications, forms are never flat. They are always divided into logical sections, such as:

  • Personal Details
  • Account Settings
  • Address Details
  • Security Information

Angular’s FormGroup exists specifically to model these sections.

Why is FormGroup used for Sections?

A FormGroup is not just a container. It allows us to:

  • Treat a section as one unit
  • Apply section-level validation
  • Enable/disable/reset an entire section
  • Keep the form structured and readable

Think of a FormGroup as a sub-form inside the main form.

Amazon-Style Seller Registration Using FormGroup Sections

In an Amazon-like seller onboarding flow, the form is never a single big block. It’s split into logical sections like Business Details, KYC/Identity, Bank & Payout, and Pickup AddressEach section is a nested FormGroup, so it can be validated, saved as a draft, reset, and shown with section-level errors independently.

Real-time Section Design (FormGroups used as sections)
  • Business section: Seller type, legal name, brand name, GSTIN (conditional)
  • KYC section: PAN + Aadhaar last-4 + “Name match” section rule
  • Bank section: Account no + confirm + IFSC rule (cross-field)
  • Pickup section: Address + Pin Code → City/State auto-rule demonstration (simple)
Create an Angular Project

Run the following commands:

  • ng new SellerRegistration
  • cd SellerRegistration
Adding Bootstrap CDN

Open the src/index.html file, and copy-paste the following code.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Seller Registration </title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <app-root></app-root>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Modify the Root Component:

Open the src/app/app.ts file, and copy-paste the following code.

import { Component } from '@angular/core';                   
import { CommonModule } from '@angular/common';              
import {
  ReactiveFormsModule,        // Enables Reactive Forms directives: [formGroup], formControlName, formGroupName etc.
  FormBuilder,                // Helper service to build FormGroup objects with clean syntax
  FormGroup,                  // Represents a group of related form controls (perfect for sections)
  Validators,                 // Built-in validators: required, minlength, pattern, etc.
  AbstractControl,            // Base type for FormControl/FormGroup/FormArray (used in custom validators)
  ValidationErrors,           // Standard type for returning error objects from custom validators
  ValidatorFn                 // Type alias for a custom validator function
} from '@angular/forms';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {

  // Parent Form: Seller Registration 
  // This parent form holds 4 "sections", each represented as a nested FormGroup.
  // Why nested groups?
  // - Cleaner structure: each section is independent
  // - Easy validation: section-wise validation messages
  // - Easy reset: reset one section without touching others
  sellerRegistrationForm: FormGroup;

  // Stores the final submitted data (for showing JSON preview after submit)
  submittedSellerData: any = null;

  // Dropdown values (in real projects, these usually come from an API)
  businessTypeOptions = ['Individual', 'Proprietorship', 'Company'] as const;
  categoryOptions = ['Electronics', 'Fashion', 'Home & Kitchen', 'Books', 'Grocery'] as const;

  constructor(private formBuilder: FormBuilder) {

    // Build parent form with nested FormGroups (sections)
    this.sellerRegistrationForm = this.formBuilder.group({

      // SECTION 1: BUSINESS DETAILS
      businessDetails: this.formBuilder.group(
        {
          // Drives GSTIN requirement: Company/Proprietorship must provide GSTIN
          businessType: ['Individual', Validators.required],

          // Legal name as per PAN/GST/Company registration
          legalBusinessName: ['', [Validators.required, Validators.minLength(3)]],

          // Store name shown to customers (branding)
          storeDisplayName: ['', [Validators.required, Validators.minLength(3)]],

          // Seller category selection
          sellingCategory: ['', Validators.required],

          // GSTIN is conditionally required (validators will be added dynamically)
          gstin: ['']
        },
        {
          // Section-level validation = rules that require multiple fields together
          validators: [this.businessDetailsSectionValidator()]
        }
      ),

      // SECTION 2: KYC DETAILS
      kycDetails: this.formBuilder.group(
        {
          // PAN format in India: ABCDE1234F
          panNumber: ['', [Validators.required, Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]],

          // Last 4 digits for basic verification scenario
          aadhaarLast4Digits: ['', [Validators.required, Validators.pattern(/^\d{4}$/)]],

          // Mandatory consent checkbox (must be checked)
          isKycConsentGiven: [false, Validators.requiredTrue]
        },
        {
          validators: [this.kycDetailsSectionValidator()]
        }
      ),

      // SECTION 3: BANK / PAYOUT DETAILS
      bankPayoutDetails: this.formBuilder.group(
        {
          // Account name should match bank passbook
          bankAccountHolderName: ['', [Validators.required, Validators.minLength(3)]],

          // Account number length varies by banks; keep a practical range
          bankAccountNumber: ['', [Validators.required, Validators.pattern(/^\d{9,18}$/)]],

          // Confirm account number to avoid payout failures due to wrong entry
          confirmBankAccountNumber: ['', [Validators.required, Validators.pattern(/^\d{9,18}$/)]],

          // IFSC format: HDFC0001234
          ifscCode: ['', [Validators.required, Validators.pattern(/^[A-Z]{4}0[A-Z0-9]{6}$/)]]
        },
        {
          validators: [this.bankPayoutSectionValidator()]
        }
      ),

      // SECTION 4: PICKUP ADDRESS
      pickupAddressDetails: this.formBuilder.group(
        {
          pickupAddressLine1: ['', [Validators.required, Validators.minLength(5)]],
          pickupCity: ['', [Validators.required, Validators.minLength(2)]],
          pickupState: ['', [Validators.required, Validators.minLength(2)]],
          pickupPincode: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]],

          // Courier pickup consent (required)
          isPickupConsentGiven: [false, Validators.requiredTrue]
        },
        {
          validators: [this.pickupAddressSectionValidator()]
        }
      )
    });

    // Apply dynamic validation rules (GSTIN required/optional based on business type)
    this.applyBusinessTypeBasedGstinRules(); 
  }

  
  // Section Getters (cleaner code in template and component)
  get businessDetailsGroup(): FormGroup {
    return this.sellerRegistrationForm.get('businessDetails') as FormGroup;
  }

  get kycDetailsGroup(): FormGroup {
    return this.sellerRegistrationForm.get('kycDetails') as FormGroup;
  }

  get bankPayoutDetailsGroup(): FormGroup {
    return this.sellerRegistrationForm.get('bankPayoutDetails') as FormGroup;
  }
  
  get pickupAddressDetailsGroup(): FormGroup {
    return this.sellerRegistrationForm.get('pickupAddressDetails') as FormGroup;
  }

  // Dynamic Validation: Make GSTIN required ONLY for certain Business Types
  // Real marketplace onboarding rule:
  // - Individual sellers: GSTIN may be optional
  // - Proprietorship / Company sellers: GSTIN is mandatory and must be valid
  //
  // Why do we need this method?
  // - A section validator can only "report" errors at the group level.
  // - Here we must actually CHANGE the GSTIN field rules (Validators.required / pattern)
  //   whenever the user changes the Business Type dropdown.
  private applyBusinessTypeBasedGstinRules(): void {

    // We need two controls:
    // 1) businessTypeControl  -> the "driver" control (dropdown)
    // 2) gstinControl         -> the "dependent" control (validators change)
    const businessTypeControl = this.businessDetailsGroup.get('businessType');
    const gstinControl = this.businessDetailsGroup.get('gstin');

    // SAFETY CHECK
    // In your current form, these controls exist, so this will almost never fail. 
    // But this check protects you in real projects when:
    // - someone renames 'gstin' to something else but forgets to update get('gstin')
    // Without this check, calling gstinControl.clearValidators() would throw
    // "Cannot read properties of null" and crash the page.
    if (!businessTypeControl || !gstinControl) {
      return; // Exit early to avoid runtime errors
    }

    // SUBSCRIBE to business type changes
    // valueChanges emits the latest selected value whenever user changes the dropdown (Individual / Proprietorship / Company).
    // Each time it emits, we update GSTIN validators.
    businessTypeControl.valueChanges.subscribe((selectedBusinessType: string) => {

      // 1) Remove old validators first.
      //    This is required because user may switch like:
      //    Company -> Individual -> Company.
      //    If we don't clear, GSTIN may remain "required" even for Individual.
      gstinControl.clearValidators();

      // 2) Add validators ONLY when GSTIN should be mandatory.
      const isGstinMandatory =
        selectedBusinessType === 'Proprietorship' || selectedBusinessType === 'Company';

      if (isGstinMandatory) {
        gstinControl.setValidators([
          Validators.required, // GSTIN must be entered
          // GSTIN structural format check (example: 22AAAAA0000A1Z5)
          Validators.pattern(/^\d{2}[A-Z]{5}\d{4}[A-Z]{1}[A-Z\d]{1}Z[A-Z\d]{1}$/)
        ]);
      }

      // 3) Recalculate GSTIN validation after changing validators.
      //    This updates:
      //    - gstinControl.valid / invalid
      //    - gstinControl.errors
      //    - UI messages and is-invalid class bindings
      //
      // emitEvent:false prevents unnecessary extra events / loops.
      gstinControl.updateValueAndValidity({ emitEvent: false });
    });

    // INITIAL RUN (important)
    // On page load, businessType already has a default value ("Individual").
    // But the subscription runs only when the value changes.
    // So we call our validator update once immediately to ensure GSTIN rules
    // match the default business type at startup.
    gstinControl.updateValueAndValidity({ emitEvent: false });
  }

  // Submit: Final step of registration
  onSubmit(): void {
    this.submittedSellerData = null;

    // If invalid, mark all fields touched so Bootstrap validation messages appear
    if (this.sellerRegistrationForm.invalid) {
      this.sellerRegistrationForm.markAllAsTouched();
      return;
    }

    // Real app: send payload to API
    this.submittedSellerData = this.sellerRegistrationForm.value;
    console.log(this.submittedSellerData);
  }

  // Reset only one section (real onboarding convenience feature)
  resetSection(sectionName: 'businessDetails' | 'kycDetails' | 'bankPayoutDetails' | 'pickupAddressDetails'): void {

    (this.sellerRegistrationForm.get(sectionName) as FormGroup).reset();

    // Restore defaults when resetting Business section (so GST rule is applied correctly)
    if (sectionName === 'businessDetails') {
      // patchValue will mark the default value of businessType as Individual
      this.businessDetailsGroup.patchValue({ businessType: 'Individual', sellingCategory: '' });
    }
  }

  // SECTION VALIDATOR 1: BUSINESS DETAILS (cross-field rules)
  private businessDetailsSectionValidator(): ValidatorFn {
    return (sectionGroup: AbstractControl): ValidationErrors | null => {

      const legalBusinessName = sectionGroup.get('legalBusinessName')?.value?.toString().trim().toLowerCase() ?? '';
      const storeDisplayName = sectionGroup.get('storeDisplayName')?.value?.toString().trim().toLowerCase() ?? '';
      const businessType = sectionGroup.get('businessType')?.value?.toString().trim() ?? '';

      // Branding rule: store display name must be different from legal name
      if (legalBusinessName && storeDisplayName && legalBusinessName === storeDisplayName) {
        return { storeNameShouldDifferFromLegalName: true };
      }

      // Company naming hint: encourage registered company naming style
      if (businessType === 'Company' && legalBusinessName) {
        const looksLikeRegisteredCompany =
          legalBusinessName.includes('pvt') ||
          legalBusinessName.includes('ltd') ||
          legalBusinessName.includes('llp') ||
          legalBusinessName.includes('limited');

        if (!looksLikeRegisteredCompany) {
          return { companyNameMustLookRegistered: true };
        }
      }

      return null;
    };
  }

  // SECTION VALIDATOR 2: KYC DETAILS
  private kycDetailsSectionValidator(): ValidatorFn {
    return (sectionGroup: AbstractControl): ValidationErrors | null => {

      const panNumber = sectionGroup.get('panNumber')?.value?.toString().trim().toUpperCase() ?? '';
      const isKycConsentGiven = sectionGroup.get('isKycConsentGiven')?.value === true;

      // Rule: Consent should not be considered valid if PAN is not complete
      if (isKycConsentGiven && panNumber.length !== 10) {
        return { panMustBeValidBeforeConsent: true };
      }

      return null;
    };
  }

  // SECTION VALIDATOR 3: BANK PAYOUT DETAILS
  private bankPayoutSectionValidator(): ValidatorFn {
    return (sectionGroup: AbstractControl): ValidationErrors | null => {

      const bankAccountNumber = sectionGroup.get('bankAccountNumber')?.value?.toString().trim() ?? '';
      const confirmBankAccountNumber = sectionGroup.get('confirmBankAccountNumber')?.value?.toString().trim() ?? '';

      // Rule: Account number must match confirm account number
      if (bankAccountNumber && confirmBankAccountNumber && bankAccountNumber !== confirmBankAccountNumber) {
        return { bankAccountNumbersDoNotMatch: true };
      }

      return null;
    };
  }

  // SECTION VALIDATOR 4: PICKUP ADDRESS
  private pickupAddressSectionValidator(): ValidatorFn {
    return (sectionGroup: AbstractControl): ValidationErrors | null => {

      const pickupPincode = sectionGroup.get('pickupPincode')?.value?.toString().trim() ?? '';
      const pickupState = sectionGroup.get('pickupState')?.value?.toString().trim().toLowerCase() ?? '';

      // Demo realistic rule: Odisha pincodes generally start with 75 or 76
      const looksLikeOdishaPincode = pickupPincode.startsWith('75') || pickupPincode.startsWith('76');

      if (pickupPincode.length === 6 && looksLikeOdishaPincode && pickupState && pickupState !== 'odisha') {
        return { pickupPincodeStateMismatch: true };
      }

      return null;
    };
  }

  //Reset all
  resetAll(): void {
    // Reset the full form AND set the default Business Type back to Individual
    this.sellerRegistrationForm.reset({
      businessDetails: {
        businessType: 'Individual',
        sellingCategory: '' 
      }
    });

    // Clear the submitted JSON preview too
    this.submittedSellerData = null;
  }
}
Modify Root Template

Open the src/app/app.html file, and copy-paste the following code.

<div class="container py-4">

  <!-- PAGE HEADER -->
  <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
    <div>
      <h2 class="fw-bold mb-1">Seller Registration</h2>
    </div>

    <!-- Desktop buttons (bind to form using form="sellerRegistrationForm") -->
    <div class="d-none d-lg-flex gap-2">
      <button type="submit" form="sellerRegistrationForm" class="btn btn-success" [disabled]="sellerRegistrationForm.invalid">
        Submit Registration
      </button>
      <button type="button" class="btn btn-outline-primary" (click)="resetAll()">
        Reset All
      </button>
    </div>
  </div>

  <form id="sellerRegistrationForm" [formGroup]="sellerRegistrationForm" (ngSubmit)="onSubmit()" novalidate>
    <div class="row g-3 align-items-start">

      <div class="col-12 col-lg-8">

        <!-- SECTION 1: BUSINESS DETAILS -->
        <div class="card shadow-sm mb-3">
          <div class="card-header bg-white d-flex justify-content-between align-items-center">
            <span class="fw-semibold">Business Details</span>
            <button type="button" class="btn btn-outline-primary btn-sm" (click)="resetSection('businessDetails')">
              Reset Section
            </button>
          </div>

          <div class="card-body" formGroupName="businessDetails">

            <!-- Section-level messages -->
            @if (businessDetailsGroup.touched && businessDetailsGroup.errors?.['storeNameShouldDifferFromLegalName']) {
              <div class="alert alert-warning py-2 mb-3">
                Store Display Name should be different from Legal Name.
              </div>
            }

            @if (businessDetailsGroup.touched && businessDetailsGroup.errors?.['companyNameMustLookRegistered']) {
              <div class="alert alert-warning py-2 mb-3">
                For Company type, Legal Name should include something like <b>Pvt</b>, <b>Ltd</b>, or <b>LLP</b>.
              </div>
            }

            <div class="row g-3">
              <div class="col-12 col-md-6">
                <label class="form-label">Business Type</label>
                <select class="form-select" formControlName="businessType"
                        [class.is-invalid]="businessDetailsGroup.get('businessType')?.touched && businessDetailsGroup.get('businessType')?.invalid">
                  @for (type of businessTypeOptions; track type) {
                    <option [value]="type">{{ type }}</option>
                  }
                </select>
                @if (businessDetailsGroup.get('businessType')?.touched && businessDetailsGroup.get('businessType')?.invalid) {
                  <div class="invalid-feedback d-block">Business Type is required.</div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">Category</label>
                <select class="form-select" formControlName="sellingCategory"
                        [class.is-invalid]="businessDetailsGroup.get('sellingCategory')?.touched && businessDetailsGroup.get('sellingCategory')?.invalid">
                  <option value="">-- Select --</option>
                  @for (category of categoryOptions; track category) {
                    <option [value]="category">{{ category }}</option>
                  }
                </select>
                @if (businessDetailsGroup.get('sellingCategory')?.touched && businessDetailsGroup.get('sellingCategory')?.invalid) {
                  <div class="invalid-feedback d-block">Category is required.</div>
                }
              </div>

              <div class="col-12">
                <label class="form-label">Legal Name</label>
                <input class="form-control" formControlName="legalBusinessName"
                       placeholder="As per PAN/GST/Company registration"
                       [class.is-invalid]="businessDetailsGroup.get('legalBusinessName')?.touched && businessDetailsGroup.get('legalBusinessName')?.invalid" />
                @if (businessDetailsGroup.get('legalBusinessName')?.touched && businessDetailsGroup.get('legalBusinessName')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (businessDetailsGroup.get('legalBusinessName')?.errors?.['required']) { <div>Legal Name is required.</div> }
                    @if (businessDetailsGroup.get('legalBusinessName')?.errors?.['minlength']) { <div>Minimum 3 characters.</div> }
                  </div>
                }
              </div>

              <div class="col-12">
                <label class="form-label">Store Display Name</label>
                <input class="form-control" formControlName="storeDisplayName"
                       placeholder="This name will be visible to customers"
                       [class.is-invalid]="businessDetailsGroup.get('storeDisplayName')?.touched && businessDetailsGroup.get('storeDisplayName')?.invalid" />
                @if (businessDetailsGroup.get('storeDisplayName')?.touched && businessDetailsGroup.get('storeDisplayName')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (businessDetailsGroup.get('storeDisplayName')?.errors?.['required']) { <div>Store Display Name is required.</div> }
                    @if (businessDetailsGroup.get('storeDisplayName')?.errors?.['minlength']) { <div>Minimum 3 characters.</div> }
                  </div>
                }
              </div>

              <div class="col-12">
                <label class="form-label">
                  GSTIN <span class="text-muted">(Required for Proprietorship/Company)</span>
                </label>
                <input class="form-control" formControlName="gstin" placeholder="22AAAAA0000A1Z5"
                       [class.is-invalid]="businessDetailsGroup.get('gstin')?.touched && businessDetailsGroup.get('gstin')?.invalid" />
                @if (businessDetailsGroup.get('gstin')?.touched && businessDetailsGroup.get('gstin')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (businessDetailsGroup.get('gstin')?.errors?.['required']) { <div>GSTIN is required for this Business Type.</div> }
                    @if (businessDetailsGroup.get('gstin')?.errors?.['pattern']) { <div>Enter a valid GSTIN format.</div> }
                  </div>
                }

                <div class="form-text">
                  GSTIN becomes mandatory based on selected Business Type (this is a common marketplace rule).
                </div>
              </div>
            </div>

          </div>
        </div>

        <!-- SECTION 2: KYC -->
        <div class="card shadow-sm mb-3">
          <div class="card-header bg-white d-flex justify-content-between align-items-center">
            <span class="fw-semibold">KYC Verification</span>
            <button type="button" class="btn btn-outline-primary btn-sm" (click)="resetSection('kycDetails')">
              Reset Section
            </button>
          </div>

          <div class="card-body" formGroupName="kycDetails">

            @if (kycDetailsGroup.touched && kycDetailsGroup.errors?.['panMustBeValidBeforeConsent']) {
              <div class="alert alert-danger py-2 mb-3">
                Please enter a valid PAN before giving KYC consent.
              </div>
            }

            <div class="row g-3">
              <div class="col-12 col-md-6">
                <label class="form-label">PAN</label>
                <input class="form-control" formControlName="panNumber" placeholder="ABCDE1234F"
                       [class.is-invalid]="kycDetailsGroup.get('panNumber')?.touched && kycDetailsGroup.get('panNumber')?.invalid" />
                @if (kycDetailsGroup.get('panNumber')?.touched && kycDetailsGroup.get('panNumber')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (kycDetailsGroup.get('panNumber')?.errors?.['required']) { <div>PAN is required.</div> }
                    @if (kycDetailsGroup.get('panNumber')?.errors?.['pattern']) { <div>Enter valid PAN format (e.g., ABCDE1234F).</div> }
                  </div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">Aadhaar (Last 4 digits)</label>
                <input class="form-control" formControlName="aadhaarLast4Digits" placeholder="1234"
                       [class.is-invalid]="kycDetailsGroup.get('aadhaarLast4Digits')?.touched && kycDetailsGroup.get('aadhaarLast4Digits')?.invalid" />
                @if (kycDetailsGroup.get('aadhaarLast4Digits')?.touched && kycDetailsGroup.get('aadhaarLast4Digits')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (kycDetailsGroup.get('aadhaarLast4Digits')?.errors?.['required']) { <div>Last 4 digits are required.</div> }
                    @if (kycDetailsGroup.get('aadhaarLast4Digits')?.errors?.['pattern']) { <div>Enter exactly 4 digits.</div> }
                  </div>
                }
              </div>

              <div class="col-12">
                <div class="form-check">
                  <input class="form-check-input" type="checkbox" id="isKycConsentGiven" formControlName="isKycConsentGiven"
                         [class.is-invalid]="kycDetailsGroup.get('isKycConsentGiven')?.touched && kycDetailsGroup.get('isKycConsentGiven')?.invalid" />
                  <label class="form-check-label" for="isKycConsentGiven">
                    I consent to KYC verification and data usage for onboarding.
                  </label>
                </div>
                @if (kycDetailsGroup.get('isKycConsentGiven')?.touched && kycDetailsGroup.get('isKycConsentGiven')?.invalid) {
                  <div class="text-danger small mt-1">Consent is required.</div>
                }
              </div>
            </div>

            <div class="alert alert-info py-2 mt-3 mb-0">
              Tip: Keep PAN details exactly as per official records to avoid verification failures.
            </div>
          </div>
        </div>

        <!-- SECTION 3: BANK & PAYOUT -->
        <div class="card shadow-sm mb-3">
          <div class="card-header bg-white d-flex justify-content-between align-items-center">
            <span class="fw-semibold">Bank & Payout</span>
            <button type="button" class="btn btn-outline-primary btn-sm" (click)="resetSection('bankPayoutDetails')">
              Reset Section
            </button>
          </div>

          <div class="card-body" formGroupName="bankPayoutDetails">

            @if (bankPayoutDetailsGroup.touched && bankPayoutDetailsGroup.errors?.['bankAccountNumbersDoNotMatch']) {
              <div class="alert alert-danger py-2 mb-3">
                Account Number and Confirm Account Number must match.
              </div>
            }

            <div class="row g-3">
              <div class="col-12">
                <label class="form-label">Account Holder Name</label>
                <input class="form-control" formControlName="bankAccountHolderName" placeholder="As per bank passbook"
                       [class.is-invalid]="bankPayoutDetailsGroup.get('bankAccountHolderName')?.touched && bankPayoutDetailsGroup.get('bankAccountHolderName')?.invalid" />
                @if (bankPayoutDetailsGroup.get('bankAccountHolderName')?.touched && bankPayoutDetailsGroup.get('bankAccountHolderName')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (bankPayoutDetailsGroup.get('bankAccountHolderName')?.errors?.['required']) { <div>Account Holder Name is required.</div> }
                    @if (bankPayoutDetailsGroup.get('bankAccountHolderName')?.errors?.['minlength']) { <div>Minimum 3 characters.</div> }
                  </div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">Account Number</label>
                <input class="form-control" formControlName="bankAccountNumber" placeholder="9 to 18 digits"
                       [class.is-invalid]="bankPayoutDetailsGroup.get('bankAccountNumber')?.touched && bankPayoutDetailsGroup.get('bankAccountNumber')?.invalid" />
                @if (bankPayoutDetailsGroup.get('bankAccountNumber')?.touched && bankPayoutDetailsGroup.get('bankAccountNumber')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (bankPayoutDetailsGroup.get('bankAccountNumber')?.errors?.['required']) { <div>Account Number is required.</div> }
                    @if (bankPayoutDetailsGroup.get('bankAccountNumber')?.errors?.['pattern']) { <div>Enter 9 to 18 digits.</div> }
                  </div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">Confirm Account Number</label>
                <input class="form-control" formControlName="confirmBankAccountNumber" placeholder="Re-enter account number"
                       [class.is-invalid]="bankPayoutDetailsGroup.get('confirmBankAccountNumber')?.touched && bankPayoutDetailsGroup.get('confirmBankAccountNumber')?.invalid" />
                @if (bankPayoutDetailsGroup.get('confirmBankAccountNumber')?.touched && bankPayoutDetailsGroup.get('confirmBankAccountNumber')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (bankPayoutDetailsGroup.get('confirmBankAccountNumber')?.errors?.['required']) { <div>Confirm Account Number is required.</div> }
                    @if (bankPayoutDetailsGroup.get('confirmBankAccountNumber')?.errors?.['pattern']) { <div>Enter 9 to 18 digits.</div> }
                  </div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">IFSC</label>
                <input class="form-control" formControlName="ifscCode" placeholder="HDFC0001234"
                       [class.is-invalid]="bankPayoutDetailsGroup.get('ifscCode')?.touched && bankPayoutDetailsGroup.get('ifscCode')?.invalid" />
                @if (bankPayoutDetailsGroup.get('ifscCode')?.touched && bankPayoutDetailsGroup.get('ifscCode')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (bankPayoutDetailsGroup.get('ifscCode')?.errors?.['required']) { <div>IFSC is required.</div> }
                    @if (bankPayoutDetailsGroup.get('ifscCode')?.errors?.['pattern']) { <div>Enter valid IFSC format.</div> }
                  </div>
                }
              </div>
            </div>

            <div class="alert alert-info py-2 mt-3 mb-0">
              Tip: Use a bank account in your business/legal name for faster payout verification.
            </div>
          </div>
        </div>

        <!-- SECTION 4: PICKUP ADDRESS -->
        <div class="card shadow-sm">
          <div class="card-header bg-white d-flex justify-content-between align-items-center">
            <span class="fw-semibold">Pickup Address</span>
            <button type="button" class="btn btn-outline-primary btn-sm" (click)="resetSection('pickupAddressDetails')">
              Reset Section
            </button>
          </div>

          <div class="card-body" formGroupName="pickupAddressDetails">

            @if (pickupAddressDetailsGroup.touched && pickupAddressDetailsGroup.errors?.['pickupPincodeStateMismatch']) {
              <div class="alert alert-warning py-2 mb-3">
                Pincode starting with <b>75</b>/<b>76</b> usually belongs to <b>Odisha</b>. Please verify State.
              </div>
            }

            <div class="row g-3">
              <div class="col-12">
                <label class="form-label">Address Line 1</label>
                <input class="form-control" formControlName="pickupAddressLine1" placeholder="House/Building, Street, Area"
                       [class.is-invalid]="pickupAddressDetailsGroup.get('pickupAddressLine1')?.touched && pickupAddressDetailsGroup.get('pickupAddressLine1')?.invalid" />
                @if (pickupAddressDetailsGroup.get('pickupAddressLine1')?.touched && pickupAddressDetailsGroup.get('pickupAddressLine1')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (pickupAddressDetailsGroup.get('pickupAddressLine1')?.errors?.['required']) { <div>Address Line 1 is required.</div> }
                    @if (pickupAddressDetailsGroup.get('pickupAddressLine1')?.errors?.['minlength']) { <div>Minimum 5 characters.</div> }
                  </div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">City</label>
                <input class="form-control" formControlName="pickupCity" placeholder="City"
                       [class.is-invalid]="pickupAddressDetailsGroup.get('pickupCity')?.touched && pickupAddressDetailsGroup.get('pickupCity')?.invalid" />
                @if (pickupAddressDetailsGroup.get('pickupCity')?.touched && pickupAddressDetailsGroup.get('pickupCity')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (pickupAddressDetailsGroup.get('pickupCity')?.errors?.['required']) { <div>City is required.</div> }
                    @if (pickupAddressDetailsGroup.get('pickupCity')?.errors?.['minlength']) { <div>Minimum 2 characters.</div> }
                  </div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">State</label>
                <input class="form-control" formControlName="pickupState" placeholder="State"
                       [class.is-invalid]="pickupAddressDetailsGroup.get('pickupState')?.touched && pickupAddressDetailsGroup.get('pickupState')?.invalid" />
                @if (pickupAddressDetailsGroup.get('pickupState')?.touched && pickupAddressDetailsGroup.get('pickupState')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (pickupAddressDetailsGroup.get('pickupState')?.errors?.['required']) { <div>State is required.</div> }
                    @if (pickupAddressDetailsGroup.get('pickupState')?.errors?.['minlength']) { <div>Minimum 2 characters.</div> }
                  </div>
                }
              </div>

              <div class="col-12 col-md-6">
                <label class="form-label">Pincode</label>
                <input class="form-control" formControlName="pickupPincode" placeholder="6-digit pincode"
                       [class.is-invalid]="pickupAddressDetailsGroup.get('pickupPincode')?.touched && pickupAddressDetailsGroup.get('pickupPincode')?.invalid" />
                @if (pickupAddressDetailsGroup.get('pickupPincode')?.touched && pickupAddressDetailsGroup.get('pickupPincode')?.invalid) {
                  <div class="invalid-feedback d-block">
                    @if (pickupAddressDetailsGroup.get('pickupPincode')?.errors?.['required']) { <div>Pincode is required.</div> }
                    @if (pickupAddressDetailsGroup.get('pickupPincode')?.errors?.['pattern']) { <div>Pincode must be 6 digits.</div> }
                  </div>
                }
              </div>

              <div class="col-12">
                <div class="form-check">
                  <input class="form-check-input" type="checkbox" id="isPickupConsentGiven" formControlName="isPickupConsentGiven"
                         [class.is-invalid]="pickupAddressDetailsGroup.get('isPickupConsentGiven')?.touched && pickupAddressDetailsGroup.get('isPickupConsentGiven')?.invalid" />
                  <label class="form-check-label" for="isPickupConsentGiven">
                    I confirm this pickup address is correct for courier pickups.
                  </label>
                </div>

                @if (pickupAddressDetailsGroup.get('isPickupConsentGiven')?.touched && pickupAddressDetailsGroup.get('isPickupConsentGiven')?.invalid) {
                  <div class="text-danger small mt-1">Pickup consent is required.</div>
                }
              </div>
            </div>

            <div class="alert alert-info py-2 mt-3 mb-0">
              Tip: Use an address where someone is available during courier pickup hours.
            </div>
          </div>
        </div>

        <!-- Mobile BUTTONS -->
        <div class="d-flex justify-content-end gap-2 mt-3 d-lg-none">
          <button type="submit" class="btn btn-success" [disabled]="sellerRegistrationForm.invalid">
            Submit Registration
          </button>
          <button type="button" class="btn btn-outline-primary" (click)="sellerRegistrationForm.reset(); submittedSellerData=null;">
            Reset All
          </button>
        </div>

      </div>

      <!-- RIGHT: PROGRESS + HELP -->
      <div class="col-12 col-lg-4">
        <div class="sticky-top pt-2">

          <!-- PROGRESS -->
          <div class="card shadow-sm mb-3">
            <div class="card-header bg-white fw-semibold">Progress</div>
            <div class="card-body">
              <p class="small text-muted mb-3">
                Complete all sections to submit your seller account.
              </p>

              <div class="list-group list-group-flush">
                <div class="list-group-item px-0 d-flex justify-content-between align-items-center">
                  <span>Business Details</span>
                  <span class="badge"
                        [class.text-bg-success]="businessDetailsGroup.valid"
                        [class.text-bg-secondary]="!businessDetailsGroup.valid">
                    {{ businessDetailsGroup.valid ? 'Done' : 'Pending' }}
                  </span>
                </div>

                <div class="list-group-item px-0 d-flex justify-content-between align-items-center">
                  <span>KYC Verification</span>
                  <span class="badge"
                        [class.text-bg-success]="kycDetailsGroup.valid"
                        [class.text-bg-secondary]="!kycDetailsGroup.valid">
                    {{ kycDetailsGroup.valid ? 'Done' : 'Pending' }}
                  </span>
                </div>

                <div class="list-group-item px-0 d-flex justify-content-between align-items-center">
                  <span>Bank & Payout</span>
                  <span class="badge"
                        [class.text-bg-success]="bankPayoutDetailsGroup.valid"
                        [class.text-bg-secondary]="!bankPayoutDetailsGroup.valid">
                    {{ bankPayoutDetailsGroup.valid ? 'Done' : 'Pending' }}
                  </span>
                </div>

                <div class="list-group-item px-0 d-flex justify-content-between align-items-center">
                  <span>Pickup Address</span>
                  <span class="badge"
                        [class.text-bg-success]="pickupAddressDetailsGroup.valid"
                        [class.text-bg-secondary]="!pickupAddressDetailsGroup.valid">
                    {{ pickupAddressDetailsGroup.valid ? 'Done' : 'Pending' }}
                  </span>
                </div>
              </div>

              <hr class="my-3">

              <div class="d-grid gap-2">
                <button type="button" class="btn btn-outline-primary btn-sm" (click)="businessDetailsGroup.markAllAsTouched()">
                  Validate Business
                </button>
                <button type="button" class="btn btn-outline-primary btn-sm" (click)="kycDetailsGroup.markAllAsTouched()">
                  Validate KYC
                </button>
                <button type="button" class="btn btn-outline-primary btn-sm" (click)="bankPayoutDetailsGroup.markAllAsTouched()">
                  Validate Bank
                </button>
                <button type="button" class="btn btn-outline-primary btn-sm" (click)="pickupAddressDetailsGroup.markAllAsTouched()">
                  Validate Pickup
                </button>
              </div>

              <div class="alert alert-info py-2 mt-3 mb-0">
                Tip: Fill section-by-section. Use “Reset Section” if needed.
              </div>
            </div>
          </div>

          <!-- HELP -->
          <div class="card shadow-sm">
            <div class="card-header bg-white fw-semibold">Need Help?</div>
            <div class="card-body">
              <ul class="small mb-0">
                <li class="mb-2">Use your legal name as per PAN/GST registration.</li>
                <li class="mb-2">Bank details must match exactly to avoid payout holds.</li>
                <li class="mb-0">Pickup address should be reachable during courier pickup hours.</li>
              </ul>
            </div>
          </div>

        </div>
      </div>

    </div>
  </form>

  <!-- SUBMITTED DATA -->
  @if (submittedSellerData) {
    <div class="card shadow-sm mt-4">
      <div class="card-header bg-white fw-semibold">Submitted Data</div>
      <div class="card-body">
        <pre class="mb-0">{{ submittedSellerData | json }}</pre>
      </div>
    </div>
  }
</div>
Conclusion:

FormGroup in Angular Reactive Forms helps organize large forms into clear, logical sections that match real business requirements. By grouping related controls together, it makes validation, maintenance, and scalability much easier, especially in enterprise applications. Using FormGroup leads to cleaner code, better user experience, and more professional, real-world form design.

Leave a Reply

Your email address will not be published. Required fields are marked *