TdnInputDate
Paquete: @tiendana/ngx-core/forms
Componente de selector de fecha con calendario desplegable, soporte para rangos, integración con selector de hora y estilos consistentes con el Sistema de Diseño Tiendana.
Características
- ✅ Selector de fecha simple o rango de fechas
- ✅ Integración con
TdnInputTimepara selección de fecha y hora - ✅ Calendario desplegable que se aloja en
document.bodymientras está abierto, con posicionamiento inteligente - ✅ Formateado de fechas a través de
TDN_DATE_FORMATTER - ✅ Soporte para validaciones de Angular Reactive Forms
- ✅ Botón de limpieza opcional
- ✅ Reposiciona el calendario con el scroll y el redimensionado
- ✅ Modo fullscreen cuando no cabe ni arriba ni abajo
- ✅ Localización en español por defecto
- ✅ Implementa
ControlValueAccessor
Selector
tdn-input-date
El componente inyecta TDN_DATE_FORMATTER sin ser opcional y el token no tiene fábrica: si la aplicación no lo provee, se lanza NullInjectorError al crear el componente. Toda aplicación debe proveerlo. Ver Personalización avanzada.
Uso Básico
Ver código
Inputs
| Input | Tipo | Descripción |
|---|---|---|
settings | TdnDateInputSettings | Configuración del componente (ver configuración disponible) |
timeInput | TdnInputTimeComponent | Referencia a componente de tiempo para selección de fecha/hora |
Outputs
| Output | Tipo | Descripción |
|---|---|---|
onSelect | Date | DateRangeResult | null | Se emite cuando cambia la selección: Date, { from, to } en modo rango, o null |
onDeSelect | any | Declarado por paridad con tdn-select; el componente nunca lo emite |
onClear | any | Se emite cuando se limpia la selección |
onOpen | boolean | Se emite cuando se abre el dropdown |
onClose | boolean | Se emite cuando se cierra el dropdown |
focus | void | Se emite al abrir el calendario |
blur | void | Se emite al cerrarlo |
Métodos públicos
openDropdown(), closeDropdown(), toggleDropdown(e), clearSelection(e), emitValue() y setPositionFixedDropdown().
Clases del host
tdn-input-date, disabled, active y ontop (el calendario abre por encima del campo), más las de settings.classes.
Atributos HTML
| Atributo | Descripción |
|---|---|
range | Habilita modo de rango de fechas |
time | Habilita selector de tiempo integrado |
Ejemplos y Demos
Selector de Fecha Simple
Ver código
Rango de Fechas
Usa el atributo range para habilitar selección de rango:
Ver código
Integración con Selector de Hora
Combina con TdnInputTime para selección completa de fecha y hora:
Ver código
Configuración
Interface TdnDateInputSettings
Ver todas las opciones de configuración disponibles
interface TdnDateInputSettings {
// Clases CSS adicionales
classes?: string;
// Habilita selector de tiempo integrado
timePicker?: boolean;
// Sin efecto: el componente no lo lee (el formato lo da TDN_DATE_FORMATTER)
inputFormat?: string;
// Muestra botón de limpieza
clear?: boolean;
// Cierra el dropdown al presionar Escape
escapeToClose?: boolean;
// Sin efecto: el componente no lee shortcuts (ni IDateOptionShotcuts)
shortcuts?: IDateOptionShotcuts;
// Sin efecto: el formato lo da TDN_DATE_FORMATTER
formatter?: Intl.DateTimeFormat;
// Sin efecto: al seleccionar una fecha el calendario siempre se cierra
autoClose?: boolean;
// Opciones del calendario (Datepicker)
calendarOptions?: DatepickerOptions;
// Iconos personalizados
icons?: {
clear: string | SafeHtml;
chevron: string | SafeHtml;
};
// Función para renderizar celdas personalizadas
onRenderCell?: (params: {
date: Date | Date[],
cellType: CellType
}) => {
html?: string,
classes?: string,
attrs?: { title: string }
} | void;
}
interface TdnDateRangeInputSettings extends TdnDateInputSettings {
startDateField?: string; // Sin efecto: el componente no lo lee
endDateField?: string; // Sin efecto: el componente no lo lee
forceRangeField?: string; // Sin efecto: el componente no lo lee
}
Valores predeterminados
{
classes: '',
timePicker: false,
inputFormat: 'dd/MM/y', // sin efecto
clear: true,
escapeToClose: true,
autoClose: true, // sin efecto
// calendarOptions: configuración del calendario en español (locale es)
}
inputFormat, formatter, shortcuts, autoClose y los campos de TdnDateRangeInputSettings están declarados, pero el componente no los lee. En particular, los «atajos de fecha» no hacen nada. El calendario siempre está en español: calendarOptions.locale se ignora.
Ejemplo con Configuración Personalizada
Ver código
Eventos
Capturar Selección de Fecha
Ver código
Detectar Apertura/Cierre del Calendario
Ver código
El calendario abierto vive en el <body>
Al abrirse, el calendario (.tdn-input-date__dropdown) se mueve a document.body, queda fijo al viewport y se coloca a partir del rectángulo del campo. Al cerrarse se oculta con display: none pero permanece en el body; se retira cuando el campo se destruye.
- Se abre debajo del campo si hay espacio; si solo cabe arriba, encima (clase
ontopen el host y en el calendario); si no cabe en ninguno de los dos lados, toma la clasefullscreen. - Se centra horizontalmente sobre el campo y se mantiene a 5 px dentro de la ventana.
- Se reposiciona con el redimensionado y con el scroll del documento o de cualquier ancestro con scroll.
- Un clic dentro del calendario cuenta como clic dentro del campo y no se propaga: un panel que se cierra con clics externos sigue abierto mientras eliges una fecha.
- Se cierra con un clic externo, con
Escape(siescapeToClose) o, en ventanas de más de 600 px, cuando el campo sale de la vista por scroll.
Una regla como .mi-formulario .tdn-input-date__dropdown no coincide mientras el calendario está abierto, y settings.classes se aplica solo al host. Estiliza el calendario con un selector que se sostenga solo.
Personalización de Celdas
Usa onRenderCell para personalizar la apariencia de días específicos con HTML, clases CSS y atributos:
Ver código
Validación
Elegir una fecha no marca el control como touched: un mensaje con when="touched" aparece al limpiar el campo o tras markAllAsTouched().
Validadores Personalizados
Ver código
Validación de Rango
Ver código
Ejemplo Completo
Ver formulario completo con validación y manejo de errores
import { Component, inject } from '@angular/core';
import {
ReactiveFormsModule,
FormBuilder,
Validators,
AbstractControl,
ValidationErrors
} from '@angular/forms';
import {
TdnFormElement,
TdnControlDirective,
TdnInputDateComponent,
TdnErrorsDirective,
TdnErrorDirective,
TdnDateInputSettings
} from '@tiendana/ngx-core/forms';
@Component({
selector: 'app-reservation-form',
imports: [
ReactiveFormsModule,
TdnFormElement,
TdnControlDirective,
TdnInputDateComponent,
TdnErrorsDirective,
TdnErrorDirective
],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!-- Fecha Simple -->
<tdn-form-element>
<label>Check-in</label>
<tdn-input-date
tdnControl
formControlName="checkIn"
[settings]="checkInSettings"
(onSelect)="updateCheckOutMinDate($event)"
/>
<div tdnErrors="checkIn">
<div tdnError="required" when="touched">
La fecha de check-in es obligatoria
</div>
<div tdnError="minDate">
La fecha debe ser hoy o posterior
</div>
</div>
</tdn-form-element>
<!-- Rango de Fechas -->
<tdn-form-element>
<label>Período de Reserva</label>
<tdn-input-date
range
tdnControl
formControlName="reservationPeriod"
[settings]="rangeSettings"
/>
<div tdnErrors="reservationPeriod">
<div tdnError="required" when="touched">
Selecciona el período de reserva
</div>
<div tdnError="invalidRange">
La fecha de fin debe ser posterior al inicio
</div>
<div tdnError="maxRangeDays">
El período máximo es de 30 días
</div>
</div>
</tdn-form-element>
<button type="submit" class="bttn bttn--primary">
Confirmar Reserva
</button>
</form>
`
})
export class ReservationFormComponent {
private fb = inject(FormBuilder);
checkInSettings: TdnDateInputSettings = {
clear: true,
calendarOptions: {
minDate: new Date(),
firstDay: 1
}
};
rangeSettings: TdnDateInputSettings = {
clear: true,
calendarOptions: {
minDate: new Date(),
maxDate: new Date(new Date().setMonth(new Date().getMonth() + 6))
}
};
form = this.fb.group({
checkIn: [null, [Validators.required, this.minDateValidator()]],
reservationPeriod: [null, [Validators.required, this.validRangeValidator()]]
});
minDateValidator() {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value) return null;
const today = new Date();
today.setHours(0, 0, 0, 0);
return new Date(control.value) < today ? { minDate: true } : null;
};
}
validRangeValidator() {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value?.from || !value?.to) return null;
const from = new Date(value.from);
const to = new Date(value.to);
if (to < from) return { invalidRange: true };
const diffDays = (to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24);
return diffDays > 30 ? { maxRangeDays: true } : null;
};
}
updateCheckOutMinDate(date: Date) {
// Actualizar configuración del segundo selector
console.log('Check-in seleccionado:', date);
}
onSubmit() {
this.form.markAllAsTouched();
if (this.form.valid) {
console.log('Reserva:', this.form.value);
}
}
}
Personalización Avanzada
Tokens de Angular
El componente usa dos tokens de inyección, exportados junto a TDN_DATE_INPUT_DEFAULT_SETTINGS (los valores integrados) y la interfaz DateFormatter. La fábrica de TDN_DATE_INPUT_DEFAULT_SETTINGS_TOKEN clona los valores por defecto, de modo que el valor del token no comparte objetos con la constante exportada TDN_DATE_INPUT_DEFAULT_SETTINGS.
TDN_DATE_INPUT_DEFAULT_SETTINGS_TOKEN
Token para configurar el comportamiento predeterminado de todos los componentes TdnInputDate en la aplicación. Un proveedor reemplaza el objeto completo: parte de TDN_DATE_INPUT_DEFAULT_SETTINGS.
import {
TDN_DATE_INPUT_DEFAULT_SETTINGS,
TDN_DATE_INPUT_DEFAULT_SETTINGS_TOKEN,
TdnDateInputSettings
} from '@tiendana/ngx-core/forms';
import { bootstrapApplication } from '@angular/platform-browser';
const customDateSettings: TdnDateInputSettings = {
...TDN_DATE_INPUT_DEFAULT_SETTINGS,
clear: false,
calendarOptions: {
...TDN_DATE_INPUT_DEFAULT_SETTINGS.calendarOptions,
firstDay: 1, // Lunes como primer día de la semana
showOtherMonths: false
}
};
bootstrapApplication(AppComponent, {
providers: [
{
provide: TDN_DATE_INPUT_DEFAULT_SETTINGS_TOKEN,
useValue: customDateSettings
}
]
});
TDN_DATE_FORMATTER
Token obligatorio que el componente usa para mostrar la fecha en el campo: llama directamente a format y formatRange. No tiene fábrica ni valor por defecto, y el componente lo inyecta sin optional, así que omitir el proveedor lanza NullInjectorError al crear el componente. (El comentario del código que menciona un respaldo con DatePipe e inputFormat no está implementado.)
El showcase lo provee en app.config.ts con Intl.DateTimeFormat('es-ES'):
import { TDN_DATE_FORMATTER, DateFormatter } from '@tiendana/ngx-core/forms';
const dateFormatter = new Intl.DateTimeFormat('es-ES', { day: '2-digit', month: '2-digit', year: 'numeric' });
export const appConfig: ApplicationConfig = {
providers: [
{
provide: TDN_DATE_FORMATTER,
useValue: {
format: (date: Date) => dateFormatter.format(date),
formatRange: (from: Date, to: Date) => `${dateFormatter.format(from)} - ${dateFormatter.format(to)}`
} satisfies DateFormatter
}
]
};
También puede ser una clase:
class CustomDateFormatter implements DateFormatter {
format(date: Date): string {
const month = date.toLocaleDateString('es', { month: 'short' });
return `${date.getDate()} ${month} ${date.getFullYear()}`;
}
formatRange(from: Date, to: Date): string {
return `${this.format(from)} - ${this.format(to)}`;
}
}
bootstrapApplication(AppComponent, {
providers: [{ provide: TDN_DATE_FORMATTER, useClass: CustomDateFormatter }]
});
Interfaz DateFormatter:
interface DateFormatter {
format(date: Date): string; // Formato para fecha única
formatRange(from: Date, to: Date): string; // Formato para rango de fechas
}
Configuración a Nivel de Módulo/Componente
También puedes proporcionar configuraciones específicas a nivel de un módulo o componente:
import { Component } from '@angular/core';
import {
TDN_DATE_INPUT_DEFAULT_SETTINGS,
TDN_DATE_INPUT_DEFAULT_SETTINGS_TOKEN
} from '@tiendana/ngx-core/forms';
@Component({
selector: 'app-date-section',
providers: [
{
provide: TDN_DATE_INPUT_DEFAULT_SETTINGS_TOKEN,
useValue: {
...TDN_DATE_INPUT_DEFAULT_SETTINGS,
clear: false // Solo para este componente y sus hijos
}
}
],
template: `
<!-- Todos los TdnInputDate aquí usarán la configuración local -->
`
})
export class DateSectionComponent {}
Buenas Prácticas
- Usa validación de fechas: Siempre valida fechas mínimas/máximas en el backend también
- Maneja rangos correctamente: Valida que la fecha de fin sea posterior al inicio
- Configura
minDate/maxDate: Previene selección de fechas no válidas - Localización: el calendario es siempre en español; el texto del campo lo da
TDN_DATE_FORMATTER
Accesibilidad
- ✅ Escape cierra el calendario (con
escapeToClose) - ✅ Estados visuales claros (focus, disabled, active)
- ✅ Compatible con validación de formularios de Angular
- ✅ Responsive: modo fullscreen cuando el calendario no cabe ni arriba ni abajo
El campo no tiene un elemento data-tdn-aria-target, por lo que tdnControl no escribe aria-describedby ni aria-invalid en él.
Componentes Relacionados
- TdnFormElement - Wrapper para estilos consistentes
- tdnControl - Seguimiento de estado con signals
- TdnInputTime - Selector de hora para combinar con la fecha
- Datepicker (web-foundation) - Estilos del calendario