Skip to content
This repository has been archived by the owner on Oct 24, 2019. It is now read-only.
/ clidget Public archive

Clidget is a lightweight CLJS state utility that allows you to build UIs through small, composable ‘widgets’.

Notifications You must be signed in to change notification settings

jarohen/clidget

Repository files navigation

Clidget

Clidget is a lightweight CLJS state utility that allows you to build UIs through small, composable ‘widgets’.

Getting started

Add the following to your project.clj

[jarohen/clidget "0.2.0"]

and the following to your CLJS :require:

(:require [clidget.widget :refer-macros [defwidget]])

For the Clidget changelog, including breaking changes, please see the changelog.

Clidget’s Rationale:

When writing Clidget, I had the following aims:

  • Do one thing, and do it well - Clidget does not aim to be an ‘all batteries included’ framework. In particular, it leaves you free to choose how to render DOM elements (do you prefer Hiccup-like libraries? Mustache templating?) and how to handle events.

    Clidget is good at knowing when to re-render widgets, the rest is up to you.

    This means that the ease/maintainability/expressiveness of Clidget is not dependent on waiting for new versions of Clidget - if DOM libraries get better or an amazing new library is released, Clidget users get the benefits immediately!

  • No Magic - ‘magic’ is great when it’s a small example (‘wow, that’s magic! How did they do that so concisely?!’) but not so great when the magic stops working, you stray off the beaten track, or encounter an incomprehensible error.

    Clidget uses vanilla Clojure data structures and functions, and is mostly based around standard Clojure watches on atoms. It expects you to return standard JS DOM elements (which you can pass to other JS libraries as-they-are)

  • Immutability where possible - it goes without saying that immutable values are far easier to reason about and test.

    Even though the DOM is inherently mutable, the widgets that you write are functions that take in values and return a value (and can be tested as such).

  • Preserve Clojure’s composability - Clojure is great at composing small libraries together (I believe it’s one of Clojure’s core strengths) so let’s ensure that you can use other libraries easily (even other JS libraries).

If you’re looking for a comparison with Om and Reagent, it can be found here.

Example Clidget applications

If you want to dive straight into examples, there are two in the repo: a simple counter (in the clidget-sample directory), and a TodoMVC example in the todomvc directory.

You can run the examples by cloning the repo, cd’ing, and running lein dev.

The ‘back of an envelope’ story:

The basic premise is that clidget.widget/defwidget watches the application state for you, and invites you to re-render the component when any of the state changes. The values destructured in the first param are just that - values - that you can use to build a DOM element:

