Skip to content

Show active filters as removable chips with a full reset link#144

Open
loevgaard wants to merge 1 commit into
masterfrom
issue-51-active-filters
Open

Show active filters as removable chips with a full reset link#144
loevgaard wants to merge 1 commit into
masterfrom
issue-51-active-filters

Conversation

@loevgaard

Copy link
Copy Markdown
Member

Resolves #51

What

Applied filters now render as chips above the search results, matching the mockup in the issue:

  • Clicking a chip's ✕ removes that single filter (via the existing AJAX swap + pushState flow; the chips are plain links, so they degrade to full page loads without JavaScript).
  • A Reset all filters link clears every filter while keeping the query (q), the sorting (s), and any unrelated query parameters.
  • The chips also render on the no-results page, where the filter form is not rendered at all today — so users can un-filter their way back to results instead of being stranded.
  • Removing a filter always resets the page parameter (p).

How

  • New ActiveFiltersProvider (src/Provider/ActiveFilters/) computes the chips server side from the request's f query parameters. Filter types are resolved through the index metadata (the same source the form/filter builders use), and the value guards mirror ArrayFilterBuilder/BooleanFilterBuilder/FloatFilterBuilder, so a chip is only shown for a filter the engine would actually apply. Unknown facet names are skipped.
  • Templates get the chips via a new ssm_active_filters() Twig function (SearchRuntime) and a new search/_active_filters.html.twig partial, included inside #search-form in _results.html.twig and _no_results.html.twig so it is re-rendered on every AJAX swap.
  • search.js gains a shared #navigate(url) (extracted from #submitForm), a public navigate(), click delegation for the chip links (wired before the form check so it also works on the no-results <div>), new onFilterRemove/onFiltersReset options, and new search:filter-removed/search:filters-reset events. All additive.
  • Labels: choice chips show the raw facet value (already localized at index time); boolean chips use new count-free active_filters.facet.* keys with a humanized fallback for untranslated custom facets; range chips read e.g. "Price: from 50". New translation keys added to all 12 locales.

Behavior change: disjunctive facetStats

The range inputs are prefilled with the facet's bounds and submitted with every form interaction, so the URL always carries f[price][min|max] even when the user never touched them — naively that would render a phantom "Price" chip after any interaction. A range chip is therefore only shown when a bound actually narrows the facet's stats.

For that comparison to work, SearchEngine::provideSearchResult() now merges facetStats across the disjunctive per-facet queries the same way it already merges facetDistribution (previously the stats came only from the fully-filtered main query, against which a user-set bound never registers as narrowing). Side effect: range inputs now prefill with bounds that exclude their own filter, which is the standard disjunctive faceting behavior.

Notes for extenders

  • Integrators overriding _results.html.twig/_no_results.html.twig keep their current markup and can opt in with one include line; opting out with stock templates is overriding _active_filters.html.twig with an empty file.
  • ActiveFiltersProviderInterface is aliased, so chip computation can be decorated/replaced.
  • Known limitation: a hand-crafted falsy boolean filter (f[onSale]=0) filters to false but gets no chip — that state is unreachable through the shipped UI.

Testing

  • Unit: SearchEngineTest (disjunctive stats merge) and ActiveFiltersProviderTest (15 cases: choice/bool/range chips, narrowing suppression, invalid values, unknown facets, reset URL, taxon-style paths).
  • Functional: ActiveFiltersProviderTest proves the wiring + real Meilisearch stats end to end, including the "prefilled bounds produce no chip" case.
  • E2E: new "active filters" describe block in search.spec.ts (6 tests) covering chip rendering, removal, the phantom-price-chip guard, the no-results rescue, and reset keeping query + sorting.

All suites pass locally: 66 unit, 13 functional, 20 e2e; PHPStan (level max), ECS, Rector, lint:container, lint:twig, composer validate --strict and composer normalize --dry-run are clean.

Applied filters now render as chips above the search results: clicking a
chip's X removes that single filter, and a "Reset all filters" link clears
every filter while keeping the query and sorting. The chips also render on
the no-results page, where the filter form is gone, so users can un-filter
their way back to results.

Chips are computed server side from the request's `f` query parameters by
the new ActiveFiltersProvider (exposed to templates via the
`ssm_active_filters()` Twig function) and are plain links, so they work
without JavaScript; search.js intercepts their clicks and reuses the
existing fetch/swap/pushState flow.

The search form prefills range inputs with the facet bounds and submits
them on every interaction, so a range chip is only shown when a bound
actually narrows the facet's stats. That required SearchEngine to merge
facetStats across the disjunctive facet queries the same way it already
merged facetDistribution — as a side effect, range inputs now prefill with
bounds that exclude their own filter.

Resolves #51
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.58559% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.11%. Comparing base (14bef06) to head (43f3a43).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
.../Provider/ActiveFilters/ActiveFilterCollection.php 0.00% 7 Missing ⚠️
src/Twig/SearchRuntime.php 0.00% 6 Missing ⚠️
...c/Provider/ActiveFilters/ActiveFiltersProvider.php 97.87% 2 Missing ⚠️
src/Provider/ActiveFilters/ActiveFilter.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #144      +/-   ##
============================================
+ Coverage     37.92%   41.11%   +3.18%     
- Complexity      711      768      +57     
============================================
  Files           131      135       +4     
  Lines          2286     2403     +117     
============================================
+ Hits            867      988     +121     
+ Misses         1419     1415       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature request]: Show active filters and full reset button

1 participant