A live language switch never reached the panes inside Simple Research

The composite re-pushed its FILTER state to the map and the grid on every
change, and nothing else. So a host switching language moved the composite's own
chrome and stopped there: the outer element received the new profile-config,
the push re-parsed each child's EXISTING attribute, and the boot `lang` survived
every write. Measured on a live en to ar switch: the map and the grid both stayed
English while the frame around them changed.

The panes were never at fault. Each child's own chain is fully reactive -- it is
reactive to its own prop, and its prop was frozen.

The map now carries the locale in the same push that carries the filters. The
grid needs its own line, because its three linked facets travel as separate
attributes while its language travels in profile-config, which this composite
set only when a host supplied a list, and never again. And a locale change now
triggers a push at all: the existing watchers cover filter state only.

The grid write is conditional so an unchanged locale writes nothing. That
matters here more than it looks: setting a pane's attribute makes it re-filter
and emit, and the composite reads those emissions -- which is what the `pushing`
guard above exists for, after a measured round trip once cancelled a selection
the composite had just applied.

Verified two-sided by driving the real pushToChildren against stub elements:
the locale reaches both panes, and a repeat at the same locale adds no write.
Removing the map's line fails that assertion alone and leaves the grid's passing.
This commit is contained in:
Doxa Maps 2026-09-29 19:29:12 +00:00
parent c6f9e11427
commit b0d08224c1

View file

@ -423,6 +423,13 @@ function pushToChildren() {
let cfg = {}
try { cfg = JSON.parse(map.getAttribute('profile-config') || '{}') } catch (_) { cfg = {} }
cfg.externalFilters = { region: linked.region.value, religion: linked.religion, language: linked.language.label, fitSlugs: gridSlugs.value }
// ★ AND THE LOCALE. This rewrote the FILTERS and nothing else, so a live language switch moved
// the composite's own chrome and never reached the panes: the outer element got the new
// profile-config, this re-parsed the child's EXISTING attribute, and the boot `lang` survived
// every push. MEASURED: the map stayed English on a live en->ar switch while the composite
// around it changed. The leaf's own chain is fully reactive and was never at fault — it is
// reactive to its own prop, and its prop was frozen.
if (hostLang.value) cfg.lang = hostLang.value
map.setAttribute('profile-config', JSON.stringify(cfg))
}
const grid = rootEl.querySelector('people-groups-grid')
@ -433,8 +440,21 @@ function pushToChildren() {
// so a stable ISO code matches in every locale while a display name only matches in the one it
// was fetched in.
grid.setAttribute('primary-language', gridLanguage())
// The grid's three linked facets travel as their own attributes, but its LANGUAGE does not —
// it reads that from profile-config, which this composite set only when a host list was
// supplied, and never again. Same frozen-prop defect as the map above, one channel over.
if (hostLang.value) {
let gcfg = {}
try { gcfg = JSON.parse(grid.getAttribute('profile-config') || '{}') } catch (_) { gcfg = {} }
if (gcfg.lang !== hostLang.value) {
gcfg.lang = hostLang.value
grid.setAttribute('profile-config', JSON.stringify(gcfg))
}
}
}
}
// A locale change has to reach the panes too; `linked` and `gridSlugs` only cover filter state.
watch(hostLang, pushToChildren)
watch(linked, pushToChildren, { deep: true })
// The slug set changes without any facet changing (a search term, a cleared filter), so it needs
// its own trigger — `linked` alone would leave the camera behind.