fix(goals/index): persist filter + search in URL across reloads

UX audit finding. The filter chip state and search input lived only
in Stimulus values — a Behind-filter selection survived turbo
morphs but vanished on F5, browser back from a goal's show page,
and any deeplink share. For a family with 10+ goals filtering by
"behind", every navigation reset to "all".

Hydrate on `connect()`:
  - read `?filter=behind` → statusValue
  - read `?q=…` → input target

Sync on every `filter()` call via `history.replaceState`:
  - filter=all → drop key
  - q empty → drop key
  - else preserve both

Uses `replaceState` (not `pushState`) so each keystroke / chip
click doesn't bloat the back-history. The page URL becomes
shareable for the filtered view.
This commit is contained in:
Guillem Arias
2026-05-14 21:25:51 +02:00
parent 04eb7abbb8
commit bfb50e1b19

View File

@@ -25,7 +25,11 @@ export default class extends Controller {
};
connect() {
this.#hydrateFromUrl();
this.syncChipState();
if (this.statusValue !== "all" || (this.hasInputTarget && this.inputTarget.value)) {
this.filter();
}
}
filter() {
@@ -56,6 +60,37 @@ export default class extends Controller {
}
this.updateEmptyState(visible, query, active);
this.#syncUrl();
}
#hydrateFromUrl() {
const params = new URLSearchParams(window.location.search);
const status = params.get("filter");
if (status && this.chipTargets.some((c) => c.dataset.status === status)) {
this.statusValue = status;
}
const q = params.get("q");
if (q && this.hasInputTarget) {
this.inputTarget.value = q;
}
}
#syncUrl() {
const params = new URLSearchParams(window.location.search);
if (this.statusValue && this.statusValue !== "all") {
params.set("filter", this.statusValue);
} else {
params.delete("filter");
}
const q = this.hasInputTarget ? this.inputTarget.value.trim() : "";
if (q) {
params.set("q", q);
} else {
params.delete("q");
}
const qs = params.toString();
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
window.history.replaceState(window.history.state, "", url);
}
updateEmptyState(visible, query, active) {