Logo

Architecture & Best Practices

Thinking in Components

From Spaghetti Code to Scalable Architecture: How I refactor monolithic forms into reusable, smart components using ControlContainer.

When I develop a new feature, I often start "quick and dirty". I write everything in one file to see if the logic works. But as soon as the basic structure is in place, I pause. I look at my code and search for patterns. When I copy the same HTML block multiple times or my file becomes barely readable due to nested divs and inline logic, that's a sign: It's time for a Component.

1. The "Monolith" (Before)

Here's a typical registration form. Look at how much code is needed just to display four fields. We constantly repeat labels, input wrappers, error messages, and logic. This makes the file huge and hard to maintain.

register.component.html
<!-- ❌ REPETITIVE & HARD TO MAINTAIN -->
<form [formGroup]="registerForm">

  <!-- Firstname -->
  <div class="form-group">
    <label>First Name</label>
    <input formControlName="firstName" type="text">
    @if(firstName?.touched && firstName?.errors?.['required']) {
      <span class="error">Required field</span>
    }
  </div>

  <!-- Lastname (Copy Paste...) -->
  <div class="form-group">
    <label>Last Name</label>
    <input formControlName="lastName" type="text">
    @if(lastName?.touched && lastName?.errors?.['required']) {
      <span class="error">Required field</span>
    }
  </div>

  <!-- Email (More Copy Paste...) -->
  <div class="form-group">
    <label>Email</label>
    <input formControlName="email" type="email">
    @if(email?.touched && email?.errors?.['email']) {
      <span class="error">Invalid email</span>
    }
  </div>

  <!-- Password (Complex Logic inline) -->
  <div class="form-group">
    <label>Password</label>
    <div class="input-wrapper">
      <input
        [type]="showPassword ? 'text' : 'password'"
        formControlName="password">
      <button (click)="togglePassword()">👁️</button>
    </div>
  </div>

  <button type="submit">Register</button>
</form>
⚠️

If you want to change the design, you have to do it 4 times here. The file is bursting at the seams, even though it only queries simple data.

2. The Abstraction

I isolate the logic and HTML into an app-auth-inputs component. The trick is ControlContainer: This allows the child component to access the parent's FormGroup. The logic for the password toggle also moves in here.

auth-inputs.component.ts
@Component({
  selector: 'app-auth-inputs',
  // The Magic Sauce: Access parent FormGroup 👇
  viewProviders: [
    { provide: ControlContainer, useExisting: FormGroupDirective }
  ]
})
export class AuthInputs {

  @Input({ required: true }) name!: string;
  @Input() label = '';
  @Input() type: 'text' | 'password' | 'email' = 'text';

  // Logic encapsulated specifically for this component
  showPassword = signal(false);

  togglePassword() {
    this.showPassword.update(v => !v);
  }

  get currentType() {
    return this.type === 'password' && this.showPassword()
      ? 'text'
      : this.type;
  }
}
auth-inputs.component.html
<div class="form-group">
  <label [for]="name">{{ label }}</label>

  <div class="input-wrapper">
    <input
      [type]="currentType"
      [id]="name"
      [formControlName]="name">

    @if(type === 'password') {
      <button (click)="togglePassword()">👁️</button>
    }
  </div>

  <!-- Error handling central & consistent -->
  @if(control.touched && control.hasError('required')) {
    <span class="error">
      Please enter a {{ label }}
    </span>
  }
</div>

3. The Result (Clean Code)

This is what my code looks like after refactoring. The input fields are abstracted, which massively cleans up the HTML. The logic is where it belongs.

register.component.html
<form [formGroup]="registerForm" (ngSubmit)="submit()">

  <!-- ✅ CLEAN, READABLE, REUSABLE -->

  <app-auth-inputs
    type="text"
    name="firstName"
    label="First Name">
  </app-auth-inputs>

  <app-auth-inputs
    type="text"
    name="lastName"
    label="Last Name">
  </app-auth-inputs>

  <app-auth-inputs
    type="email"
    name="email"
    label="Email">
  </app-auth-inputs>

  <app-auth-inputs
    type="password"
    name="password"
    label="Password">
  </app-auth-inputs>

  <!-- Specific Logic remains here -->
  <button class="btn-primary" type="submit">
    Register
  </button>

</form>
💡

Reusability is not the only reason for components. I often extract code simply to keep the HTML structure clean and encapsulate styling (SCSS). Even if I only need the button here: If it has 50 lines of styles, a separate component is often worthwhile.