Saltar al contenido principal

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​

PropiedadTipoDefaultDescripción
dataany[][]Array de objetos a mostrar en el dropdown
settingsTdnSelectSettingsVer configuraciónObjeto de configuración completo

Atributos HTML​

AtributoTipoDescripción
multiple-Activa modo multi-selección (singleSelection: false)
simple-Modo simple sin búsqueda ni limpiar (singleSelection: true, sin filtros)
primaryKeystringSobrescribe la clave primaria de los objetos (default: 'id')
labelKeystringSobrescribe la clave del label (default: 'name')
noResponsive-Desactiva modo fullscreen en móviles
groupBystringAgrupa items por esta propiedad
noClear-Oculta el botón limpiar
controls-in-column-Dispone controles en columna en lugar de fila
tdnControlDirectiveConecta con FormControl y activa estados visuales
formControlNamestringNombre del control en un FormGroup
[formControl]FormControlReferencia directa a un FormControl

Outputs​

EventoTipoDescripción
onSelectanyEmite cuando se selecciona un item
onDeSelectanyEmite cuando se deselecciona un item
onClearany[]Emite cuando se limpia la selección
onSelectAllany[]Emite cuando se seleccionan todos los items
onDeSelectAllany[]Emite cuando se deseleccionan todos
onOpenbooleanEmite cuando se abre el dropdown
onClosebooleanEmite cuando se cierra el dropdown
onScrollToEndanyEmite cuando se hace scroll al final (para paginación)
onSearchvoidEmite cuando se realiza una búsqueda
focusvoidEmite cuando recibe foco
blurvoidEmite 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)
}
Ajustes sin efecto

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​

Cargando demo...
Ver código

Selección Múltiple​

Cargando demo...
Ver código

Modo Simple (Sin Búsqueda)​

Cargando demo...
Ver código

Con Búsqueda Personalizada​

Cargando demo...
Ver código

Con Límite de Selección​

Cargando demo...
Ver código

Con Agrupación​

Cargando demo...
Ver código

Con Validación Required​

Cargando demo...
Ver código

Validación Personalizada (Mínimo de Selecciones)​

Cargando demo...
Ver código

Eventos del Componente​

Cargando demo...
Ver código

Plantilla Personalizada (Solo Items)​

Cargando demo...
Ver código

Plantilla Personalizada (Solo Badges)​

Cargando demo...
Ver código

Plantilla Personalizada (Items y Badges Diferentes)​

Cargando demo...
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.

Cargando demo...
Ver código

Paginación Remota​

Cargando demo...
Ver código

Ejemplo Completo​

Cargando demo...
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) y tdn-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 document y window no 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 (si escapeToClose) o, en ventanas de más de 600 px, cuando el trigger sale de la vista por scroll.
Trampas al estilizar la lista

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-list o tdn-select.ancho .tdn-select__dropdown-list no 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.classes se aplica solo al host, no a la lista.
  • No fijes width inline ni con !important en 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.

VariableQuién la escribeEfecto
--tdn-select-trigger-widthEl componente, sobre la lista, en cada posicionamientoAncho del trigger en px. Es el ancho por defecto
--tdn-select-dropdown-widthLa app, en el host tdn-select o en cualquier ancestroLista 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
Cargando demo...
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.

PlantillaContextoUso
#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 multiple attribute para multi-selección en lugar de configurar en settings
  • Definir primaryKey y labelKey cuando 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 minLengthArray para multi-selección requerida
  • Proporcionar un translations.searchPlaceholderText descriptivo

❌ 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: tdnControl escribe en él aria-describedby y aria-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​