/carry

ClojureScript application framework.

Primary LanguageClojureMIT LicenseMIT

Carry

ClojureScript single-page application framework inspired by re-frame, Elm Architecture, Redux and Cerebral.

Carry provides a structure for making GUI application code easier to modify, debug, test and be worked on by multiple programmers.

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

Status

Stable. Was used in production.

I now focus on the successor Aide framework instead.

Features

Pattern

Carry enforces:

  • Separation of presentation code.
  • Events as first-class citizens.
  • Splitting event handling code into side-effectful and "pure" model updating phases.
  • Storing model in a single observable atom.

It also advises to decouple view and view model code in the presentation layer:

pattern

  • An app is defined by its initial model value, signal handler and action handler.
  • All app state is stored inside a single model atom.
  • Anyone can read model value at any given time and subscribe to its changes.
  • Signal handler performs side effects and dispatches actions.
  • Anyone can dispatch a new signal: signal handler, views, timers, etc.
  • Typically UI layer dispatches signals on UI events and subscribes to model changes to redraw the GUI when needed.
  • Model can be modified only by dispatching actions.
  • Only signal handler can dispatch actions.
  • Action handler is a pure function which returns a new model value based on an incoming action.

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/blueprint)
      [_ 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]]
            [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"]])

Blueprint:

(def -initial-model {:val 0})

(defn -on-signal
  [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 -on-action
  [model action]
  (match action
         :increment (update model :val inc)
         :decrement (update model :val dec)))

(def blueprint {:initial-model -initial-model
                :on-signal     -on-signal
                :on-action     -on-action})

Packages

UI Bindings

Middleware

Documentation

More information can be found at the project site:

License

Copyright © 2016 Yuri Govorushchenko.

Released under an MIT license.