/Hammer

An alternative to Om and Reagent reactive UI. Based on Mithril.js with emphasis on simplicity.

Primary LanguageClojure

Hammer

An alternative to Om and Reagent reactive UI. Based on Mithril.js with emphasis on simplicity.

Lein / Boot dependency

[hammer "0.1.1"]

An example program.

(ns ^:figwheel-load hammer-examples.example1
  (:require [hammer.ui :as ui]))

(defn view [this _]
  (let [title (ui/s this :some-title "Title Not Found")
        {textf :read text! :write} (ui/g this :global-text)
        {ltextf :read ltext! :write} (ui/l this :local-text)]
    (ui/vd :div {}
           (ui/vd :h3 {:style "color:red;"} title)
           (ui/vd :button {:onclick text!} (str "Global Count: " (textf)))
           (ui/vd :button {:onclick ltext!} (str "Local Count: " (ltextf))))))

(defn- global-counter-reader [app-state]
  #(:counter @app-state))

(defn- global-counter-writer [app-state]
  #(swap! app-state update :counter inc))

(defn- local-counter-reader [local-state]
  #(:counter @local-state))

(defn- local-counter-writer [local-state]
  #(vswap! local-state update :counter inc))

(defn init [{:keys [counter] :as app-state}]
  (let [local-state (volatile! {:counter 0})
        local-text (local-counter-reader local-state)
        local-text! (local-counter-writer local-state)
        global-text (global-counter-reader app-state)
        global-text! (global-counter-writer app-state)
        another-example {:in nil :out nil}]
    ;; Possible State Keys :read :write :update :delete :channels 
    (ui/component
     {:component {:view view}
      :state {:static {:some-title "Hello World!"}
              :global {:global-text {:read global-text :write global-text!}
                       :another-example {:channels another-example}}
              :local {:local-text {:read local-text :write local-text!}}}})))

Component Overview

The :component value should be a map with optional overrides. For example, to have an event run before a component is updated, your :component value should contain {:on-before-update (fn [this vnode old])}

(on-before-remove [state vnode])
(on-before-update [state vnode old])
(on-create [state vnode])
(on-init [state vnode])
(on-remove [state vnode])
(on-update [state vnode])
(gchannels [state])
(greaders [state])
(gwriters [state])
(global-v [state k])
(lchannels [state])
(lreaders [state])
(lwriters [state])
(local-v [state k])
(state-params [state])
(static-v [state k])
(view [state vnode])

Components can be mounted with mount!

(ui/mount! (.getElementById js/document "app") component)

State Overview

The :state value should be a map containing the keys :static, :global, and :local.

With the exception of :static, the values should be a map that provides functions or channels. Literal values should be placed in :static and accessed with the s function, g is used for :global, l for :local.

Example :state map.

(ui/component
  {:component {:view view}
   :state {:static {:some-title "Hello World!"}
           :global {:global-text {:read global-text :write global-text!}
                    :another-example {:channels another-example}}
           :local {:local-text {:read local-text :write local-text!}}}})

Getting value functions.

  (let [title (ui/s this :some-title "Title Not Found")
        {textf :read text! :write} (ui/g this :global-text)
        {ltextf :read ltext! :write} (ui/l this :local-text)])

Example Application

(ns ^:figwheel-load hammer-examples.todo
  (:require [hammer.ui :as ui]))

;; Example `app-state`
;;
;; {:todo/list [0 1 2]
;;  :todo/list-content ["one" "two" "three"]}

(declare local-reader-input
         local-writer-input
         global-reader-list
         global-writer-list
         view)

(defn init
  [{:keys [todo/list todo/list-content]
    :as app-state}]
  (let [local-state (volatile! {:value ""})
        input-reader (local-reader-input local-state)
        input-writer! (local-writer-input local-state)
        items-reader (global-reader-list app-state)
        items-writer! (global-writer-list input-reader app-state)]
    (ui/component
     {:component {:view view}
      :state {:static {:title "Example Title"}
              :local {:input {:read input-reader
                              :write input-writer!}}
              :global {:items {:read items-reader
                               :write items-writer!}}}})))

(defn view [this _]
  (let [title (ui/s this :title "Title Not Found")
        {input-f :read input-f! :write} (ui/l this :input)
        {items-f :read items-f! :write} (ui/g this :items)]
    (ui/vd :div {}
           (ui/vd :h4 {} title)
           (ui/vd :textarea
                  {:oninput input-f! :value (input-f) :style "width: 100%;"})
           (ui/vd :br)
           (ui/vd :button
                  {:onclick items-f! :style "width: 100%;"}
                  "Add")
           (ui/vd :br)
           (items-f))))

(defn- global-writer-delete-item
  [{:keys [todo/list todo/list-content]
    :as app-state}]
  (fn [id]
    (let [data @app-state
          items (:todo/list data)
          contents (:todo/list-content data)
          size (count items)
          limit (dec size)
          items' (-> limit range vec)
          contents' (->> (range size)
                         (keep
                          (fn [n]
                            (if (not= n id)
                              (get contents n))))
                         vec)]
      (swap! app-state
             assoc
             :todo/list items'
             :todo/list-content contents'))))

(defn- global-writer-move-item-up
  [{:keys [todo/list todo/list-content]
    :as app-state}]
  (fn [id]
    (if-not (zero? id)
      (let [data @app-state
            items (:todo/list data)
            contents (:todo/list-content data)
            limit (-> items count dec)
            old-id id
            old-content (get contents old-id)
            new-id' (dec id)
            new-id (if (neg? new-id') limit new-id')
            new-content (get contents new-id)]
        (swap! app-state
               update
               :todo/list-content
               (fn [x]
                 (-> x
                     (assoc-in [old-id] new-content)
                     (assoc-in [new-id] old-content))))))))

(defn- global-writer-move-item-down
  [{:keys [todo/list todo/list-content]
    :as app-state}]
  (fn [id]
    (let [data @app-state
          items (:todo/list data)
          contents (:todo/list-content data)
          size (count items)
          limit (dec size)]
      (if-not (= id limit)
        (let [old-id id
              old-content (get contents old-id)
              new-id' (inc id)
              new-id (if (> new-id' limit) 0 new-id')
              new-content (get contents new-id)]
          (swap! app-state
                 update
                 :todo/list-content
                 (fn [x]
                   (-> x
                       (assoc-in [old-id] new-content)
                       (assoc-in [new-id] old-content)))))))))

(def ^:private item-style
  (str "border-width: 1px;"
       "border-style: solid;"
       "border-color: black;"
       "padding: 4px;"
       "margin: 4px;"))

(def ^:private item-style-a
  (str "margin-right: 5px;"
       "padding-right: 5px;"))

(defn- global-reader-list
  [{:keys [todo/list todo/list-content]
    :as app-state}]
  (fn [& _]
    (let [data @app-state
          items (:todo/list data)
          contents (:todo/list-content data)
          move-up! (global-writer-move-item-up app-state)
          move-down! (global-writer-move-item-down app-state)
          delete! (global-writer-delete-item app-state)]
      (map (fn [id]
             (let [content (get contents id)]
               (ui/vd :div
                      {:id id :style item-style}
                      [(ui/vd :a
                              {:href "#"
                               :style item-style-a
                               :onclick #(delete! id)}
                              (ui/vd :b "delete"))
                       (ui/vd :a
                              {:href "#"
                               :style item-style-a
                               :onclick #(move-up! id)}
                              (ui/vd :b "up"))
                       (ui/vd :a
                              {:href "#"
                               :style item-style-a
                               :onclick #(move-down! id)}
                              (ui/vd :b "down"))
                       (ui/vd :div {} content)])))
           items))))

(defn- global-writer-list
  [read-value-fn
   {:keys [todo/list todo/listcontent]
    :as app-state}]   
  #(let [data @app-state
         items (:todo/list data)
         contents (:todo/list-content data)
         items' (conj items (count items))
         contents' (into [(read-value-fn)] contents)]
     (swap! app-state
            assoc
            :todo/list items'
            :todo/list-content contents')))

(defn- local-writer-input [{:keys [value] :as local-state}]
  #(this-as this
     (vswap! local-state assoc :value (.-value this))))

(defn- local-reader-input [{:keys [value] :as local-state}]
  #(:value @local-state))

License

Copyright © 2017 Ryan Kelker

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