Back to catalog

angular-components

Patrones y mejores prácticas para crear componentes Angular con signals, forms, y lifecycle. Usa esto al crear o modificar componentes, especialmente formularios y componentes con estado.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
description: Patrones y mejores prácticas para crear componentes Angular con signals, forms, y lifecycle. Usa esto al crear o modificar componentes, especialmente formularios y componentes con estado.
---

# Angular Components Best Practices 🎯

## General Component Guidelines

- Always use **standalone components** (no NgModules)
- Must **NOT** set `standalone: true` inside decorators - it's the default
- Keep components small and focused on a single responsibility
- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
- Prefer inline templates for small components

## Modern Angular APIs

### Inputs and Outputs

**ALWAYS** use the new function-based APIs:

```typescript
// ✅ Correct
export class MyComponent {
  name = input<string>(); // input signal
  nameChange = output<string>(); // output emitter
}

// ❌ Avoid
@Input() name: string;
@Output() nameChange = new EventEmitter<string>();
```

### Host Bindings

**DO NOT** use `@HostBinding` and `@HostListener` decorators:

```typescript
// ✅ Correct
@Component({
  host: {
    '(click)': 'onClick()',
    '[class.active]': 'isActive()'
  }
})

// ❌ Avoid
@HostListener('click') onClick() {}
@HostBinding('class.active') isActive: boolean;
```

## Form Component Lifecycle Pattern 🔄

**ALWAYS** implement this pattern for form components that can create OR edit entities:

### 1. Use `effect()` to Watch Input Changes

```typescript
constructor() {
  // Watch for changes in initial input to reset/populate form
  effect(() => {
    const initialData = this.initial();

    if (initialData === null) {
      // Drawer closed or creating new record - reset form completely
      this.resetForm();
    } else if (initialData) {
      // Editing existing record - populate form
      this.patchInitial(initialData);
    }
  });
}
```

### 2. Implement Comprehensive `resetForm()` Method

```typescript
private resetForm() {
  // Reset form to default values
  this.form.reset({ 
    name: '',
    email: '',
    // ... other fields with default values
  });

  // Clear any uploaded documents, files, or additional state
  this.uploadedDocuments.set([]);

  // Clear computed/derived data (calculations, etc.)
  this.calculatedData.set(null);

  // Mark form as pristine and untouched
  this.form.markAsPristine();
  this.form.markAsUntouched();
}
```

### 3. Parent Component Pattern

```typescript
// In list/parent component
closeDrawer() {
  this.openDrawer.set(false);
  this.editingData.set(null);  // This triggers form reset via effect()
}
```

### Why This Pattern?

- ✅ Form always starts fresh when creating new records
- ✅ No stale data from previous edits
- ✅ Automatic cleanup when drawer/dialog closes
- ✅ Single source of truth for form state (the `initial` input)
- ✅ Works seamlessly with OnPush change detection

## Forms

- Prefer **Reactive forms** instead of Template-driven ones
- Always validate forms properly
- Use typed forms with proper interfaces

## Template Best Practices

- Do **NOT** use `ngClass`, use `class` bindings instead
- Do **NOT** use `ngStyle`, use `style` bindings instead
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`

```typescript
// ✅ Correct
@if (isVisible()) {
  <div>Content</div>
}

@for (item of items(); track item.id) {
  <div>{{ item.name }}</div>
}

// ❌ Avoid
<div *ngIf="isVisible">Content</div>
<div *ngFor="let item of items">{{ item.name }}</div>
```

## Images

- Use `NgOptimizedImage` for all static images
- Note: `NgOptimizedImage` does NOT work for inline base64 images

## Input Formatting Pattern 🎨

**ALWAYS** implement auto-formatting for inputs that require specific formats (MAC address, phone, credit card, etc.)

### Pattern: Auto-Format While Typing

```typescript
/**
 * Example: MAC Address Formatter
 * Auto-formats to XX:XX:XX:XX:XX:XX while user types
 */
formatMacAddress(event: Event): void {
  const input = event.target as HTMLInputElement;
  const cursorPosition = input.selectionStart || 0;
  let value = input.value.toUpperCase();

  // Step 1: Clean input - remove invalid characters
  const cleanValue = value.replace(/[^0-9A-F]/g, '');

  // Step 2: Limit length (12 hex chars = 6 pairs)
  const limitedValue = cleanValue.substring(0, 12);

  // Step 3: Format - add separator every 2 characters
  const formatted = limitedValue.match(/.{1,2}/g)?.join(':') || limitedValue;

  // Step 4: Calculate new cursor position
  const oldLength = value.length;
  const newLength = formatted.length;
  const lengthDiff = newLength - oldLength;
  const newCursorPosition = cursorPosition + lengthDiff;

  // Step 5: Update form control WITHOUT emitting event (prevents loops)
  this.form.get('macAddress')?.setValue(formatted, { emitEvent: false });

  // Step 6: Restore cursor position (setTimeout needed for DOM update)
  setTimeout(() => {
    if (input) {
      input.setSelectionRange(newCursorPosition, newCursorPosition);
    }
  }, 0);
}
```

### HTML Template

```html
<input
  id="macAddress"
  type="text"
  pInputText
  formControlName="macAddress"
  placeholder="Ingrese los caracteres hexadecimales"
  (input)="formatMacAddress($event)"
  maxlength="17"
  class="w-full">

