Saltar al contenido principal

TdnInputPin

Paquete: @tiendana/ngx-core/forms

Entrada de códigos cortos casilla por casilla: PIN de caja, código de un solo uso (OTP) por SMS, código de emparejamiento. Es un ControlValueAccessor que dibuja un grupo etiquetado de casillas de un carácter.

Para texto libre usa un input normal. Mientras se verifica un código ya enviado, cámbialo por tdn-code-checking, que dibuja la misma fila de celdas.

Características​

✅ Longitud configurable - 4, 6, 8 o las casillas que necesites
✅ Solo dígitos o alfanumérico - dígitos por defecto; con onlyNumber desactivado acepta letras ASCII y dígitos
✅ Pegado y autocompletado - pegado, autocompletado del sistema, chips de sugerencia del teclado e IME reparten el código entre las casillas
✅ Máscara opcional - isCodeHidden dibuja un glifo sobre cada casilla llena en lugar del punto del navegador
✅ Teclado completo - tabindex itinerante, flechas, Inicio, Fin y Retroceso
✅ Accesible - role="group", nombre de grupo, descripción y estado inválido
✅ Valor de todo o nada - el formulario recibe null hasta que todas las casillas están llenas

Selector​

<tdn-input-pin />

API​

Inputs​

Todos son signal inputs.

PropiedadTipoDescripciónPor defecto
settingsTdnInputPinSettingsConfiguración parcial, combinada sobre TDN_PIN_INPUT_DEFAULT_SETTINGSundefined
codeLengthnumberNúmero de casillas (y de caracteres del valor). Gana sobre settings4
onlyNumberbooleanSolo dígitos (true) o letras ASCII y dígitos (false). Gana sobre settingstrue
isCodeHiddenbooleanCasillas type="password" con el glifo de máscara sobre cada casilla llena. Gana sobre settingsfalse
autocompletestringAtributo autocomplete de cada casilla'chrome-off'
labelstringNombre accesible del grupo (aria-label), ya traducidoundefined
describedBystringIds separados por espacios que describen el código; se escriben en el aria-describedby del grupoundefined
invalidbooleanMarca cada casilla con aria-invalid="true" y el grupo con tdn-input-pin--invalidfalse

Outputs​

EventoTipoDescripción
focusvoidEl foco entra al grupo. Se emite una vez por grupo, no por casilla
blurvoidEl foco sale del grupo. Se emite una vez por grupo, no por casilla

Métodos​

MétodoDescripción
setFocus()Enfoca la primera casilla vacía, o la primera si el código está completo. Así quien vuelve a un código a medias no pisa un dígito ya escrito

Clases del host​

tdn-input-pin, tdn-input-pin--active (el foco está dentro), tdn-input-pin--disabled y tdn-input-pin--invalid. Las casillas son .tdn-input-pin__input dentro de .tdn-input-pin__wrapper; la máscara es .tdn-input-pin__mask. El aspecto viene de @tiendana/web-foundation.

Valor del control​

El valor es una cadena con los caracteres de todas las casillas, o null hasta que todas estén llenas.

  • "0123" conserva el cero inicial, incluso con onlyNumber. No lo conviertas a número.
  • Solo un cambio de ese valor llega al formulario: un código parcial no emite nada y deja el control pristine.
  • null llega también cuando se acorta un código completo.
minLength nunca se dispara

Como el valor es null mientras el código está incompleto, Validators.minLength(n) nunca ve un código parcial. Quien informa de un código incompleto es Validators.required.

Reglas de entrada​

  • Dígitos por defecto (inputmode="numeric"). Con onlyNumber desactivado, letras ASCII y dígitos.
  • Escribir sobre una casilla llena reemplaza su carácter y avanza.
  • Pegado, autocompletado del sistema, chips de sugerencia del teclado e IME: el código se reparte desde la casilla donde cae, empezando antes si desbordara, de modo que un código completo siempre queda desde la casilla 1.
  • Antes de repartir se descartan espacios, guiones y puntos: 123 456, 12-34-56 y 1.234 se aceptan.
  • Un fragmento más largo que el código, o con otros caracteres, se rechaza entero. Nunca se extraen dígitos de una frase (Código 123456 se rechaza).
  • Presionar una casilla vacía posterior a la primera vacía mueve el foco a la primera vacía; presionar una casilla llena la mantiene, para corregir un carácter.
  • Una composición IME se confirma una sola vez, no una por casilla.

Teclado​

TeclaAcción
Tab / Shift+TabEntra y sale del grupo una sola vez (tabindex itinerante: la casilla enfocada, o la primera vacía)
← / →Casilla anterior / siguiente
Inicio / FinPrimera / última casilla
RetrocesoEn una casilla vacía, borra la anterior y se mueve a ella

Configuración​

Interfaces TdnInputPinSettings, TdnInputPinIcons y TdnInputPinTranslations
interface TdnInputPinSettings {
codeLength?: number; // Default: 4
onlyNumber?: boolean; // Default: true
isCodeHidden?: boolean; // Default: false
icons?: Partial<TdnInputPinIcons>;
translations?: Partial<TdnInputPinTranslations>;
}

interface TdnInputPinIcons {
// Glifo dibujado sobre cada casilla llena mientras isCodeHidden está activo.
// SVG/HTML como cadena (se confía tal cual) o SafeHtml.
mask: string | SafeHtml; // Default: TDN_ICON_PASSWORD_MASK de @tiendana/utils
}

interface TdnInputPinTranslations {
// {position} y {length} se reemplazan por la posición (desde 1) y la longitud del código.
digit: string; // Default: 'Dígito {position} de {length}'
character: string; // Default: 'Carácter {position} de {length}'
}
nota

