Filtres transverses multi-sources pour les dashboards a filtre commun. Le contexte ecoute des elements d'UI natifs (select, input...) et diffuse les filtres a N sources nommees — le « fan-out » declaratif qui evitait jusqu'ici d'ecrire du JavaScript d'orchestration a la main.

Opt-in et additif (ADR-031) : sans contexte, chaque source reste autonome — rien ne change pour les pages mono-graphique. Le contexte n'effectue aucun fetch et ne transforme aucune donnee : il emet des commandes where avec un whereKey stable par filtre, combinees en AND par le merge multi-emetteurs des sources (jamais « le dernier gagne »).

Pattern

<!-- UI native libre (formulaire DSFR) -->
<select id="ui-catégorie" multiple>...</select>
<input id="ui-mois" type="month">

<!-- Le contexte orchestre -->
<dsfr-data-context sources="src-a src-b src-c" url-sync>
  <dsfr-data-context-filter field="categorie_produit" label="Catégorie"
    operator="in" ui="ui-catégorie"></dsfr-data-context-filter>
  <dsfr-data-context-filter field="date_rappel" label="Mois"
    operator="month-of" ui="ui-mois"></dsfr-data-context-filter>
</dsfr-data-context>

<!-- Recap supprimable des filtres actifs -->
<dsfr-data-context-tags for="ctx"></dsfr-data-context-tags>

Attributs — dsfr-data-context

