loganlinn.log

Push State and ClojureScript

A common problem people have when using HTML5 pushState is that previous scroll positions are not always restored when navigating back. A strategy to address this is to store the current scroll position in history.state before navigating forward.

Let’s see how you might use the Push State API directly to accomplish this:

1
2
3
4
5
6
7
8
9
10
(declare scroll-top) ;; returns current scroll position

(defn set-scroll-top! []
  (let [state (or (.-state js/history) #js {})]
    (aset state "scroll-top" (scroll-top))
    (.replaceState js/history state )))

(defn get-scroll-top []
  (when-let [state (.-state js/history)]
    (aget state "scroll-top")))

This works, but what if we could interface with history.state as if it were Clojure data? More specifically, stateful Clojure data, like an atom.

It might look something like this:

1
2
3
4
5
6
7
8
9
(require 'history)

(declare scroll-top)

(defn set-scroll-top! []
  (swap! history/state assoc :scroll-top (scroll-top)))

(defn get-scroll-top []
  (:scroll-top @history/state))

And in fact, this can be done by anonymously implementing a few protocols.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(ns history)

(def state
  (let [clj-state #(js->clj (.-state js/history) :keywordize-keys true)]
    (reify
      IDeref
      (-deref [_]
        (clj-state))
      IReset
      (-reset! [_ v]
        (.replaceState js/history (clj->js v) (.-title js/document)))
      ISwap
      (-swap! [s f]
        (-reset! s (f (clj-state))))
      (-swap! [s f x]
        (-reset! s (f (clj-state) x)))
      (-swap! [s f x y]
        (-reset! s (f (clj-state) x y)))
      (-swap! [s f x y more]
        (-reset! s (apply f (clj-state) x y more))))))

You can checkout a more complete wrapper for js/history here: https://gist.github.com/loganlinn/930c043331c52cb73a98