TdnSelect
Paquete: @tiendana/ngx-core/forms
Componente dropdown altamente configurable para selección única o múltiple con búsqueda, paginación, agrupación y plantillas personalizadas.
Características
- ✅ Selección única o múltiple: Control completo del modo de selección
- ✅ Búsqueda integrada: Filtrado en tiempo real con debounce
- ✅ Paginación remota: Carga lazy con scroll infinito
- ✅ Agrupación: Organiza items por categorías
- ✅ Plantillas personalizadas: Personaliza items y badges
- ✅ Seleccionar/Deseleccionar todo: Para selección múltiple
- ✅ Límite de selección: Define cantidad máxima de items
- ✅ Lista fuera del formulario: La lista abierta se aloja en
document.body, así ninguna animación ni contenedor la recorta - ✅ Ancho configurable: Variables CSS para ensanchar la lista respecto al trigger
- ✅ Responsive: Hoja a pantalla completa en teléfonos
- ✅ Escape cierra la lista (con
escapeToClose)
API
Inputs
| Propiedad | Tipo | Default | Descripción |
|---|---|---|---|
data | any[] | [] | Array de objetos a mostrar en el dropdown |
settings | TdnSelectSettings | Ver configuración | Objeto de configuración completo |
Atributos HTML
| Atributo | Tipo | Descripción |
|---|---|---|
multiple | - | Activa modo multi-selección (singleSelection: false) |
simple | - | Modo simple sin búsqueda ni limpiar (singleSelection: true, sin filtros) |
primaryKey | string | Sobrescribe la clave primaria de los objetos (default: 'id') |
labelKey | string | Sobrescribe la clave del label (default: 'name') |
noResponsive | - | Desactiva modo fullscreen en móviles |
groupBy | string | Agrupa items por esta propiedad |
noClear | - | Oculta el botón limpiar |
controls-in-column | - | Dispone controles en columna en lugar de fila |
tdnControl | Directive | Conecta con FormControl y activa estados visuales |
formControlName | string | Nombre del control en un FormGroup |
[formControl] | FormControl | Referencia directa a un FormControl |
Outputs
| Evento | Tipo | Descripción |
|---|---|---|
onSelect | any | Emite cuando se selecciona un item |
onDeSelect | any | Emite cuando se deselecciona un item |
onClear | any[] | Emite cuando se limpia la selección |
onSelectAll | any[] | Emite cuando se seleccionan todos los items |
onDeSelectAll | any[] | Emite cuando se deseleccionan todos |
onOpen | boolean | Emite cuando se abre el dropdown |
onClose | boolean | Emite cuando se cierra el dropdown |
onScrollToEnd | any | Emite cuando se hace scroll al final (para paginación) |
onSearch | void | Emite cuando se realiza una búsqueda |
focus | void | Emite cuando recibe foco |
blur | void | Emite cuando pierde foco |
Configuración
Interfaz TdnSelectSettings
interface TdnSelectSettings {
singleSelection?: boolean; // true = selección única, false = múltiple (default: true)
enableCheckAll?: boolean; // Mostrar "Seleccionar todo" (default: true). Se oculta en selección única, con remote o con limitSelection
enableSearchFilter?: boolean; // Activar búsqueda (default: true)
searchBy?: string[]; // Campos por los que buscar (vacío = todos los strings)
maxHeight?: number; // Altura máxima de la lista en px (default: 300)
badgeShowLimit?: number; // Límite de badges a mostrar (default: Infinity)
classes?: string; // Clases CSS adicionales
limitSelection?: number; // Cantidad máxima de selección (0 = sin límite)
showCheckbox?: boolean; // Mostrar checkboxes (default: true)
labelKey?: string; // Clave del label (default: 'name')
primaryKey?: string; // Clave primaria (default: 'id')
disabledKey?: string; // Clave para deshabilitar items (default: 'disabled')
remote?: boolean; // Activar modo remoto con paginación
pagination?: Partial<TdnSelectPagination>; // Configuración de paginación remota
groupBy?: string; // Agrupar por esta propiedad
escapeToClose?: boolean; // Cerrar con Escape (default: true)
clearAll?: boolean; // Mostrar botón limpiar (default: true)
scrollEndGap?: number; // Píxeles para trigger de scroll infinito (default: 200)
dropdownMobileFixed?: boolean; // Fullscreen en móviles (default: true)
controlsColumn?: boolean; // Controles en columna (default: false)
icons?: Partial<TdnSelectIcons>;
translations?: Partial<TdnSelectTranslations>;
canItemSelected?: (settings, selectedItem, additionalData) => boolean; // Función para validar selección
}
interface TdnSelectPagination {
pageSize: number; // Default: 30
rowCountPath: string[];
hasNextPagePath: string[];
listPath: string[];
getList: (state: TdnSelectPaginationState, selectedItems?: any[]) => Observable<any>;
}
interface TdnSelectPaginationState {
pageSize: number;
currentPage: number;
searchTerm: string;
hasNextPage: boolean;
rowCount: number;
}
interface TdnSelectIcons {
clear: string | SafeHtml; // Ícono del botón limpiar
chevron: string | SafeHtml; // Ícono del dropdown
search: string | SafeHtml; // Ícono de búsqueda
arrowBack: string | SafeHtml; // Ícono de regresar (modo móvil)
}
interface TdnSelectTranslations {
selectAllText: string; // Default: 'Seleccionar todo'
unSelectAllText: string; // Default: 'Deseleccionar todo'
filterSelectAllText: string; // Default: 'Seleccionar todos los resultados filtrados' (no se muestra)
filterUnSelectAllText: string; // Default: 'Deseleccionar todos los resultados filtrados' (no se muestra)
searchPlaceholderText: string; // Default: 'Buscar'
noDataLabel: string; // Default: 'Sin datos disponibles'
addNewButtonText: string; // Default: 'Agregar' (no se muestra)
}
filterSelectAllText, filterUnSelectAllText y addNewButtonText existen en la interfaz pero el componente nunca los muestra. searchAutofocus no forma parte de los valores por defecto ni se lee en ninguna parte: el campo de búsqueda recibe el foco siempre que enableSearchFilter está activo.
Ejemplos y Demos
Selección Única
Ver código
Selección Múltiple
Ver código
Modo Simple (Sin Búsqueda)
Ver código
Con Búsqueda Personalizada
Ver código
Con Límite de Selección
Ver código
Con Agrupación
Ver código
Con Validación Required
Ver código
Validación Personalizada (Mínimo de Selecciones)
Ver código
Eventos del Componente
Ver código
Plantilla Personalizada (Solo Items)
Ver código
Plantilla Personalizada (Solo Badges)
Ver código
Plantilla Personalizada (Items y Badges Diferentes)
Ver código
Plantilla Unificada (badgeListItem)
Cuando la plantilla del dropdown y el badge es la misma, usa #badgeListItem en lugar de definir ambas por separado.
Ver código
Paginación Remota
Ver código
Ejemplo Completo
Ver código
La lista abierta vive en el <body>
Mientras está abierta, la lista (.tdn-select__dropdown-list) se mueve a document.body, queda fija al viewport y se posiciona a partir del rectángulo del trigger. Al cerrarse, o al destruirse el componente, sale del body. El motivo: cualquier ancestro con transform, filter, perspective o contención (basta una animación de entrada que deje su estado final) se convierte en el bloque contenedor de un elemento fijo y lo recortaría o lo movería.
- Se abre debajo del trigger si la ventana tiene espacio y encima en caso contrario, con las clases
tdn-select--ontop(host) ytdn-select__dropdown-list--ontop(lista). - Se reposiciona al cambiar el tamaño de la ventana y con el scroll del documento o de cualquier ancestro con scroll.
- Un clic dentro de la lista cuenta como clic dentro del select y no se propaga: los listeners de
documentywindowno lo ven, de modo que un panel que se cierra con clics externos sigue abierto mientras eliges una opción. - Se cierra con un clic externo, con
Escape(siescapeToClose) o, en ventanas de más de 600 px, cuando el trigger sale de la vista por scroll.
Como la lista abierta no es descendiente del select, no falla nada con un error: simplemente la regla no aplica.
- Un selector como
.mis-filtros .tdn-select__dropdown-listotdn-select.ancho .tdn-select__dropdown-listno coincide mientras está abierta. Estiliza la lista con un selector que se sostenga solo. - Las custom properties no se heredan desde donde está el select, salvo
--tdn-select-dropdown-width. settings.classesse aplica solo al host, no a la lista.- No fijes
widthinline ni con!importanten la lista: pisa las dos variables de ancho.
Ancho de la lista
El ancho de la lista es var(--tdn-select-dropdown-width, var(--tdn-select-trigger-width, auto)), declarado bajo :where() (especificidad cero), así que cualquier regla de la app gana sin importar el orden de carga.
| Variable | Quién la escribe | Efecto |
|---|---|---|
--tdn-select-trigger-width | El componente, sobre la lista, en cada posicionamiento | Ancho del trigger en px. Es el ancho por defecto |
--tdn-select-dropdown-width | La app, en el host tdn-select o en cualquier ancestro | Lista más ancha o más angosta que el trigger. El componente lee el valor computado en el host y lo copia a la lista |
Ver código
Hoja a pantalla completa en teléfonos
Salvo noResponsive (o dropdownMobileFixed: false), la lista lleva la clase tdn-select__dropdown-list--responsive, que @tiendana/web-foundation convierte en una hoja a pantalla completa en teléfonos. Ese estilo requiere web-foundation 0.5.4 o superior; con una versión anterior la clase no estiliza nada. Con noResponsive el host lleva tdn-select--no-responsive.
Clases del host
tdn-select, tdn-select--no-responsive, tdn-select--disabled, tdn-select--active, tdn-select--multiple, tdn-select--controls-column y tdn-select--ontop.
Plantillas: contexto
Las plantillas reciben un objeto de contexto, no un valor implícito. Declara las variables con su nombre: con let-item a secas, item queda undefined.
| Plantilla | Contexto | Uso |
|---|---|---|
#listItem | { item, searchTerm } | let-item="item" let-searchTerm="searchTerm" |
#badge | { item } | let-item="item" |
#badgeListItem | { item, searchTerm } | let-item="item" let-searchTerm="searchTerm" |
#badgeListItem tiene prioridad sobre #listItem y #badge; si defines #listItem y #badge juntas, cada zona se renderiza por separado.
Funcionalidades Avanzadas
Paginación Remota
Para listas grandes, usa paginación con scroll infinito:
settings: TdnSelectSettings = {
remote: true,
pagination: {
pageSize: 30,
rowCountPath: ['totalCount'],
hasNextPagePath: ['hasMore'],
listPath: ['data'],
getList: (state, selectedItems) => {
return this.apiService.getItems({
page: state.currentPage,
pageSize: state.pageSize,
search: state.searchTerm
});
}
}
};
Plantillas Personalizadas
Personaliza el renderizado de items y badges:
<tdn-select [data]="users" [formControl]="selectedUsers">
<ng-template #listItem let-item="item">
<div class="custom-item">
<img [src]="item.avatar" alt="">
<span>{{ item.name }}</span>
</div>
</ng-template>
<ng-template #badge let-item="item">
<span class="custom-badge">{{ item.name }}</span>
</ng-template>
</tdn-select>
Validación de Selección Personalizada
Usa canItemSelected para validar si un item puede ser seleccionado:
settings: TdnSelectSettings = {
canItemSelected: (settings, selectedItem, additionalData) => {
return selectedItem.stock > 0; // Solo items con stock
}
};
Buenas Prácticas
✅ Hacer
- Usar
multipleattribute para multi-selección en lugar de configurar en settings - Definir
primaryKeyylabelKeycuando los objetos no usen 'id' y 'name' - Usar paginación remota para listas con más de 100 items
- Agrupar items cuando hay muchas categorías
- Validar con
minLengthArraypara multi-selección requerida - Proporcionar un
translations.searchPlaceholderTextdescriptivo
❌ Evitar
- Cargar miles de items sin paginación
- Omitir validación en multi-selección obligatoria
- Usar selección múltiple sin límite en contextos restrictivos
- Deshabilitar búsqueda en listas largas
Accesibilidad
- ✅ Label asociado con
<label>dentro de<tdn-form-element> - ✅ El trigger lleva
data-tdn-aria-target:tdnControlescribe en élaria-describedbyyaria-invalid - ✅ El trigger es enfocable (
tabindex="0"), pero la lista se abre con clic: no hay navegación por teclado entre opciones - ✅ Escape cierra la lista (con
escapeToClose) - ✅ Estados visuales para disabled, touched, valid
- ✅ Mensajes de error claros
Personalización Avanzada
Token de Configuración Global
Puedes configurar valores por defecto para todos los componentes TdnSelect usando el token TDN_SELECT_DEFAULT_SETTINGS_TOKEN. Los valores integrados están exportados como TDN_SELECT_DEFAULT_SETTINGS; la fábrica del token los clona (icons, translations y pagination), de modo que el valor del token no comparte objetos con la constante exportada TDN_SELECT_DEFAULT_SETTINGS. Un proveedor reemplaza el objeto completo; parte de TDN_SELECT_DEFAULT_SETTINGS.
Configuración Global
import { ApplicationConfig } from '@angular/core';
import {
TDN_SELECT_DEFAULT_SETTINGS,
TDN_SELECT_DEFAULT_SETTINGS_TOKEN
} from '@tiendana/ngx-core/forms';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: TDN_SELECT_DEFAULT_SETTINGS_TOKEN,
useValue: {
...TDN_SELECT_DEFAULT_SETTINGS,
singleSelection: true,
enableCheckAll: true,
translations: {
...TDN_SELECT_DEFAULT_SETTINGS.translations,
searchPlaceholderText: 'Buscar...'
},
maxHeight: 300,
badgeShowLimit: 5,
showCheckbox: true,
enableSearchFilter: true,
clearAll: true,
escapeToClose: true,
dropdownMobileFixed: true
}
}
]
};
Componentes Relacionados
- TdnFormElement - Wrapper para estilos consistentes
- TdnInputTag - Para entrada libre de múltiples valores
- tdnControl - Seguimiento de estado con signals
- Select (web-foundation) - Estilos del componente