npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cbm-common/auto-tab-directive

v0.0.1

Published

Directiva Angular standalone para navegación automática entre campos de entrada. Facilita la experiencia de usuario en formularios con campos secuenciales como códigos, números de teléfono, fechas y otros datos estructurados.

Readme

Auto Tab Directive

Directiva Angular standalone para navegación automática entre campos de entrada. Facilita la experiencia de usuario en formularios con campos secuenciales como códigos, números de teléfono, fechas y otros datos estructurados.

📦 Instalación

npm install @cbm-common/auto-tab-directive

⚙️ Configuración

Importación de la Directiva

La directiva es standalone, por lo que se puede importar directamente:

import { CbmAutoTabDirective } from '@cbm-common/auto-tab-directive';

@Component({
  selector: 'app-form-with-tabs',
  standalone: true,
  imports: [CbmAutoTabDirective],
  template: `
    <div class="form-group">
      <input #input1 type="text" maxlength="2" placeholder="Código 1">
      <input #input2 type="text" maxlength="2" placeholder="Código 2"
             [cbmAutoTab]="input1">
      <input #input3 type="text" maxlength="2" placeholder="Código 3"
             [cbmAutoTab]="input2">
    </div>
  `
})
export class FormWithTabsComponent {}

🎯 Propiedades de Entrada

cbmAutoTab: HTMLElement (Requerido)

Referencia al siguiente elemento HTML que debe recibir el foco cuando el input actual alcance su longitud máxima.

// Usando template reference variables
<input #phone1 type="text" maxlength="3">
<input #phone2 type="text" maxlength="3" [cbmAutoTab]="phone1">
<input #phone3 type="text" maxlength="4" [cbmAutoTab]="phone2">

// Usando ViewChild para acceso programático
@ViewChild('phoneInput2', { static: true }) phoneInput2!: ElementRef;
<input #phoneInput2 type="text" maxlength="3" [cbmAutoTab]="phoneInput1">

🏗️ Arquitectura de la Directiva

Patrón de Diseño

La directiva sigue el patrón Attribute Directive de Angular:

CbmAutoTabDirective
├── Selector: [cbmAutoTab]
├── Input: HTMLElement (siguiente elemento)
├── HostListener: input event
├── Lógica de navegación automática
└── Navegación bidireccional

Funcionamiento Interno

@Directive({
  selector: '[cbmAutoTab]',
  standalone: true,
})
export class CbmAutoTabDirective {
  @Input('cbmAutoTab') nextInput!: HTMLElement;

  @HostListener('input', ['$event'])
  onInput(event: Event) {
    // 1. Verificar si se alcanzó maxLength
    // 2. Configurar listener para navegación inversa
    // 3. Hacer foco en el siguiente input
  }
}

Navegación Bidireccional

La directiva implementa navegación inteligente:

  • Hacia adelante: Cuando se completa el campo actual
  • Hacia atrás: Cuando se borra el contenido del campo siguiente

🎨 Características Principales

Navegación Automática

  • Detección automática de longitud máxima (maxLength)
  • Transición suave entre campos
  • Prevención de pérdida de foco inesperada

Navegación Inversa

  • Retroceso automático al borrar contenido
  • Mantenimiento del contexto de entrada
  • Experiencia fluida de edición

Compatibilidad

  • Todos los tipos de input: text, number, tel, password
  • Campos con máscara: Compatible con librerías de máscara
  • Formularios reactivos: Funciona con FormControl
  • Validaciones: No interfiere con validaciones existentes

🚀 Ejemplos de Uso

Ejemplo Básico: Código de Verificación

