API reference
Headless Datepicker is a controller plus a small set of binding directives. You still own the field, trigger, overlay, and calendar layout, while the primitive owns parsing, roving focus, panel transitions, and the public accessibility attributes.
createDatepickerController(...)
Controller + Angular binding
import { bindTngDatepicker, createDatepickerController } from '@tailng-ui/primitives';
readonly controller = createDatepickerController<Date>({
ownerDocument: document,
value: '2024-04-22',
today: '2024-04-18',
minDate: '2024-04-01',
maxDate: '2026-03-31',
closeOnSelect: true,
trapFocus: true,
showOutsideDays: true,
});
readonly datepicker = bindTngDatepicker(this.controller);
| Option | Purpose |
|---|---|
value / defaultValue | Initial or controlled selection state. |
today | Overrides the date marked as today in the calendar grid. |
minDate / maxDate | Disables out-of-range days, months, and years. |
adapter | Controls input parsing, formatting, and visible period labels. |
closeOnSelect | Closes the popup after a committed day selection. |
trapFocus | Keeps focus inside the popup while it is open. |
showOutsideDays | Shows adjacent-month days in the visible month grid. |
placement / overlayGap | Controls overlay positioning and spacing from the field. |
Primitive directives
These directives are the current headless binding layer. They keep the docs and applications out of the manual key, click, aria, and active-descendant plumbing that the older examples had to repeat.
Field + overlay wiring
<section [tngDatepickerHost]="controller">
<div data-slot="datepicker-field">
<div #anchorShell>
<div
data-slot="datepicker-input-shell"
[attr.data-invalid]="datepicker.outputs().validationError !== null ? 'true' : null"
[attr.data-open]="datepicker.outputs().getTriggerAttributes()['data-open']"
>
<input [tngDatepickerInput]="controller" type="text" placeholder="MM-DD-YYYY" />
<button [tngDatepickerTrigger]="controller" type="button">Open</button>
</div>
<section [tngDatepickerOverlay]="controller" [tngDatepickerOverlayAnchor]="anchorShell">
<button [tngDatepickerPrevButton]="controller" type="button">‹</button>
<button [tngDatepickerPeriodButton]="controller" type="button">
{{ datepicker.periodLabel() }}
</button>
<button [tngDatepickerNextButton]="controller" type="button">›</button>
</section>
</div>
</div>
</section>
| Directive | Purpose |
|---|---|
[tngDatepickerHost] | Applies the public root attributes like data-open, data-view, and root ARIA labels. |
[tngDatepickerInput] | Wires input text, manual commits, open-on-click, and basic keyboard entry into the controller. |
[tngDatepickerTrigger] | Registers the trigger element and forwards the wrapper-grade trigger keyboard behavior. |
[tngDatepickerOverlay] | Ports the popup to document.body, syncs overlay attrs, and keeps positioning/focus semantics aligned. |
[tngDatepickerPrevButton] / [tngDatepickerNextButton] | Pages the current view without repeating day/month/year branching in your component code. |
[tngDatepickerPeriodButton] | Handles the standard period drill-down flow and keeps focus synced after the view changes. |
[tngDatepickerDayGrid], [tngDatepickerMonthGrid], [tngDatepickerYearGrid] | Forward the correct keyboard behavior for each panel. |
[tngDatepickerDayCell], [tngDatepickerMonthOption], [tngDatepickerYearOption] | Apply the public cell attrs and click behavior so buttons stay headless but not repetitive. |
Grid rendering
The controller still exposes the view data. The difference is that the cell and grid directives now own the common interactions, so the template only needs to render the panels it wants.
This keeps the layout genuinely headless: you decide which panel to show and how to arrange it, while the primitive keeps the keyboard and selection model consistent.
Day / month / year panels
@if (datepicker.outputs().view === 'day') {
<div [tngDatepickerDayGrid]="controller">
@for (cell of datepicker.outputs().cells; track cell.id) {
<button [tngDatepickerDayCell]="cell" type="button">{{ cell.label }}</button>
}
</div>
}
@if (datepicker.outputs().view === 'month') {
<div [tngDatepickerMonthGrid]="controller">
@for (option of datepicker.outputs().monthOptions; track option.id) {
<button [tngDatepickerMonthOption]="option" type="button">{{ option.label }}</button>
}
</div>
}
@if (datepicker.outputs().view === 'year') {
<div [tngDatepickerYearGrid]="controller">
@for (option of datepicker.outputs().yearOptions; track option.id) {
<button [tngDatepickerYearOption]="option" type="button">{{ option.label }}</button>
}
</div>
}
Controller outputs
datepicker.outputs() is the primary read model. The get*Attributes() helpers are still available for advanced custom composition, but they are now the lower-level escape hatch rather than the default path.
| Output | Details |
|---|---|
cells | Visible day cells with disabled, selected, active, today, and in-month state. |
monthOptions / yearOptions | Picker options for month and year drill-down panels. |
inputText | Current editable input text, including in-progress manual entry. |
labelMonthYear | Ready-to-render month/year label for the day view header. |
getHostAttributes() / getOverlayAttributes() / getGridAttributes() | Low-level attribute maps for advanced compositions or environments where you are not using the helper directives. |
getCellAttributes(...) / getMonthAttributes(...) / getYearAttributes(...) | Low-level item attrs for fully custom cell markup beyond the provided option directives. |
Controller methods
| Method | Purpose |
|---|---|
open() / close() / toggleOpen() | Owns popup visibility. |
setInputText(...) / commitInputText() | Supports manual editing with adapter validation and bounds checks. |
subscribe(...) | Low-level subscription hook. Angular apps can usually prefer bindTngDatepicker(...) instead. |
showYearsPanel() / showMonthsPanel() / showDaysPanel() | Lets the implementation drive the visible panel explicitly when it wants a custom view flow. |
prevMonth() / nextMonth() / prevYear() / nextYear() | Pages the visible range for the current view. |
selectMonth(...) / selectYear(...) / handleCellClick(...) | Low-level selection hooks for layouts that want to bypass the stock option directives. |
handleTriggerKeyDown(...) / handleOverlayKeyDown(...) / handleGridKeyDown(...) | Still available as the lower-level escape hatch if you are building a custom directive layer of your own. |