(ns icebreaker.xstate "Convenience methods for working with XState in a ClojureScript app. This namespace does not attempt to fully cover every feature of XState. Automatically enables the XState inspector when ?xstate-debug URL param is present" (:refer-clojure :exclude [send]) (:require ["@xstate/inspect" :as xstate-inspect] ["@xstate/react" :as xstate-react] ["xstate" :as xstate] [applied-science.js-interop :as j] [cljs-bean.core :as cljs-bean])) (defn inspect "It is very important that his function is called before `.start` of the machines you want to inspect." [{:keys [iframe]}] (xstate-inspect/inspect #js {:iframe iframe})) (def debug? (-> (js/URLSearchParams. js/window.location.search) (.get "xstate-debug") (boolean))) (defonce inspector (when debug? (js/console.log ::open-inspector!) (inspect {}))) (defn ->context [initial] #js {"__cljs_context" initial}) (def context-key "__cljs_context") (defn get-context [context] (j/get context context-key)) (defn send [event] (if (string? event) (xstate/send event) (xstate/send (clj->js event)))) (defn assign [updater] (xstate/assign (fn [context event] (j/assoc! context context-key (updater (get-context context) (cljs-bean/->clj event)))))) (defn guard [guard-fn] (fn [context event] (guard-fn (get-context context) (cljs-bean/->clj event)))) (defn add-guards! [js-obj guards-map] (doseq [[k guard-fn] guards-map] (j/assoc-in! js-obj [:guards k] (guard guard-fn))) js-obj) (defn add-actions! [js-obj actions-map] (doseq [[k action-fn] actions-map] (j/assoc-in! js-obj [:actions k] (assign action-fn))) js-obj) (defn create-machine [{:keys [id initial context states]} {:keys [guards actions]}] (xstate/createMachine #js {:id id :initial initial :context (->context context) :states (clj->js states)} (-> #js {} (add-guards! guards) (add-actions! actions)))) (defn interpret [machine] (xstate/interpret machine #js {:devTools debug?})) (defn use-machine "Like useMachine but instead of returning entire state of Machine, just return context. This might be something to revisit at some point but this way we can ensure that the thing we get back is the ClojureScript datastructure we're storing in context. https://xstate.js.org/docs/packages/xstate-react/" [machine opts] (let [actor (xstate-react/useMachine machine (clj->js opts)) state (first actor)] [(get-context (j/get state :context)) (second actor)])) (defn use-actor "Like useActor but instead of returning entire state of Actor, just return context. This might be something to revisit at some point but this way we can ensure that the thing we get back is the ClojureScript datastructure we're storing in context. https://xstate.js.org/docs/packages/xstate-react/" [service] (let [actor (xstate-react/useActor service) state (first actor)] [(get-context (j/get state :context)) (second actor)])) (defn use-selector "Like useSelector but applies selector function to CLJS context. (-> (js/React.useContext popover/*popover-seq-machine*) (xstate/use-selector :showing))" [service selector] (let [select (fn [state] (selector (get-context (j/get state :context))))] (xstate-react/useSelector service select =)))