@Component({
  selector: 'app-verification-code',
  standalone: true,
  imports: [CbmAutoTabDirective],
  template: `
    <div class="verification-code">
      <h3>Ingresa el código de verificación</h3>

      <div class="code-inputs">
        <input #digit1
               type="text"
               maxlength="1"
               placeholder="0"
               class="digit-input">

        <input #digit2
               type="text"
               maxlength="1"
               placeholder="0"
               class="digit-input"
               [cbmAutoTab]="digit1">

        <input #digit3
               type="text"
               maxlength="1"
               placeholder="0"
               class="digit-input"
               [cbmAutoTab]="digit2">

        <input #digit4
               type="text"
               maxlength="1"
               placeholder="0"
               class="digit-input"
               [cbmAutoTab]="digit3">

        <input #digit5
               type="text"
               maxlength="1"
               placeholder="0"
               class="digit-input"
               [cbmAutoTab]="digit4">

        <input #digit6
               type="text"
               maxlength="1"
               placeholder="0"
               class="digit-input"
               [cbmAutoTab]="digit5">
      </div>

      <button type="button" (click)="verifyCode()">
        Verificar Código
      </button>
    </div>
  `,
  styles: [`
    .code-inputs {
      display: flex;
      gap: 8px;
      justify-content: center;
      margin: 20px 0;
    }

    .digit-input {
      width: 40px;
      height: 40px;
      text-align: center;
      font-size: 18px;
      border: 2px solid #ddd;
      border-radius: 4px;
      transition: border-color 0.2s;
    }

    .digit-input:focus {
      outline: none;
      border-color: #007bff;
    }
  `]
})
export class VerificationCodeComponent {
  @ViewChild('digit1') digit1!: ElementRef;
  @ViewChild('digit2') digit2!: ElementRef;
  @ViewChild('digit3') digit3!: ElementRef;
  @ViewChild('digit4') digit4!: ElementRef;
  @ViewChild('digit5') digit5!: ElementRef;
  @ViewChild('digit6') digit6!: ElementRef;

  getCode(): string {
    return [
      this.digit1.nativeElement.value,
      this.digit2.nativeElement.value,
      this.digit3.nativeElement.value,
      this.digit4.nativeElement.value,
      this.digit5.nativeElement.value,
      this.digit6.nativeElement.value
    ].join('');
  }

  verifyCode() {
    const code = this.getCode();
    if (code.length === 6) {
      console.log('Verificando código:', code);
      // Llamar a API de verificación
    }
  }
}

Ejemplo con Número de Teléfono

@Component({
  selector: 'app-phone-input',
  standalone: true,
  imports: [CbmAutoTabDirective],
  template: `
    <div class="phone-input">
      <label>Número de teléfono</label>

      <div class="phone-fields">
        <span class="prefix">(+57)</span>

        <input #areaCode
               type="text"
               maxlength="3"
               placeholder="601"
               class="phone-field">

        <input #central
               type="text"
               maxlength="3"
               placeholder="123"
               class="phone-field"
               [cbmAutoTab]="areaCode">

        <input #line
               type="text"
               maxlength="4"
               placeholder="4567"
               class="phone-field"
               [cbmAutoTab]="central">
      </div>

      <div class="phone-display">
        Número completo: (+57) {{ getFullNumber() }}
      </div>
    </div>
  `,
  styles: [`
    .phone-fields {
      display: flex;
      align-items: center;
      gap: 4px;
      margin: 8px 0;
    }

    .prefix {
      color: #666;
      font-weight: 500;
    }

    .phone-field {
      width: 60px;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      text-align: center;
    }

    .phone-display {
      margin-top: 12px;
      padding: 8px;
      background: #f5f5f5;
      border-radius: 4px;
      font-family: monospace;
    }
  `]
})
export class PhoneInputComponent {
  @ViewChild('areaCode') areaCode!: ElementRef;
  @ViewChild('central') central!: ElementRef;
  @ViewChild('line') line!: ElementRef;

  getFullNumber(): string {
    const area = this.areaCode.nativeElement.value;
    const central = this.central.nativeElement.value;
    const line = this.line.nativeElement.value;

    if (area && central && line) {
      return `${area}-${central}-${line}`;
    }
    return '';
  }
}

Ejemplo con Fecha (DD/MM/YYYY)

@Component({
  selector: 'app-date-input',
  standalone: true,
  imports: [CbmAutoTabDirective],
  template: `
    <div class="date-input">
      <label>Fecha de nacimiento</label>

      <div class="date-fields">
        <input #day
               type="text"
               maxlength="2"
               placeholder="DD"
               class="date-field"
               (input)="validateDay($event)">

        <span class="separator">/</span>

        <input #month
               type="text"
               maxlength="2"
               placeholder="MM"
               class="date-field"
               [cbmAutoTab]="day"
               (input)="validateMonth($event)">

        <span class="separator">/</span>

        <input #year
               type="text"
               maxlength="4"
               placeholder="YYYY"
               class="date-field"
               [cbmAutoTab]="month"
               (input)="validateYear($event)">
      </div>

      <div class="date-display">
        Fecha: {{ getFormattedDate() }}
      </div>
    </div>
  `,
  styles: [`
    .date-fields {
      display: flex;
      align-items: center;
      gap: 2px;
      margin: 8px 0;
    }

    .date-field {
      width: 50px;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      text-align: center;
    }

    .separator {
      color: #666;
      font-weight: bold;
    }

    .date-display {
      margin-top: 12px;
      padding: 8px;
      background: #f0f8ff;
      border-radius: 4px;
    }
  `]
})
export class DateInputComponent {
  @ViewChild('day') dayInput!: ElementRef;
  @ViewChild('month') monthInput!: ElementRef;
  @ViewChild('year') yearInput!: ElementRef;

