/carry

ClojureScript application framework.

Primary LanguageClojureMIT LicenseMIT

Carry

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.

Clojars Project Gitter Slack

Features

Pattern

pattern

  • 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.

Example (counter app)

Demo, Source code

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})

Packages

UI Bindings

Middleware

Documentation

More information can be found at the project site:

License

Copyright © 2016 Yuri Govorushchenko.

Released under an MIT license.