Aggregation date
Render a date facet as preset ranges plus an optional custom range, and turn a pick into an applied filter — live or deferred, controlled or not.
<aggregation-date> is what <Aggregation> delegates to for a date column — see
Aggregation. Unlike the list and tree variants, its options are a radio
list (never a multi-select), and it opens a dialog for a custom range instead of paging through buckets.
Concept — date option vs. custom range
The server turns a date column's buckets into named date options ("Today", "This week", "This year") via
translateAggregationToDateOptions. Picking "custom-range" is the seventh, always-present option: it opens
the custom-range dialog instead of applying immediately.
Minimal example
import { Component } from "@angular/core";
import { AggregationDateComponent } from "@sinequa/atomic-angular";
@Component({
selector: "sample-component",
imports: [AggregationDateComponent],
template: ` <aggregation-date name="Modified" column="modified" [showFiltersCount]="true" /> `,
})
export class SampleComponent {}Picking a preset radio, or confirming a date in the custom-range dialog, reveals the Apply button —
exactly like the list and tree facets — rather than applying instantly. Clicking it writes the filter to
QueryParamsStore and re-runs the query.
How it works
The custom range dialog
Selecting "custom-range" opens a dialog, mounted imperatively with AggregationDateRange.call({ min, max, lang }) (the createCallable pattern): one calendar showing two months side by
side, in which the range is picked and stays visible — both ends, and every day in between, with a hover
preview while picking the second end. A first click sets the start, a second one closes the range (clicking an
earlier day swaps the two), and a localized summary under the calendar reads the result back. Validating after a
single click means that day alone (from and to are the same day); the dialog cannot pick an open-ended
range.
Every call mounts a fresh dialog instance and tears it down on close, so no selection survives from the last
time it was used. The call resolves with the picked range, or undefined when the dialog is dismissed (cancel,
Escape, backdrop) — the facet then leaves its model untouched.
Inside a popover
A facet is often rendered in a native popover="auto" — that is what <filter-button>
does. The custom-range dialog is modal, and showModal() hides every auto popover that is not an ancestor of
the dialog. Mounted in document.body like any callable dialog, it would close the popover that opened it
and destroy the facet with it, dialog included.
So <aggregation-date> mounts the dialog inside itself, through the call's container option, instead of in
document.body: the dialog becomes a DOM descendant of the facet, hence of the popover, which stays open behind
the modal. The dialog is still rendered in the top layer, so it is not clipped by the facet's box. Mounting it
in the facet also means the calendar inherits CSS custom properties set on the facet or above it, which is how
a single facet's colors are restyled.
Nothing to configure: the behaviour is automatic. The same container option is available to your own dialogs:
see "Open a dialog from inside a native popover" in galactik's Dialog page.
Controlled vs. uncontrolled, and emitOn
Like <Aggregation>, setting any of query, aggregation or selection switches to controlled mode. Being a
radio list, controlled mode has one behaviour the list/tree variants do not: emitOn="selection" (the
default) emits live, at the moment a preset is picked or a custom range confirmed — there is no Apply click
to wait for. Set emitOn="apply" to defer every emission to the Apply button instead, matching the list/tree
select-then-Apply flow.
Masked dates
A date column aggregated with a mask (YYYY, YYYY-MM) returns items such as { value: "2016", count: 12 },
with no display. The mask is deduced from the values, and the label is the value itself, formatted in the
active language for YYYY-MM (March 2024 / mars 2024). Picking one commits the full dates of the period,
gte 2016-01-01 and lt 2017-01-01 (December rolls over to the next year), and puts the mask in the query's
aggregation_overrides, keyed by aggregation name ({ Modified: { mask: "YYYY" } }), which tells the server
how the values are built. Overrides of other aggregations are kept.
- Default mode: the component writes the override to
QueryParamsStoreitself — not to the URL, since a committed range is self-sufficient; only the masked aggregation values are lost after a page reload until the next apply — and removes it when the filter is cleared. - Controlled mode: it is emitted through
(aggregationOverridesChange), right before(filtersChange), on the same occasions. Assign it to the query you fetch with.
Recipes
A controlled date facet, with a live selection
import { Component, signal } from "@angular/core";
import { AggregationDateComponent } from "@sinequa/atomic-angular";
import type { AggregationSelection } from "@sinequa/atomic-angular";
import type { Aggregation, Filter, Query } from "@sinequa/atomic";
@Component({
selector: "agent-date-facet",
imports: [AggregationDateComponent],
template: `
<aggregation-date
name="Modified"
column="modified"
[aggregation]="modifiedAggregation()"
[selection]="selection()"
[query]="agentQuery"
(selectionChange)="selection.set($event)"
(filtersChange)="applyToQuery($event)"
/>
`,
})
export class AgentDateFacetComponent {
private readonly agentQuery: Query = { name: "agent-query", text: "" };
protected readonly selection = signal<AggregationSelection | undefined>(undefined);
protected readonly modifiedAggregation = signal<Aggregation>({
name: "Modified",
column: "modified",
isDistribution: true,
items: [],
} as unknown as Aggregation);
protected async applyToQuery(filters: Filter | undefined) {
console.log("Filters to send:", filters);
}
}The emitted AggregationSelection is { kind: "option", option } for a preset — the item's real filter
value, never its display, so a round-trip survives across days for presets computed relative to today —
or { kind: "range", from, to } for a custom range.
Disable the custom range entirely
Some deployments only want the fixed presets — no "custom range" option at all.
import { FILTER_DATE_ALLOW_CUSTOM_RANGE } from "@sinequa/atomic-angular";
providers: [{ provide: FILTER_DATE_ALLOW_CUSTOM_RANGE, useValue: false }];Options
Prop
Type
Controlled mode
Prop
Type
Outputs: selectionChange and filtersChange as described above, plus aggregationOverridesChange
(Record<string, { mask?: string }> | undefined) for masked dates.
Set up the calendar
The custom-range dialog renders its two-month calendar with vanilla-calendar-pro.
The calendar is internal to <aggregation-date> — there is no component to declare or provider to register —
but the application owns two pieces of setup.
1. Install the peer dependency.
npm install vanilla-calendar-pro2. Import the stylesheet once, from the application's global stylesheet (not from a component: at 26 kB
it would break the default anyComponentStyle budget).
@import "@sinequa/atomic-angular/styles/calendar.css";It bundles vanilla-calendar-pro's layout and light theme with every color mapped onto galactik's semantic tokens, so the calendar follows the light/dark theme with no extra rule. It is a single flat file with no nested import: without a bundler that resolves package exports, link it instead.
<link rel="stylesheet" href="node_modules/@sinequa/atomic-angular/styles/calendar.css" />Nothing else to configure: month and weekday names, and the first day of the week, come from the browser's
Intl API for the active Transloco language — no locale file to register.
Customize the colors
Every color of the calendar is a --vc-* custom property set on the date-range-calendar element. Override
the ones to change after the @import, with the same selector, or scoped to one facet with a class on
<aggregation-date> since the calendar is mounted inside it (.my-facet date-range-calendar { … }). Pointing them at galactik tokens keeps the light/dark switch for free; a raw color applies to both themes.
The variables are those of vanilla-calendar-pro, which remains the reference for
what each one paints.
@import "@sinequa/atomic-angular/styles/calendar.css";
date-range-calendar {
--vc-date-selected-bg: var(--bg-accent-base);
--vc-date-range-middle-bg: var(--bg-neutral-hovered);
--vc-date-range-middle-color: var(--font-neutral-base);
}| Property | Paints | Default |
|---|---|---|
--vc-bg / --vc-color | calendar surface and text | --bg-neutral-white / --font-neutral-base |
--vc-date-selected-bg / --vc-date-selected-color | both ends of the range | --bg-primary-base / --font-neutral-white |
--vc-date-range-middle-bg / --vc-date-range-middle-color | days between the ends | --bg-primary-lighter / --font-primary-base |
--vc-date-hover-bg / --vc-date-hover-edge-bg | preview while picking the second end | --bg-primary-lighter / --bg-primary-light |
--vc-date-today-bg / --vc-date-today-color | today | --bg-primary-lighter / --font-primary-base |
--vc-date-disabled-color | disabled days | --font-neutral-disabled |
--vc-focus-outline-color | keyboard focus ring | --stroke-focus |
The full list (month/year grids, days outside the month…) is in the generated stylesheet, under the
date-range-calendar rule at its end. Layout — sizes, spacing, radii — comes from vanilla-calendar-pro and is
best left alone; the dialog sizes itself to the two months.
Pitfalls
What's next
Aggregation
Render a facet as a list, a tree or a date range from a query's aggregations, and turn a click into an applied filter — with a custom item template or full control over the data.
Filters bar
A toolbar listing the facets a user can filter on — one popover button per aggregation, with overflow handled by a "More" button.