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.
A facet — "Sources", "Authors", "Modified date" — is one aggregation of a query's result, rendered so the
user can turn a bucket into a filter with a click. <Aggregation> picks and renders the right shape (a flat
list, a tree, or a date range) for a given column, and handles selecting, applying and clearing the filter for
you.
Concept — aggregation
An aggregation is a bucketed count over one column of the result set — how many documents have each
value of docformat, how many fall in each date range of modified. The server computes it alongside the
result records; <Aggregation> never computes it itself, only renders and lets the user filter on it.
Minimal example
import { Component } from "@angular/core";
import { AggregationComponent } from "@sinequa/atomic-angular";
import type { AggregationItem } from "@sinequa/atomic";
@Component({
selector: "sample-component",
imports: [AggregationComponent],
template: `
<Aggregation
name="Sources"
column="sourcestr4"
[searchable]="true"
[showFiltersCount]="true"
(onSelect)="handleSelect($event)"
/>
`,
})
export class SampleComponent {
protected handleSelect(items: AggregationItem[]) {
console.log(
"Selected:",
items.map((item) => item.value),
);
}
}name and column identify which aggregation of the current query's result to render — name is matched
first against the app's aggregation configuration, column as a fallback. Selecting an item and clicking
Apply (or ticking a value in a facet search) writes the corresponding filter to the shared
QueryParamsStore and re-runs the query; nothing else in the template has to react to that by hand.
How it works
<Aggregation> does not render anything itself — it resolves which of three sub-components actually renders,
then delegates:
Set type explicitly ("list" | "tree" | "date", instead of the default "auto") when the heuristic cannot
apply — a date facet of a query the app's column map does not know about, for instance.
Missing vs. empty
Two different situations can leave a facet with nothing to show, and <Aggregation> treats them differently:
| Case | Behaviour |
|---|---|
Missing — no entry in the store matching name/column, or an empty [aggregation] in controlled mode | Always hidden, unconditionally |
| Resolves to zero items | Visible with a dimmed, non-expandable header by default; hidden too when [hideWhenEmpty]="true" |
A missing aggregation is a misconfiguration (a typo'd name, an aggregation removed from the app's setup) —
not a state worth showing the user anything about — so it always disappears. A facet that genuinely resolves
to zero items is a legitimate, often temporary state, so it stays by default and only disappears once you opt
in with hideWhenEmpty. Two exemptions keep a facet the user can still act on regardless of that input: a
date facet (no "resolved but empty" state exists for it — see Pitfalls) and a facet currently
carrying an applied filter (hiding it would leave no way to clear that filter from the facet itself).
Controlled vs. uncontrolled
By default <Aggregation> reads and writes the shared QueryParamsStore/AggregationsStore and the URL. Set
any of query, aggregation or selection to switch the whole instance to controlled mode: it then
reads only from those inputs, holds its own state, and never touches the shared stores, the URL or
sessionStorage — changes surface exclusively through (selectionChange)/(filtersChange). This is what lets
several independent facet instances coexist on one page, each over its own query.
Recipes
Custom item content, in a list
Project an <ng-template aggregationItem> to own the content/label region of each row; <aggregation-list>
keeps the checkbox, click-to-select, count and virtualization.
import { Component, input } from "@angular/core";
import { AggregationItemDirective, AggregationListComponent } from "@sinequa/atomic-angular";
import type { Aggregation } from "@sinequa/atomic";
@Component({
selector: "languages-facet",
imports: [AggregationListComponent, AggregationItemDirective],
template: `
<aggregation-list name="Languages" column="language" [aggregation]="aggregation()">
<ng-template aggregationItem let-item let-name="name" let-count="count" let-selected="selected">
<img [src]="'/flags/' + item.value + '.svg'" alt="" class="size-4" />
<span [class.font-semibold]="selected">{{ name }}</span>
<span class="badge">{{ count }}</span>
</ng-template>
</aggregation-list>
`,
})
export class LanguagesFacetComponent {
readonly aggregation = input.required<Aggregation>();
}The same directive works on <aggregation-tree> — the context gains level/expanded/hasChildren, typed
as required through AggregationTreeItemContext if you annotate let-* manually.
Announce a state the row only shows visually
Each row carries an aria-label (its name), which replaces whatever the row renders in the accessible name — so a
"not applied yet" mark drawn inside an aggregationItem template, even as sr-only text, is never read. Give
itemDescription a callback instead: a non-empty string is exposed through aria-describedby on the row and
announced after its name.
import { Component, input } from "@angular/core";
import { AggregationComponent } from "@sinequa/atomic-angular";
import type { AggregationItemDescription } from "@sinequa/atomic-angular";
import type { Aggregation } from "@sinequa/atomic";
@Component({
selector: "pending-facet",
imports: [AggregationComponent],
template: `
<Aggregation
name="Languages"
column="language"
[aggregation]="aggregation()"
[itemDescription]="describe"
/>
`,
})
export class PendingFacetComponent {
readonly aggregation = input.required<Aggregation>();
// Runs during the facet's change detection: signals read here are tracked.
protected readonly describe: AggregationItemDescription = (_item, { selected }) =>
selected ? "Selected, not applied yet" : null;
}The callback receives the item and { selected, level? } (level is tree-only). It does not apply to the date
variant.
Full control over the data and the selection
Set query, aggregation and selection together to render an aggregation that belongs to a query the
current app does not manage — an independent facet panel over a per-agent query, for instance. Nothing is
read from or written to the shared stores; every change surfaces through the outputs.
import { Component, computed, inject, signal } from "@angular/core";
import { AggregationComponent } from "@sinequa/atomic-angular";
import type { AggregationSelection } from "@sinequa/atomic-angular";
import { fetchQuery, type Aggregation, type Filter, type Query } from "@sinequa/atomic";
@Component({
selector: "agent-language-facet",
imports: [AggregationComponent],
template: `
<Aggregation
name="Languages"
column="language"
[aggregation]="languagesAggregation()"
[selection]="selection()"
[query]="agentQuery"
(selectionChange)="selection.set($event)"
(filtersChange)="applyToQuery($event)"
/>
`,
})
export class AgentLanguageFacetComponent {
private readonly agentQuery: Query = { name: "agent-query", text: "" };
protected readonly selection = signal<AggregationSelection | undefined>(undefined);
// A stable, typed reference: an inline `{ ...agg, display }` literal in the
// template would reseed the instance on every change-detection pass — see Pitfalls.
protected readonly languagesAggregation = computed<Aggregation>(() => this.rawAggregation());
private readonly rawAggregation = signal<Aggregation>({
name: "Languages",
column: "language",
items: [],
} as unknown as Aggregation);
protected async applyToQuery(filters: Filter | undefined) {
await fetchQuery({ ...this.agentQuery, filters });
}
}The aggregation input is deep-cloned on the way in, so the host's object is never mutated and several
instances can safely share one source object — but any reference change of that input reseeds the whole
internal state (pages loaded through load-more, tree nodes opened by the user are discarded).
Options
Prop
Type
Controlled mode
Prop
Type
Pitfalls
What's next
Filtering results
Rendering facets over a query's aggregations, and turning clicks into filters — the aggregation family, the filters bar and its overflow, and what ends up applied.
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.