(defwidget counter-widget [{:keys [counter]} events-ch]
  (node
   [:div
    [:h2 "counter is now: " counter]
    [:p
     (doto (node [:button.btn "Inc counter"])
       (d/listen! :click #(a/put! events-ch :inc-counter)))]]))

You are free to use whichever DOM rendering/events handling libraries you choose (I’m not going to impose a particular style on you). If you’re stuck with where to get started with these, I highly recommend Dommy and Clojure’s own core.async!

To include a widget in the page, call it (it’s just a function!), and provide it with the atoms that it needs to watch:

(set! (.-onload js/window)
      (fn []
        (let [!counter (atom 0)
              events-ch (doto (a/chan)
                          (watch-events! !counter))]

          (d/replace-contents! (.-body js/document)
                               (node [:h2 {:style {:margin-top "1em"}}
                                      (counter-widget {:!counter !counter} events-ch)])))))

Note the exclamation mark in :!counter that you pass to counter-widget - this informs Clidget that you’re passing an atom. (If you don’t put an exclamation mark in, Clidget will treat it as a value - more on this later.)

To complete the example, watch-events! then needs to watch any events coming out of the widget, and update the state accordingly (no Clidget here):

(defn watch-events! [events-ch !counter]
  (go-loop []
    (when-let [event (a/<! events-ch)]
      (when (= event :inc-counter)
        (swap! !counter inc))
      (recur))))

Diving into ‘defwidget’

Parameters

The underlying format of defwidget is:

(defwidget widget-name [state-declaration & other-param-names]
  widget-body)

;; called with:
(widget-name system-state param1 param2)

defwidget treats its first parameter specially - this is the parameter where the widget declares the parts of the state map that it’s interested in. The ‘other parameters’ are passed through as-is by Clidget; feel free to use them for other values that the widget needs.

The majority of the defwidget macro is concerned with binding the atoms in the system-state map to the state-declaration; this section aims to explain how the binding works.

Within the state-declaration map, Clidget particularly looks for the :keys and :locals keys (the :locals key will be covered later).

Think of the :keys entry the same as normal de-structuring - we’re essentially de-structuring the system-state map - but with extra state-watching functionality. For each symbol (let’s say ‘counter’), Clidget will:

  • look up the value in the system-state map. If it finds a ‘:counter’ entry, it will bind the ‘counter’ variable to the value provided.
  • If there’s no value, it’ll look up an atom in the system-state map, prefixed with an exclamation mark (e.g. for ‘counter’, it will look up ‘:!counter’). If it finds an atom, it will assume that the appearance of the widget depends on the current value of the atom. The widget will be re-evaluated every time the atom changes value, and the variable will be bound to the new value of the atom.
  • If it can’t find either a value under ‘:counter’, or an atom under ‘:!counter’, then ‘counter’ will be nil in the widget body.

Using the example above, we can now see how the ‘counter’ is bound:

(defwidget counter-widget [{:keys [counter]} events-ch]
  (node
   [:div
    [:h2 "counter is now: " counter]
    [:p
     (doto (node [:button.btn "Inc counter"])
       (d/listen! :click #(a/put! events-ch :inc-counter)))]]))

;; using the counter-widget:
(let [!counter (atom 0)
      events-ch (a/chan)]
  
  (d/replace-contents! (.-body js/document)
                       (counter-widget {:!counter !counter} events-ch)))

For a complete example, have a look at the ‘clidget-sample’ demo application.

Testing widgets

In the above snippet, counter-widget is just a function, and so it can be called with test parameters, like any other CLJS function.

Because defwidget looks for values in the system-state map before looking for atoms, we can pass a value for the counter, and see what the DOM element would look like:

(defwidget counter-widget [{:keys [counter]} events-ch]
  (node
   ... as before ...))

;; In Chrome, this outputs a DOM tree in the developer console.
(js/console.log (counter-widget {:counter 4} nil))

We can also mock an events channel to test the events, if need be

Widget local state

Widgets occasionally need to keep local state - for example, a widget that can be edited in-place needs to store the state of whether it is currently being edited or viewed.

Clidget handles this using a ‘locals’ map, again declared in the first parameter to defwidget. When specifying a local atom, you also specify an initial value, as follows:

(defwidget todo-item-widget [{:keys [editing? !editing? todo]
                              :locals {:!editing? (atom false)}}
                             ...]
  (node
   [:li 
    (if editing?
      (doto (node (node-when-viewing ...))
        (d/listen! :dblclick #(reset! !editing? true)))
      
      (node (node-when-editing ...)))]))

Here we declare !editing? as a local atom, with a default value of false. We specify both editing? and !editing? in the :keys map, so that we have access to both editing?, the current value of the atom (to check which version of the widget we display) and !editing?, the atom itself (in order to set the ‘am-I-editing’ state in the dblclick listener).

When !editing? is reset to true, Clidget will re-render the widget, but this time it will render the (node-when-editing ...) node.

Sub-widget keys

Clidget uses the extra parameters (i.e. values passed in the system map) that you provide to a widget to differentiate between sub-widgets. This works out fine in most cases - for example, if you have a widget that contains a list of widgets, chances are you’ll provide a widget-unique ID as an extra parameter to the sub-widget, as follows:

(defwidget todo-item-widget [{:keys [todo]}]
  (let [{:keys [caption id]} todo]
    (node
     [:li caption])))

(defwidget todo-list-widget [{:keys [todos]}]
  (node
   [:ul
    (for [{:keys [id] :as todo} todos]
      (todo-item-widget {:todo todo}))]))

The ‘id’ here in the todo map is enough to differentiate between different todo items, so Clidget will know when to re-render each individual item.

In the rare case that the combination of extra parameters may not be unique amongst sub-widgets, you can provide a unique (but consistent, for any given sub-widget) :clidget/widget-key key in the state map, as shown in the following (very contrived) example:

(defwidget child-widget [{:keys [elem]}]
  (node
   [:li ...]))

(defwidget parent-widget [{:keys [coll]}]
  (node
   [:ul
    (for [[index elem] (map vector (range) coll)]
      (child-widget {:clidget/widget-key index
                     :elem elem}))]))

Here, we are using the index of the ‘element’ in the ‘collection’ as a disambiguator.

As mentioned above, this really should be a rare occurrence!

Feedback/suggestions/ideas/bug reports/PRs etc

If you’ve made it this far through the README (congratulations!), I’d really appreciate your feedback and suggestions.

I can be reached in the traditional GitHub ways, or on Twitter at @jarohen.

Thanks!

James

License

Copyright © 2014 James Henderson

Distributed under the Eclipse Public License, the same as Clojure

About

Clidget is a lightweight CLJS state utility that allows you to build UIs through small, composable ‘widgets’.

Resources

Stars

Watchers

Forks

Packages

No packages published