Atomic Angular — Doc interne

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

A facet that applies a filter on selection
sample.component.ts
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:

yes no yes no Aggregation (type: auto) isDateColumn(column)? aggregation-date aggregation.isTree? aggregation-tree aggregation-list

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:

CaseBehaviour
Missing — no entry in the store matching name/column, or an empty [aggregation] in controlled modeAlways hidden, unconditionally
Resolves to zero itemsVisible 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.

A flag next to each language
languages-facet.component.ts
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.

Announce the rows ticked but not applied
pending-facet.component.ts
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.

A facet over an unmanaged query
agent-language-facet.component.ts
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

On this page