  validateDay(event: Event) {
    const input = event.target as HTMLInputElement;
    const value = parseInt(input.value);

    if (value < 1 || value > 31) {
      input.value = '';
    }
  }

  validateMonth(event: Event) {
    const input = event.target as HTMLInputElement;
    const value = parseInt(input.value);

    if (value < 1 || value > 12) {
      input.value = '';
    }
  }

  validateYear(event: Event) {
    const input = event.target as HTMLInputElement;
    const value = parseInt(input.value);
    const currentYear = new Date().getFullYear();

    if (value < 1900 || value > currentYear) {
      input.value = '';
    }
  }

  getFormattedDate(): string {
    const day = this.dayInput.nativeElement.value.padStart(2, '0');
    const month = this.monthInput.nativeElement.value.padStart(2, '0');
    const year = this.yearInput.nativeElement.value;

    if (day && month && year) {
      return `${day}/${month}/${year}`;
    }
    return '';
  }
}

Ejemplo con Formulario Reactivo

@Component({
  selector: 'app-reactive-form',
  standalone: true,
  imports: [CbmAutoTabDirective, ReactiveFormsModule],
  template: `
    <form [formGroup]="verificationForm" (ngSubmit)="onSubmit()">
      <div class="form-group">
        <label>Código de verificación SMS</label>

        <div class="code-inputs">
          <input #code1
                 type="text"
                 maxlength="1"
                 formControlName="digit1"
                 class="code-input">

          <input #code2
                 type="text"
                 maxlength="1"
                 formControlName="digit2"
                 class="code-input"
                 [cbmAutoTab]="code1">

          <input #code3
                 type="text"
                 maxlength="1"
                 formControlName="digit3"
                 class="code-input"
                 [cbmAutoTab]="code2">

          <input #code4
                 type="text"
                 maxlength="1"
                 formControlName="digit4"
                 class="code-input"
                 [cbmAutoTab]="code3">
        </div>

        <div *ngIf="verificationForm.invalid && verificationForm.touched"
             class="error-message">
          Por favor ingresa un código válido
        </div>
      </div>

      <button type="submit"
              [disabled]="verificationForm.invalid"
              class="submit-btn">
        Verificar
      </button>
    </form>
  `
})
export class ReactiveFormComponent implements OnInit {
  verificationForm!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.verificationForm = this.fb.group({
      digit1: ['', [Validators.required, Validators.pattern(/^\d$/)]],
      digit2: ['', [Validators.required, Validators.pattern(/^\d$/)]],
      digit3: ['', [Validators.required, Validators.pattern(/^\d$/)]],
      digit4: ['', [Validators.required, Validators.pattern(/^\d$/)]]
    });
  }

  onSubmit() {
    if (this.verificationForm.valid) {
      const code = Object.values(this.verificationForm.value).join('');
      console.log('Código verificado:', code);
    }
  }
}

Ejemplo con Componente de Tarjeta de Crédito