icons.mask no es un botón de mostrar u ocultar: el componente no tiene ese botón. Es el glifo que se dibuja sobre cada casilla llena cuando isCodeHidden está activo.

Valores predeterminados​

{
codeLength: 4,
onlyNumber: true,
isCodeHidden: false,
icons: { mask: TDN_ICON_PASSWORD_MASK },
translations: {
digit: 'Dígito {position} de {length}',
character: 'Carácter {position} de {length}'
}
}

Ejemplos y demos​

Uso básico (4 dígitos)​

Cargando demo...
Ver código

PIN de 6 dígitos​

Cargando demo...
Ver código

PIN de 8 dígitos​

Cargando demo...
Ver código

Código oculto​

Cargando demo...
Ver código

Alfanumérico​

Cargando demo...
Ver código

Configuración con settings​

Cargando demo...
Ver código

Con validación​

Cargando demo...
Ver código
touched llega con la primera pulsación

El componente marca el control como touched al primer carácter. Un mensaje required con when="touched" aparece, por tanto, mientras el código aún se escribe. Si no quieres eso, gobierna el estado visual con invalid (ver más abajo) y revela los errores al enviar con FormRevealService.

Validación personalizada​

Cargando demo...
Ver código

Ejemplo completo​

Cargando demo...
Ver código

Pantalla de verificación​

Para un código de un solo uso, activa el autocompletado del sistema, nombra el grupo y marca el control como inválido cuando el servidor rechace el código. Pasa el id del bloque de errores a describedBy: el aria-describedby automático de tdnControl no llega a las casillas.

<tdn-form-element>
<tdn-input-pin #pin tdnControl formControlName="otp" [codeLength]="6"
autocomplete="one-time-code" label="Código de verificación"
[invalid]="rejected()"
[describedBy]="errors.showing() ? errors.elementId() : ''" />
<div [tdnErrors]="'otp'" #errors="tdnErrors">
<span tdnError="rejected">El código no es válido o ya venció</span>
</div>
</tdn-form-element>
readonly pin = viewChild.required<TdnInputPinComponent>('pin');
readonly rejected = signal(false);

onRejected(): void {
this.form.controls.otp.setErrors({ rejected: true });
this.rejected.set(true);
this.pin().setFocus();
}
invalid no se deriva del control

invalid nunca se calcula a partir del control: el componente lo marca touched con la primera pulsación y un indicador derivado pintaría como inválido un código que aún se escribe. Actívalo tú, al recibir un rechazo del servidor o al enviar.

autocomplete

El valor por defecto chrome-off evita que el sistema ofrezca códigos SMS (útil para un PIN de caja). En pantallas de verificación pon autocomplete="one-time-code"; de lo contrario el autocompletado nunca llega a las casillas.

Buenas prácticas​

Hacer​

  • Usa isCodeHidden para PINs sensibles.
  • Usa Validators.required para exigir el código completo y, si hace falta, un validador de patrón sobre la cadena completa.
  • Pon autocomplete="one-time-code" y label en las pantallas de verificación.
  • Usa la longitud que corresponda al contexto (4 para PIN de caja, 6 para verificación).
  • Valida también en el servidor.

Evitar​

  • No uses minLength para detectar un código incompleto: nunca lo ve.
  • No conviertas el valor a número: perderías los ceros iniciales.
  • No guardes PINs en texto plano ni los escribas en logs.
  • No deshabilites el botón de envío con form.invalid: revela los errores al enviar con FormRevealService.

Accesibilidad​

  • El host es role="group", nombrado con label y descrito con describedBy.
  • Cada casilla se nombra «Dígito 3 de 6» («Carácter 3 de 6» con onlyNumber desactivado), a partir de TdnInputPinTranslations.
  • Con invalid activo, cada casilla lleva aria-invalid="true".
  • Con tabindex itinerante, Tab entra y sale del grupo una sola vez.
  • focus y blur se emiten una vez por grupo.

Personalización avanzada​

Token global​

TDN_PIN_INPUT_DEFAULT_SETTINGS (InjectionToken<TdnInputPinSettingsInternal>, providedIn: 'root') configura los valores de todos los tdn-input-pin. Un proveedor reemplaza el objeto completo: entrega todos los campos obligatorios, no solo los que cambian.

import {
TDN_PIN_INPUT_DEFAULT_SETTINGS,
TdnInputPinSettingsInternal
} from '@tiendana/ngx-core/forms';
import { TDN_ICON_PASSWORD_MASK } from '@tiendana/utils';

bootstrapApplication(AppComponent, {
providers: [
{
provide: TDN_PIN_INPUT_DEFAULT_SETTINGS,
useValue: {
codeLength: 6,
onlyNumber: true,
isCodeHidden: false,
icons: { mask: TDN_ICON_PASSWORD_MASK },
translations: {
digit: 'Digit {position} of {length}',
character: 'Character {position} of {length}'
}
} satisfies TdnInputPinSettingsInternal
}
]
});

Textos por defecto​

TDN_PIN_INPUT_TRANSLATIONS exporta los textos accesibles por defecto, en español. Sirve de base cuando ni el token ni settings.translations los entregan.

Alcance de componente​

El token también puede proveerse en un componente: todos los tdn-input-pin de su vista heredan esa configuración.

@Component({
selector: 'app-security-section',
providers: [
{
provide: TDN_PIN_INPUT_DEFAULT_SETTINGS,
useValue: {
codeLength: 8,
onlyNumber: true,
isCodeHidden: true,
icons: { mask: TDN_ICON_PASSWORD_MASK }
} satisfies TdnInputPinSettingsInternal
}
],
template: `<!-- Los tdn-input-pin de aquí heredan esta configuración -->`
})
export class SecuritySectionComponent {}

Componentes relacionados​