State Coordination for Rum
Clojure HTML
Latest commit 99faf10 Feb 22, 2017 @roman01la committed on GitHub Update CHANGELOG.md

README.md

scrum logo

State Coordination for Rum

Clojars

Table of Contents

Motivation

Have a simple, re-frame like state management facilities to build web apps with Rum library while leveraging its API.

Features

  • Decoupled application state in a single atom
  • Reactive queries
  • A notion of controller to keep application domains separate

Installation

Add to project.clj: [org.roman01la/scrum "0.1.0-SNAPSHOT"]

How it works

With Scrum you build everything around a well known architecture pattern in modern SPA development:

DISPATCH EVENT

HANDLE EVENT

QUERY STATE

RENDER

Dispatcher

Dispatcher communicates intention to perform an action, whether it is state update or a network request.

(scrum.dispatcher/dispatch! :controller-name :action-name &args)

Controllers

Controller is a multimethod which performs intended actions against application state. A controller usually have at least an initial state and :init method.

(def initial-state 0)

(defmulti control (fn [action] action))

(defmethod control :init [action &args db]
  (assoc db :counter initial-state))

(scrum.dispatcher/register! :counter control)

Subscriptions

A subscription is a reactive query into application state. It is basically an atom which holds a part of the state value. Optional second argument is an aggregate function which computes a materialized view. You can also do parameterized and aggregate subscriptions.

Actual subscription happens in Rum component via rum/reactive mixin and rum/react function which hooks in a watch function to update a component when an atom gets an update.

;; normal subscription
(def fname (scrum.core/subscription [:users 0 :fname]))

;; a subscription with aggregate function
(def full-name (scrum.core/subscription [:users 0] #(str (:fname %) " " (:lname %))))

;; parameterized subscription
(defn user [id]
  (scrum.core/subscription [:users id]))

;; aggregate subscription
(def discount (scrum.core/subscription [:user :discount]))
(def goods (scrum.core/subscription [:goods :selected]))

(def shopping-cart
  (rum/derived-atom [discount goods] ::key
    (fn [discount goods]
      (let [price (->> goods (map :price) (reduce +))]
        (- price (* discount (/ price 100)))))))

;; usage
(rum/defc NameField < rum/reactive []
  (let [user (rum/react (user 0))])
    [:div
     [:div.fname (rum/react fname)]
     [:div.lname (:lname user)]
     [:div.full-name (rum/react full-name)]
     [:div (str "Total: " (rum/react shopping-cart))]])

Usage

(ns counter.core
  (:require [rum.core :as rum]
            [scrum.dispatcher :as d]
            [scrum.core :refer [subscription]]))

;;
;; define controller & event handlers
;;

(def initial-state 0)

(defmulti control (fn [action] action))

(defmethod control :init [_ _ db]
  (assoc db :counter initial-state))

(defmethod control :inc [_ _ db]
  (update db :counter inc))

(defmethod control :dec [_ _ db]
  (update db :counter dec))


;;
;; define subscription
;;

(def counter (subscription [:counter]))


;;
;; define UI component
;;

;; create dispatcher for particular controller
(def dispatch-counter! (partial d/dispatch! :counter))

(rum/defc Counter < rum/reactive []
  [:div
   [:button {:on-click #(dispatch-counter! :dec)} "-"]
   [:span (rum/react counter)]
   [:button {:on-click #(dispatch-counter! :inc)} "+"]])


;;
;; start up
;;

;; register controller
(d/register! :counter control)

;; initialize registered controllers
(defonce dispatched-init (d/broadcast! :init))

;; render
(rum/mount (Counter)
           (. js/document (getElementById "app")))

Libraries

Routing

Check out scrum.router, a minimal routing library for Scrum.

Roadmap

  • Get rid of global state
  • Storage agnostic architecture? (Atom, DataScript, etc.)
  • Better effects handling (network, localStorage, etc.)

License

Copyright © 2017 Roman Liutikov

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.