AttributTypeValeursDescription
sourcesStringrequisIds des sources cibles, séparés par des espaces
url-syncBooleandéfaut : offSérialisation URL des filtres (#231, ADR-031) — OPT-IN, défaut OFF (collision possible avec le routing query-string du site hôte). Lecture au chargement (pré-remplit les UI, qui repassent par le même chemin qu'un clic — aucune injection directe dans un where) ; écriture en history.replaceState à chaque changement. Un paramètre par champ, pour les filtres classiques comme pour les facettes et la recherche enregistrées par context="id" (#678) : l'URL-sync est unique. DEUX PIÈGES À DEUX CONTEXTES, tous deux signalés en console (#922, #923). 1. Deux contextes à url-sync qui filtrent le MÊME champ écrivent le MÊME paramètre : le dernier écrase les autres, et au rechargement ils relisent tous la même valeur — un comparateur se compare alors à lui-même. Un seul contexte dans l'URL, ou url-param-map pour séparer les paramètres. 2. Le pré-remplissage depuis l'URL écrit el.value SANS émettre d'événement : un filtre d'un AUTRE contexte déjà lié au même contrôle reste sur la valeur d'avant. Déclarer le contexte à url-sync EN PREMIER dans le document.
url-param-mapString"param:field | p2:f2"Renommage des paramètres : "param:field | param2:field2" (#231)

Attributs — dsfr-data-context-filter

AttributTypeValeursDescription
fieldStringrequisColonne filtrée — une colonne des SOURCES ciblées, telle que l'API la connaît. Le filtre y est délégué et s'applique avant tout regroupement (ordre ODSQL : where puis group_by) : ni un alias d'agrégat (montant__sum), ni une colonne calculée en aval (compute d'un dsfr-data-normalize) n'existent à ce stade. Quand le contexte sait que la colonne manque (source sans select ni group-by, déjà chargée, #805) : absente de toutes les sources visées, erreur de configuration nommée et rien n'est diffusé ; absente de certaines seulement, ces sources sont exclues du filtre, avec un message console. Sinon le filtre part, et l'API répond (HTTP 400 si la colonne n'existe pas). TYPE DE LA COLONNE (#924) : un contrôle de formulaire rend toujours du TEXTE, et le filtre émet reg = "01". Sur une colonne que le jeu publie en ENTIER, le portail compare en texte et ne trouve rien, là où son refine trouvait — « — » à l'écran, sans erreur. Les codes métropolitains (« 75 ») passent, ce qui cache le défaut : seuls les codes à zéro de tête (Guadeloupe 01, départements 01 à 09) sont muets. Quand les lignes déjà rendues par une source montrent que la colonne y est numérique, la console le dit, une fois par colonne et par source. Quand elles ne la portent pas — source agrégée côté serveur (select="sum(...)"), filtre délégué au portail (#980) — c'est le type DÉCLARÉ par le jeu Opendatasoft qui décide, lu une fois par jeu. Le geste : alimenter le filtre avec la valeur sans zéro de tête, ou réserver ce filtre aux sources qui publient le code en texte avec apply-to.
uiStringrequisId(s) de l'élément d'UI écouté — deux ids (min max) pour between
operatorContextOperatoreq (défaut), in, lt, gte, between, month-of, year-of, lt-day-after, last-n-days, current-year, current-monthOpérateur : eq, in, lt, gte, between, contains (sous-chaîne, #678) — et dates (#230, clauses en plages [debut, fin)) : month-of, year-of, lt-day-after, last-n-days, current-year, current-month (#682 — case à cocher, mois en cours, borne dynamique). year-of et month-of acceptent une date plus precise que l'opérateur et la tronquent (#646) : "2026-09-09" -> annee 2026 / mois 2026-09, ce qui permet de les nourrir d'un <input type="date"> (il n'existe pas de type="year"). Une valeur qui reste inexploitable (ni date, ni mois, ni annee) retire le filtre et le signale par un avertissement console, emis une seule fois par filtre.
apply-toString*Cibles : "*" (défaut, toutes les sources du contexte) ou ids ciblés
labelStringdéfaut : fieldLibellé naturel pour l'affichage (tags #232) — défaut : field
defaultStringtoday, first-of-month, first-of-year ou littéralValeur initiale du filtre (#682), appliquée au montage APRÈS l'URL — un paramètre d'URL présent gagne toujours (ADR-031). Mots-clés dynamiques résolus dans le fuseau local : today (date du jour), first-of-month (1er du mois en cours), first-of-year (1er janvier de l'année en cours) ; toute autre valeur est un littéral. La date est adaptée au contrôle (input type="month" → AAAA-MM, year-of → AAAA) puis écrite dans l'UI et émise par le chemin normal, jamais injectée dans un where. Pour between et in, plusieurs valeurs séparées par une virgule (ex. first-of-year,today).
contextStringcontext="ctx"Id du dsfr-data-context cible (#678). Vide = le contexte parent le plus proche (closest), comportement historique. Le contexte peut être déclaré après ce filtre dans le DOM : l'enregistrement se fait alors à sa connexion.

Operateurs

OperateurUI typiqueClause générée
eqinput, selectfield = valeur
inselect multiplefield IN (v1, v2) — valeurs separees par | ou ,
lt / gteinputcomparaison stricte / large
betweendeux inputs (min, max)gte min + lt max
month-ofinput type="month"plage [1er du mois, 1er du mois suivant)
year-ofinput anneeplage annuelle
lt-day-afterinput type="date"inclusif jusqu'au jour choisi
last-n-daysinput Nborne dynamique « N derniers jours », recalculee a chaque diffusion
current-yearcheckboxplage de l'annee en cours (dynamique)
current-monthcheckboxplage du mois en cours (dynamique, #682)

Une checkbox cochée transmet la valeur 'on' — c'est ce qui active les opérateurs sans valeur comme current-year. Une valeur vide (champ effacé, checkbox décochée) retire le filtre. Les valeurs sont percent-encodées dans la clause colon.

Années non civiles — year-start-month

year-of et current-year découpent l'année civile. Une bonne partie des données publiques ne s'y range pas : année scolaire, exercice comptable, saison. year-start-month déplace le mois de début de l'année sur le filtre — 1 (défaut) pour l'année civile, 9 pour l'année scolaire, 4 pour l'exercice comptable britannique, 7 pour l'exercice australien, 10 pour une saison.

<dsfr-data-context-filter field="date_inscription" label="Année scolaire"
  operator="year-of" year-start-month="9" ui="ui-annee">
</dsfr-data-context-filter>

La clause reste une plage gte + lt : elle se délègue au serveur comme n'importe quelle autre, aucun adaptateur n'est concerné. Avec year-start-month="9", la valeur « 2024 » filtre [2024-09-01, 2025-09-01) et s'affiche « 2024-2025 » dans les tags. Une valeur plus précise (« 2025-03-10 ») désigne l'année qui la CONTIENT — 2024-2025 ici — ce qui permet de nourrir l'opérateur d'un <input type="date">. Côté client seul, une colonne d'année scolaire se dérive aussi avec le compute de dsfr-data-normalize ; l'attribut existe pour les jeux qu'on ne veut pas rapatrier.

Dialectes : la clause est construite en colon (pivot) puis traduite au whereFormat de chaque adapter cible (ODSQL pour OpenDataSoft, colon pour Tabular/Grist). Les bornes dynamiques ne figent jamais de date dans le DOM, et l'URL serialise l'intention (« 30 »), pas les dates resolues : un lien partage ne gele pas de vieilles données.

Attributs — dsfr-data-context-tags

AttributTypeValeursDescription
forStringrequisId du dsfr-data-context observé
clear-allBooleanprésent / absentBouton « Tout effacer » (#679) — un seul geste pour vider tous les filtres du contexte

Attributs — dsfr-data-context-value

dsfr-data-context-tags liste les filtres actifs sous forme de tags supprimables ; il ne s'insère pas dans une phrase. <dsfr-data-context-value> (#742) rend la valeur courante d'un filtre comme du texte, seule ou interpolée dans un gabarit — pour qu'un titre de page dise « Résultats pour la Gironde » plutôt que « Résultats ».

AttributTypeValeursDescription
forStringrequisId du dsfr-data-context observe
fieldStringfield="departement"Champ dont la valeur est rendue — raccourci de template="{{champ}}". Ignore quand template est pose.
templateString"Résultats pour {{champ}}"Gabarit texte : chaque {{champ}} est remplace par la valeur courante du filtre de ce champ (« Résultats pour {{departement}} »).
fallbackStringvide = ne rend rienTexte rendu tant qu'un champ cite n'a aucune valeur — le repli declare de #742. Vide : le composant ne rend rien du tout.
liveBooleanprésent / absentRégion live polie : le titre annonce le changement de contenu aux lecteurs d'écran. À poser sur UN seul élément de la page.
<h2>
  <dsfr-data-context-value for="ctx"
    template="Résultats pour {{departement}}"
    fallback="Résultats pour toute la France" live>
  </dsfr-data-context-value>
</h2>

Le gabarit accepte plusieurs marqueurs dans la même phrase (template="{{departement}} en {{annee}}") ; field="departement" est le raccourci de template="{{departement}}", ignoré si template est posé. Un seul champ cité sans valeur suffit à basculer sur fallback : « Résultats pour » serait pire qu'une phrase de repli. Sans fallback, le composant ne rend rien du tout. La valeur rendue est celle qu'affichent les tags, y compris pour une facette ou une recherche enregistrée sur le contexte par context="id".

Accessibilité : live fait du composant une région live discrète (aria-live="polite", role="status"), pour qu'un titre annonce le changement de contexte. À poser sur un seul élément de la page — trois libellés qui parlent en même temps sont un bruit, pas une aide. Le texte rendu est du texte : la valeur d'un filtre traverse le rendu Lit, jamais l'analyseur HTML. Le composant vit en light DOM et hérite donc des styles de la page (titre, paragraphe).

Événements

ÉvénementdetailDescription
dsfr-data-context-change—Émis sur l'élément <dsfr-data-context> à chaque application ou retrait d'un filtre. C'est cet événement qu'écoute dsfr-data-context-tags pour se rafraîchir.

dsfr-data-context-filter, dsfr-data-context-tags et dsfr-data-context-value n'émettent aucun événement custom propre : tout passe par le contexte et les commandes du data-bridge. dsfr-data-context-value se contente d'écouter dsfr-data-context-change, comme les tags.

Voir aussi