@Component({
  selector: 'app-credit-card',
  standalone: true,
  imports: [CbmAutoTabDirective],
  template: `
    <div class="credit-card-form">
      <h3>Información de Tarjeta</h3>

      <div class="form-row">
        <label>Número de tarjeta</label>
        <div class="card-number-inputs">
          <input #card1 type="text" maxlength="4" placeholder="1234" class="card-input">
          <input #card2 type="text" maxlength="4" placeholder="5678" class="card-input" [cbmAutoTab]="card1">
          <input #card3 type="text" maxlength="4" placeholder="9012" class="card-input" [cbmAutoTab]="card2">
          <input #card4 type="text" maxlength="4" placeholder="3456" class="card-input" [cbmAutoTab]="card3">
        </div>
      </div>

      <div class="form-row">
        <label>Fecha de expiración</label>
        <div class="expiry-inputs">
          <input #month type="text" maxlength="2" placeholder="MM" class="expiry-input">
          <span>/</span>
          <input #year type="text" maxlength="2" placeholder="YY" class="expiry-input" [cbmAutoTab]="month">
        </div>
      </div>

      <div class="form-row">
        <label>Código CVV</label>
        <input #cvv type="text" maxlength="3" placeholder="123" class="cvv-input">
      </div>

      <div class="card-preview">
        <div class="card-number">{{ getCardNumber() }}</div>
        <div class="card-expiry">{{ getExpiryDate() }}</div>
      </div>
    </div>
  `,
  styles: [`
    .card-number-inputs, .expiry-inputs {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .card-input {
      width: 60px;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      text-align: center;
    }

    .expiry-input {
      width: 40px;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      text-align: center;
    }

    .cvv-input {
      width: 60px;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }

    .card-preview {
      margin-top: 20px;
      padding: 16px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border-radius: 8px;
      font-family: monospace;
    }
  `]
})
export class CreditCardComponent {
  @ViewChild('card1') card1!: ElementRef;
  @ViewChild('card2') card2!: ElementRef;
  @ViewChild('card3') card3!: ElementRef;
  @ViewChild('card4') card4!: ElementRef;
  @ViewChild('month') month!: ElementRef;
  @ViewChild('year') year!: ElementRef;

  getCardNumber(): string {
    const parts = [
      this.card1.nativeElement.value,
      this.card2.nativeElement.value,
      this.card3.nativeElement.value,
      this.card4.nativeElement.value
    ].filter(part => part);

    return parts.join(' ').padEnd(19, '•');
  }

  getExpiryDate(): string {
    const month = this.month.nativeElement.value;
    const year = this.year.nativeElement.value;

    if (month && year) {
      return `${month}/${year}`;
    }
    return 'MM/YY';
  }
}

⚠️ Manejo de Errores y Validaciones

Validaciones de Entrada

La directiva no realiza validaciones por sí misma, pero se integra perfectamente con validaciones existentes:

// Validación personalizada para números
onlyNumbersValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
    if (value && !/^\d+$/.test(value)) {
      return { onlyNumbers: true };
    }
    return null;
  };
}

// Uso en formulario reactivo
this.verificationForm = this.fb.group({
  digit1: ['', [Validators.required, this.onlyNumbersValidator()]]
});

Manejo de Eventos

// Prevenir navegación automática en ciertos casos
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
  // Prevenir tab automático con tecla Backspace
  if (event.key === 'Backspace' && !this.currentInput.value) {
    event.preventDefault();
    this.previousInput.focus();
  }
}

Estados de Error

  • Campo vacío: La directiva no interviene
  • Longitud incorrecta: Se maneja con validaciones de formulario
  • Tipo de dato inválido: Compatible con validaciones personalizadas

🔧 Configuración Avanzada

Personalización del Comportamiento

// Extensión de la directiva para casos específicos
@Directive({
  selector: '[cbmAutoTabCustom]',
  standalone: true,
})
export class CbmAutoTabCustomDirective extends CbmAutoTabDirective {
  @Input() autoFocusDelay = 0;
  @Input() enableBackNavigation = true;

  override onInput(event: Event) {
    super.onInput(event);

    // Lógica personalizada adicional
    if (this.autoFocusDelay > 0) {
      setTimeout(() => {
        this.nextInput.focus();
      }, this.autoFocusDelay);
    }
  }
}

Integración con Librerías de Máscara

// Compatible con ngx-mask o text-mask
@Component({
  template: `
    <input #maskedInput
           type="text"
           mask="000-000-0000"
           [cbmAutoTab]="nextInput">
  `
})
export class MaskedInputComponent {
  // La directiva funciona con campos enmascarados
  // El foco se mueve cuando se completa la máscara
}

Configuración Programática

export class AutoTabService {
  private tabGroups = new Map<string, HTMLElement[]>();

  registerTabGroup(groupId: string, inputs: HTMLElement[]) {
    this.tabGroups.set(groupId, inputs);
  }

  navigateToNext(groupId: string, currentInput: HTMLElement) {
    const group = this.tabGroups.get(groupId);
    if (group) {
      const currentIndex = group.indexOf(currentInput);
      if (currentIndex < group.length - 1) {
        group[currentIndex + 1].focus();
      }
    }
  }
}

