ClojureScript single-page application framework inspired by re-frame, Elm architecture, Redux and Cerebral.
The core of the framework is a simple state management library. UI bindings, routing, debugger, etc. are implemented as separate optional packages.
- Functional API with no globals makes apps easy to extend and unit test.
- Agnostic to UI layer: can be effectively used with Reagent (via carry-reagent package) or any other view layer that is able to re-render UI in response to app model changes.
- Time traveling debugger inspired by Redux DevTools and Cerebral Debugger.
- Live code editing using Figwheel and debugger's replay mode.
- Fractality: Elm-ish architecture can be applied to create composite apps.
- Core library can be also used in Clojure projects.
- An app is defined by its initial model value, controller and reconciler.
- All app state is stored inside a single model atom.
- Anyone can read model value at any given time and subscribe to its changes.
- Controller function receives signals to perform side effects and dispatch actions.
- Anyone can dispatch a new signal: controller, views, timers, etc.
- Model can be modified only by dispatching actions.
- Only controller can dispatch actions.
- Reconciler is a pure function which returns a new model value based on an incoming action.
- When UI layer subscribes to model changes we get a unidirectional data flow: UI -> signal -> action -> model -> UI -> etc.
HTML:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carry • Counter</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
</head>
<body>
<div id="root"></div>
<script src="js/compiled/frontend.js" type="text/javascript"></script>
</body>
</html>
Main file:
(ns app.core
(:require [counter.core :as counter]
[carry.core :as carry]
[carry-reagent.core :as carry-reagent]
[reagent.core :as r]))
(let [app (carry/app counter/spec)
[_ app-view] (carry-reagent/connect app counter/view-model counter/view)]
(r/render app-view (.getElementById js/document "root"))
((:dispatch-signal app) :on-start))
UI (using Reagent and carry-reagent):
(ns counter.core
(:require [cljs.core.match :refer-macros [match]])
(:require-macros [reagent.ratom :refer [reaction]]))
(defn view-model
[model]
{:counter (reaction (str "#" (:val @model)))})
(defn view
[{:keys [counter] :as _view-model} dispatch]
[:p
@counter " "
[:button {:on-click #(dispatch :on-increment)} "+"] " "
[:button {:on-click #(dispatch :on-decrement)} "-"] " "
[:button {:on-click #(dispatch :on-increment-if-odd)} "Increment if odd"] " "
[:button {:on-click #(dispatch :on-increment-async)} "Increment async"]])
Spec:
(def -initial-model {:val 0})
(defn -control
[model signal _dispatch-signal dispatch-action]
(match signal
:on-start nil
:on-stop nil
:on-increment
(dispatch-action :increment)
:on-decrement
(dispatch-action :decrement)
:on-increment-if-odd
(when (odd? (:val @model))
(dispatch-action :increment))
:on-increment-async
(.setTimeout js/window #(dispatch-action :increment) 1000)))
(defn -reconcile
[model action]
(match action
:increment (update model :val inc)
:decrement (update model :val dec)))
(def spec {:initial-model -initial-model
:control -control
:reconcile -reconcile})
More information can be found at the project site:
Copyright © 2016 Yuri Govorushchenko.
Released under an MIT license.