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.
| Propiedad | Tipo | Descripción | Por defecto |
|---|---|---|---|
settings | TdnInputPinSettings | Configuración parcial, combinada sobre TDN_PIN_INPUT_DEFAULT_SETTINGS | undefined |
codeLength | number | Número de casillas (y de caracteres del valor). Gana sobre settings | 4 |
onlyNumber | boolean | Solo dígitos (true) o letras ASCII y dígitos (false). Gana sobre settings | true |
isCodeHidden | boolean | Casillas type="password" con el glifo de máscara sobre cada casilla llena. Gana sobre settings | false |
autocomplete | string | Atributo autocomplete de cada casilla | 'chrome-off' |
label | string | Nombre accesible del grupo (aria-label), ya traducido | undefined |
describedBy | string | Ids separados por espacios que describen el código; se escriben en el aria-describedby del grupo | undefined |
invalid | boolean | Marca cada casilla con aria-invalid="true" y el grupo con tdn-input-pin--invalid | false |
Outputs
| Evento | Tipo | Descripción |
|---|---|---|
focus | void | El foco entra al grupo. Se emite una vez por grupo, no por casilla |
blur | void | El foco sale del grupo. Se emite una vez por grupo, no por casilla |
Métodos
| Método | Descripció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 cononlyNumber. 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. nullllega también cuando se acorta un código completo.
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"). CononlyNumberdesactivado, 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-56y1.234se 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 123456se 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
| Tecla | Acción |
|---|---|
Tab / Shift+Tab | Entra y sale del grupo una sola vez (tabindex itinerante: la casilla enfocada, o la primera vacía) |
← / → | Casilla anterior / siguiente |
Inicio / Fin | Primera / última casilla |
Retroceso | En 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}'
}
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)
Ver código
PIN de 6 dígitos
Ver código
PIN de 8 dígitos
Ver código
Código oculto
Ver código
Alfanumérico
Ver código
Configuración con settings
Ver código
Con validación
Ver código
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
Ver código
Ejemplo completo
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 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.
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
isCodeHiddenpara PINs sensibles. - Usa
Validators.requiredpara exigir el código completo y, si hace falta, un validador de patrón sobre la cadena completa. - Pon
autocomplete="one-time-code"ylabelen 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
minLengthpara 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 conlabely descrito condescribedBy. - Cada casilla se nombra «Dígito 3 de 6» («Carácter 3 de 6» con
onlyNumberdesactivado), a partir deTdnInputPinTranslations. - Con
invalidactivo, cada casilla llevaaria-invalid="true". - Con
tabindexitinerante, Tab entra y sale del grupo una sola vez. focusyblurse 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
- TdnFormElement - Contenedor de campos de formulario
- tdnControl - Seguimiento de estado con signals
- tdn-code-checking - Animación mientras se verifica el código
- FormRevealService - Revela y enfoca el primer campo inválido al enviar