<small class="text-gray-500 text-xs mt-1 block">
  Se formatea automáticamente (ej: "001122334455" → "00:11:22:33:44:55")
</small>
```

### Key Features

1. **Preserves Cursor Position**: User can edit anywhere in the input
2. **Uppercase Conversion**: Automatically converts to uppercase
3. **Character Filtering**: Only allows valid characters
4. **Length Limiting**: Prevents over-typing
5. **Auto-Separation**: Adds separators automatically
6. **No Event Emission**: Uses `{ emitEvent: false }` to prevent validation loops

### Other Common Formatters

#### Phone Number (XXX-XXXX-XXXX)

```typescript
formatPhoneNumber(event: Event): void {
  const input = event.target as HTMLInputElement;
  const cursorPosition = input.selectionStart || 0;
  let value = input.value;

  // Remove all non-digits
  const cleanValue = value.replace(/\D/g, '');
  const limitedValue = cleanValue.substring(0, 11); // Honduras: 8 digits

  // Format: XXXX-XXXX or XXXX-XXXX-XXXX for landlines
  let formatted = limitedValue;
  if (limitedValue.length > 4 && limitedValue.length <= 8) {
    formatted = `${limitedValue.slice(0, 4)}-${limitedValue.slice(4)}`;
  } else if (limitedValue.length > 8) {
    formatted = `${limitedValue.slice(0, 4)}-${limitedValue.slice(4, 8)}-${limitedValue.slice(8)}`;
  }

  const newCursorPosition = cursorPosition + (formatted.length - value.length);
  this.form.get('phone')?.setValue(formatted, { emitEvent: false });

  setTimeout(() => {
    input.setSelectionRange(newCursorPosition, newCursorPosition);
  }, 0);
}
```

#### Credit Card (XXXX XXXX XXXX XXXX)

```typescript
formatCreditCard(event: Event): void {
  const input = event.target as HTMLInputElement;
  let value = input.value.replace(/\s/g, ''); // Remove spaces

  const cleanValue = value.replace(/\D/g, ''); // Only digits
  const limitedValue = cleanValue.substring(0, 16); // Max 16 digits

  // Add space every 4 digits
  const formatted = limitedValue.match(/.{1,4}/g)?.join(' ') || limitedValue;

  this.form.get('cardNumber')?.setValue(formatted, { emitEvent: false });
}
```

#### Currency (Format as typing)

```typescript
formatCurrency(event: Event): void {
  const input = event.target as HTMLInputElement;
  let value = input.value.replace(/[^0-9.]/g, ''); // Only numbers and dot

  // Ensure only one decimal point
  const parts = value.split('.');
  if (parts.length > 2) {
    value = parts[0] + '.' + parts.slice(1).join('');
  }

  // Limit decimals to 2 places
  if (parts[1]?.length > 2) {
    value = parts[0] + '.' + parts[1].substring(0, 2);
  }

  this.form.get('amount')?.setValue(value, { emitEvent: false });
}
```

### Best Practices

#### ✅ Do's

- Always preserve cursor position for good UX
- Use `{ emitEvent: false }` to prevent validation loops
- Add helpful placeholder and hint text
- Set appropriate `maxlength` attribute
- Use `setTimeout()` for cursor position (DOM update timing)
- Test with copy-paste scenarios
- Handle edge cases (empty input, partial input)

#### ❌ Don'ts

- Don't format on `blur` only - format while typing for better UX
- Don't move cursor to end - preserve user's editing position
- Don't forget to clean invalid characters first
- Don't allow unlimited length
- Don't emit form events during formatting (causes loops)
- Don't forget visual feedback (placeholder, helper text)

### Validation Pattern

Always combine formatting with proper validation:

```typescript
// In constructor or form initialization
this.form = this.fb.group({
  macAddress: ['', [
    Validators.required,
    Validators.pattern(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/)
  ]]
});
```

### Real Example

See complete implementation in:

- `src/app/features/biometrics/components/biometric-form/biometric-form.component.ts` (lines 252-287)
- `src/app/features/biometrics/components/biometric-form/biometric-form.component.html` (lines 86-103)