Aide
ClojureScript/Clojure event-driven application framework. Successor to Carry.
Why not Carry?
-
Carry framework is very strict about separating signals (side effects) and actions (model changing functions). In a long run it turned out to produce a lot of boilerplate (e.g. many signal handlers are anemic and only dispatch an action). Such separation was mainly needed for implementing Redux-ish time traveling debugger, but in the end I don’t find such tool too useful, it’s pretty easy to debug the app using traditional approaches.
-
By default Carry recommends using keywords for events and actions. It makes it cumbersome to maintain the codebase, because IDEs don’t understand this abstraction.
Why not re-frame?
re-frame has an implicit global state and hard dependency on Reagent.
Also it’s based on patterns which are too heavyweight:
-
Subscriptions (vs., for instance, Reagent reactions or Rum derived atoms).
-
Effects & co-effects (vs. usual function and/or method calls and using mocks in tests).
-
Interceptors (vs. middleware pattern).
-
Async event handling queue.
Features
-
Events can be defined as vars (so that IDE can jump to definition and compiler can detect undefined events).
-
Event definitions look similarly to
defn
(so that IDE can highlight arguments, etc.). -
Events can be async.
-
Events can be serialized/deserialized. E.g. it’s possible to record and replay user’s actions.
-
No enforced separation between "side-effectful" and "pure" events.
-
Testable. It’s possible to test event handlers similarly to usual functions and also mock the events.
-
Supports injecting additional functionality via third-party packages (such as logging, routing, etc.). See https://github.com/metametadata/aide/tree/master/contrib.
-
Proposes a pattern for SPAs (single app atom, view/view-model separation).
-
Framework core is agnostic to UI and model layers.
-
Proposes app lifecycle pattern (via standard start/stop events). This is needed for "stateful" third-party packages.
-
(TODO) Time traveling debugger.
Status
Alpha (API is a subject to change, needs more examples, docs, etc.). Is used in production.
Code Examples
Define an app which has some model:
(ns app.core
(:require [aide.core :as aide]))
(let [*model (atom {:val 0})
app (aide/object {:*model *model})]
,,,)
Define an event which modifies the app model:
(aide/defevent on-increment
[app _data]
(swap! (:*model app) update :val inc))
Define an event which emits another event asynchronously:
(aide/defevent on-delayed-increment
[app delay-ms]
(.setTimeout js/window #(aide/emit app on-increment) delay-ms))
Emit events into the app:
(aide/emit app aide-lifecycle/on-start)
(aide/emit app on-increment)
(aide/emit app on-delayed-increment 1000)
(aide/emit app aide-lifecycle/on-stop)
Middleware example:
(defn add-logging
"Will log all events."
[object]
(update app :aide.core/emit
(fn wrap-emit
[original-emit]
(fn emit
[object event data]
(println (str event) (pr-str data))
(original-emit object event data)))))
(def app-with-logging (-> app
add-logging))
Integration with Reagent
Developer Guide
Tests (Clojure)
Run tests once: lein test
or lein test-refresh :run-once
Autorun tests: lein test-refresh
Tests (ClojureScript)
To run tests once:
lein doo phantom test once
or automatically re-run on code changes:
lein doo phantom test auto
License
Copyright © 2018 Yuri Govorushchenko.
Released under an MIT license.