Status: done Completed: 2026-02-18 Priority: P1 Created: 2026-02-18 Owner: conductor
ui_routing2.cljc needs a headless-capable URL history abstraction so that routing+URL behavior can be tested in CLJ without a browser. The browser-touching surface is just 3 functions (current-url, push-url!, replace-url!) plus popstate listener management. A protocol cleanly separates the history stack concern from pure routing logic.
Additionally, the current pushState passes nil as state data. The W3C History API allows passing state that comes back in popstate events. By pushing a monotonic index {index: N}, the popstate listener can determine navigation direction (back vs forward) and the on-save handler can distinguish browser-initiated from programmatic navigation — preventing history stack corruption.
Define URLHistoryProvider protocol in route_url.cljc with these methods:
current-href — returns current URL string or nilcurrent-index — returns the monotonic index of the current history entry-push-url! — push new URL, increment index, store index in pushState-replace-url! — replace current URL (same index)go-back! — navigate back, fire popstate callbackgo-forward! — navigate forward, fire popstate callbackset-popstate-listener! — register (fn [index]) callback, or nil to removeImplement BrowserURLHistory (CLJS only, deftype):
js/window.history and js/window.locationcounter-atom for monotonic indexreplaceState with {index: 0} so that back-before-start produces index=0 not nilpushState passes #js {:index N} as statereplaceState passes #js {:index current} as stateset-popstate-listener! wraps callback to extract event.state.index; passes nil index if event.state is null (back past initial entry)go-back!/go-forward! delegate to js/window.historyImplement SimulatedURLHistory (cross-platform, defrecord):
{:entries [{:url "/" :index 0}] :cursor 0 :counter 0 :listener nil}-push-url! truncates forward entries at cursor, appends new entry with incremented index-replace-url! replaces URL at cursor position (same index)go-back! decrements cursor (if > 0), invokes listener synchronously with target entry's index. Cursor update happens FIRST, then listener fires (listener sees consistent state).go-forward! increments cursor (if < end), invokes listener synchronously with target entry's index. Same order guarantee.go-back! at cursor=0 and go-forward! at end are no-ops (no listener call)set-popstate-listener! stores callback in atomInspection helpers (functions, not protocol methods):
(history-stack provider) — vector of URL strings(history-cursor provider) — current cursor position(history-entries provider) — full entries with indicesExisting current-url, push-url!, replace-url! 0-arity functions remain unchanged for backward compatibility.
integration/fulcro/route_url.cljc — Protocol definition, both implementations, constructors, inspection helpersAdd protocol and implementations after the existing "Browser URL utilities" section in route_url.cljc. The BrowserURLHistory deftype goes inside #?(:cljs ...). The SimulatedURLHistory defrecord is cross-platform. Existing bare functions (current-url, push-url!, replace-url!) are untouched — the protocol is a new abstraction consumed by install-url-sync! (separate spec).
SimulatedURLHistory unit test: push 3 URLs, verify stack = [initial, url1, url2, url3], cursor = 3SimulatedURLHistory unit test: push 3, go-back twice, cursor = 1, current-href = url1SimulatedURLHistory unit test: push 3, go-back 2, go-forward 2, back at url3 (no corruption)SimulatedURLHistory unit test: push 3, go-back 2, push new-url → truncates forward history, stack = [initial, url1, new-url]SimulatedURLHistory unit test: replace-url changes URL at cursor but not indexSimulatedURLHistory unit test: go-back invokes listener with correct indexSimulatedURLHistory unit test: go-back at cursor=0 is no-op, no listener callBrowserURLHistory smoke test (CLJS): push-url stores index in history.state, popstate returns itpush-url!/replace-url!/current-url still work (backward compat)Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |