/aide

ClojureScript/Clojure event-driven application framework.

Primary LanguageClojureMIT LicenseMIT

Aide

ClojureScript/Clojure event-driven application framework. Successor to Carry.

Clojars Project

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.