Skip to content

Commit

Permalink
Added hook support for Fulcro integration
Browse files Browse the repository at this point in the history
  • Loading branch information
awkay committed Dec 31, 2023
1 parent eecb9e1 commit 34eaac7
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 0 deletions.
72 changes: 72 additions & 0 deletions Guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,78 @@ The runtime `env` in executable elements includes:
* `::sc/invocation-processors` - The supported invocation processors
* `::sc/execution-model` - The CLJC statechart ExecutionModel

== React Hooks

There is support for using a statechart as a co-located element of a hooks-based
component.

The basic idea is that the statechart will be started when the component
uses it, and when the component leaves the screen the chart is sent
an `:event/unmounted`. If the chart reaches a top-level final state, then
it will be GC'd from state.

The session ID of the chart is auto-assigned a random UUID, but you can specify
a known session ID to allow for a statechart to survive the component mount/unmount
cycle.

Here is an example of using this support to create a simple traffic light that can regulate (red/yellow/green) or
blink red:

[source]
-----
(defsc TrafficLight [this {:ui/keys [color]}]
{:query [:ui/color]
:initial-state {:ui/color "green"}
:ident (fn [] [:component/id ::TrafficLight])
:statechart (statechart {}
(state {:id :state/running}
(on :event/unmount :state/exit)
(transition {:event :event/toggle}
(script {:expr (fn [_ {:keys [blink-mode?]}]
[(ops/assign :blink-mode? (not blink-mode?))])}))
(state {:id :state/green}
(on-entry {} (script {:expr (fn [_ _] [(fops/assoc-alias :color "green")])}))
(send-after {:delay 2000
:id :gty
:event :timeout})
(transition {:event :timeout
:target :state/yellow}))
(state {:id :state/yellow}
(on-entry {} (script {:expr (fn [_ _] [(fops/assoc-alias :color "yellow")])}))
(send-after {:delay 500
:id :ytr
:event :timeout})
(transition {:event :timeout
:target :state/red}))
(state {:id :state/red}
(on-entry {} (script {:expr (fn [_ _] [(fops/assoc-alias :color "red")])}))
(send-after {:delay 2000
:id :rtg
:event :timeout})
(transition {:cond (fn [_ {:keys [blink-mode?]}]
(boolean blink-mode?))
:event :timeout
:target :state/black})
(transition {:event :timeout
:target :state/green}))
(state {:id :state/black}
(on-entry {} (script {:expr (fn [_ _] [(fops/assoc-alias :color "black")])}))
(send-after {:delay 500
:id :otr
:event :timeout})
(transition {:event :timeout
:target :state/red})))
(final {:id :state/exit}))
:use-hooks? true}
(let [{:keys [send! local-data]} (sch/use-statechart this {:data {:fulcro/aliases {:color [:actor/component :ui/color]}}})]
(dom/div {}
(dom/div {:style {:backgroundColor color
:width "20px"
:height "20px"}})
(dom/button {:onClick (fn [] (send! :event/toggle))}
(if (:blink-mode? local-data) "Blink" "Regulate")))))
-----

= Testing

Expand Down
62 changes: 62 additions & 0 deletions src/main/com/fulcrologic/statecharts/integration/fulcro/hooks.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
(ns com.fulcrologic.statecharts.integration.fulcro.hooks
(:require
[com.fulcrologic.fulcro.components :as comp]
[com.fulcrologic.fulcro.raw.application :as rapp]
[com.fulcrologic.fulcro.raw.components :as rc]
[com.fulcrologic.fulcro.react.hooks :as hooks]
[com.fulcrologic.statecharts.integration.fulcro :as scf]
[taoensso.timbre :as log]))

(defn use-statechart
"Start a statechart that is co-located on a component under the :statechart key of its options.
The component itself will be auto-assigned to the Actor :actor/component.
Returns a map with:
* :send! - A (fn ([event]) ([event data]) that can be used to send the chart events. The events that the statechart
receives will include (in :data) the value of `this` under the `:this` key.
* :config - The current statechart config (active states)
* :local-data - The local data of the statechart instance
* :aliases - Any fulcro state that has fulcro aliases on the actors
When the containing component leaves the screen, it will send an `:event/unmount`
to the chart. You can use this to move to a final state (which will GC the chart). If you
want to keep the chart running, then you should send `session-id`, or a new chart will be
created with a new ID (and the old one will just stay running with no associated UI)."
[this {:keys [session-id data]
:as start-args}]
(let [data (assoc-in data [:fulcro/actors :actor/component] (scf/actor (comp/react-type this) (comp/get-ident this)))
send-ref (hooks/use-ref nil)
id (hooks/use-generated-id)
session-id (or session-id id)
state-map (rapp/current-state this)
app (comp/any->app this)
machine-key (comp/class->registry-key (comp/react-type this))
_ (hooks/use-component app (rc/nc [:session/id
(scf/local-data-path session-id)
(scf/statechart-session-ident session-id)]
{:initial-state (fn [_] {})
:ident (fn [_] [:statechart/placeholder session-id])})
{:initialize? true})]
(hooks/use-gc this [:statechart/placeholder session-id] #{})
(when (nil? (.-current send-ref))
(scf/register-statechart! app machine-key (comp/component-options this :statechart))
(set! (.-current send-ref)
(fn send*
([event data]
(scf/send! this session-id event (assoc data :this this)))
([event]
(scf/send! this session-id event {:this this})))))
(hooks/use-lifecycle
(fn []
(let [running? (seq (scf/current-configuration this session-id))]
(if running?
(log/debug "Statechart with session id" session-id "was already running.")
(scf/start! (comp/any->app this) (assoc start-args :session-id session-id :data data :machine machine-key)))))
(fn [] (scf/send! this session-id :event/unmount)))
{:send! (.-current send-ref)
:local-data (get-in state-map (scf/local-data-path session-id))
:aliases (scf/resolve-aliases {:_event {:target session-id}
:fulcro/state-map state-map})
:config (scf/current-configuration this session-id)}))

0 comments on commit 34eaac7

Please sign in to comment.