📋 Dependencias

Peer Dependencies (Requeridas)

{
  "@angular/common": ">=20.1.5",
  "@angular/core": ">=20.1.5"
}

Dependencias Internas

{
  "tslib": "^2.3.0"
}

🛠️ Desarrollo

Estructura del Proyecto

auto-tab-directive/
├── src/
│   ├── lib/
│   │   ├── auto-tab.directive.ts       # Directiva principal
│   │   ├── auto-tab.directive.spec.ts  # Pruebas unitarias
│   │   └── index.ts                    # Exportaciones
│   └── public-api.ts                   # API pública
├── ng-package.json                     # Configuración empaquetado
├── package.json                        # Dependencias
└── README.md                           # Esta documentación

Construcción

# Construir la librería
ng build auto-tab-directive

# Construir en modo watch
ng build auto-tab-directive --watch

# Construir para producción
ng build auto-tab-directive --configuration production

Pruebas

# Ejecutar pruebas unitarias
ng test auto-tab-directive

# Ejecutar pruebas con coverage
ng test auto-tab-directive --code-coverage

# Pruebas end-to-end
ng e2e auto-tab-directive

🎯 Mejores Prácticas

1. Diseño de UX

// Agrupar inputs relacionados visualmente
<div class="input-group">
  <input class="grouped-input" [cbmAutoTab]="nextInput">
  <input class="grouped-input" [cbmAutoTab]="nextInput">
</div>

// Usar estilos consistentes
.grouped-input {
  border-radius: 0;
  border-right: none;
}

.grouped-input:first-child {
  border-radius: 4px 0 0 4px;
}

.grouped-input:last-child {
  border-radius: 0 4px 4px 0;
  border-right: 1px solid #ddd;
}

2. Accesibilidad

// Asegurar navegación por teclado
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
  switch (event.key) {
    case 'ArrowRight':
      this.navigateNext();
      break;
    case 'ArrowLeft':
      this.navigatePrevious();
      break;
    case 'Enter':
      this.submitForm();
      break;
  }
}

// Soporte para lectores de pantalla
<input
  [attr.aria-label]="'Dígito ' + (index + 1) + ' del código'"
  [attr.aria-describedby]="'code-description'">

3. Optimización de Rendimiento

// Debounce para eventos de input
private inputDebounceTimer?: number;

@HostListener('input', ['$event'])
onInput(event: Event) {
  clearTimeout(this.inputDebounceTimer);
  this.inputDebounceTimer = window.setTimeout(() => {
    this.handleAutoTab(event);
  }, 100);
}

// Cleanup en ngOnDestroy
ngOnDestroy() {
  if (this.inputDebounceTimer) {
    clearTimeout(this.inputDebounceTimer);
  }
}

4. Manejo de Estados Complejos

// Para formularios con lógica condicional
export class ConditionalAutoTabDirective extends CbmAutoTabDirective {
  @Input() condition: () => boolean = () => true;

  override onInput(event: Event) {
    if (this.condition()) {
      super.onInput(event);
    }
  }
}

// Uso
<input [cbmAutoTab]="nextInput"
       [condition]="() => formValid && !loading">

🤝 Contribución

  1. Fork el repositorio
  2. Crea una rama para tu feature (git checkout -b feature/nueva-funcionalidad)
  3. Commit tus cambios (git commit -am 'Agrega nueva funcionalidad')
  4. Push a la rama (git push origin feature/nueva-funcionalidad)
  5. Abre un PullRequest

📄 Licencia

Este proyecto está bajo la Licencia MIT - ver el archivo LICENSE para más detalles.

📞 Soporte

Para soporte técnico o reportes de bugs, contacta al equipo de desarrollo de CBM:

🔄 Changelog

v0.0.1

  • ✅ Directiva standalone para navegación automática entre inputs
  • ✅ Navegación bidireccional (adelante/atrás)
  • ✅ Detección automática de longitud máxima
  • ✅ Compatibilidad con formularios reactivos
  • ✅ Integración con validaciones existentes
  • ✅ Soporte para accesibilidad y navegación por teclado
  • ✅ Documentación completa en español

Nota: Esta directiva mejora significativamente la experiencia de usuario en formularios con campos secuenciales, especialmente útil para códigos de verificación, números de teléfono, fechas y números de tarjetas de crédito.