Skip to content

Latest commit

 

History

History
2217 lines (1749 loc) · 85.8 KB

GettingStarted.adoc

File metadata and controls

2217 lines (1749 loc) · 85.8 KB

Getting Started

Table of Contents

Your project will need a few basic directories and files:

# mkdir -p script resources/public/js src/main/app src/dev/cljs

Untangled requires that you include a few things in your dependency list.

  • Om Next (1.0.0-beta1 or above)

  • Clojure (1.9.0-alpha16 or above)

  • Clojurescript (1.9.542 or above)

  • Untangled (this library)

A typical project will also include figwheel, devtools, and at least one development build configuration.

This goes in project.clj:

(defproject my-project "0.0.1"
  :description "My Project"
  :dependencies [[org.clojure/clojure "1.9.0-alpha16"]
                 [org.clojure/clojurescript "1.9.562"]
                 [org.omcljs/om "1.0.0-beta1"]
                 [awkay/untangled "1.0.0-SNAPSHOT"]]

  :source-paths ["src/main"]
  :resource-paths ["resources"]
  :clean-targets ^{:protect false} ["resources/public/js" "target" "out"]

  :plugins [[lein-cljsbuild "1.1.6"]]

  :cljsbuild {:builds
              [{:id           "dev"
                :source-paths ["src/main" "src/dev"]
                :figwheel     {:on-jsload "cljs.user/refresh"}
                :compiler     {:main                 cljs.user
                               :output-to            "resources/public/js/app.js"
                               :output-dir           "resources/public/js/app"
                               :preloads             [devtools.preload]
                               :asset-path           "js/app"
                               :optimizations        :none}}]}

  :profiles {:dev {:source-paths ["src/dev" "src/main"]
                   :dependencies [[binaryage/devtools "0.9.2"]
                                  [figwheel-sidecar "0.5.9"]]}})

Figwheel is a hot-reload development tool. We recommend using figwheel sidecar so you can easily start the project from the command line or use it from the REPL support built into IntelliJ. This requires a couple of CLJ source files:

Put this in script/figwheel.clj:

(require '[user :refer [start-figwheel]])

(start-figwheel)

and this in src/dev/user.clj:

(ns user
  (:require
    [figwheel-sidecar.system :as fig]
    [com.stuartsierra.component :as component]))

(def figwheel (atom nil))

(defn start-figwheel
  "Start Figwheel on the given builds, or defaults to build-ids in `figwheel-config`."
  ([]
   (let [figwheel-config (fig/fetch-config)
         props           (System/getProperties)
         all-builds      (->> figwheel-config :data :all-builds (mapv :id))]
     (start-figwheel (keys (select-keys props all-builds)))))
  ([build-ids]
   (let [figwheel-config   (fig/fetch-config)
         default-build-ids (-> figwheel-config :data :build-ids)
         build-ids         (if (empty? build-ids) default-build-ids build-ids)
         preferred-config  (assoc-in figwheel-config [:data :build-ids] build-ids)]
     (reset! figwheel (component/system-map
                        :figwheel-system (fig/figwheel-system preferred-config)
                        :css-watcher (fig/css-watcher {:watch-paths ["resources/public/css"]})))
     (println "STARTING FIGWHEEL ON BUILDS: " build-ids)
     (swap! figwheel component/start)
     (fig/cljs-repl (:figwheel-system @figwheel)))))

In order to get the thing building, we need two more files with some application code in them.

Place this in src/main/app/basic_ui.cljs:

(ns app.basic-ui
  (:require [untangled.client.core :as uc]
            [om.dom :as dom]
            [om.next :as om :refer [defui]]))

; Create an application
(defonce app-1 (atom (uc/new-untangled-client)))

; Create a simple UI
(defui Root
  Object
  (render [this]
    (dom/div nil "Hello World.")))

and this in src/dev/cljs/user.cljs (NOTE THIS IS DIFFERENT FROM src/dev/user.clj!!!)

(ns cljs.user
  (:require
    [app.basic-ui :refer [app-1 Root]]
    [untangled.client.core :as uc]))

; so figwheel can call it on reloads. Remounting just forces a UI refresh.
(defn refresh [] (swap! app-1 uc/mount Root "app-1"))

(refresh) ; for initial mount

A single basic HTML file will be needed, and it must have an element on which to mount your application.

Put this in resources/public/index.html:

<!DOCTYPE html>
<html>
    <body>
        <div id="app-1"></div>
        <script src="js/app.js" type="text/javascript"></script>
    </body>
</html>

You can now run this project in various ways.

From the command line:

# lein run -m clojure.main script/figwheel.clj

Within IntelliJ:

  • Run → Edit Configurations…​

  • Press the '+' button, and choose Clojure REPL → Local

    • Give it a name (like dev)

    • Choose "Use clojure.main in normal JVM process" (important: it defaults to nREPL which won’t work right)

    • In Parameters add script/figwheel.clj

Now you should be able to start it from the Run menu.

You should see the application printing "Hello World" at: http://localhost:3449

Now that you have a basic project working, let’s understand how to add some content!

Important
When developing it is a good idea to: Use Chrome (the devtools only work there), have the developer’s console open, and in the developer console settings: "Network, Disable cache (while DevTools is open)", and "Console, Enable custom formatters".

One of the most maddening things that can happen during development is mystery around build errors. Nothing is more frustrating than not understanding what is wrong.

As you work on your code your compiler errors and warnings will show in the browser. DO NOT RELOAD THE PAGE! If you reload the page you’ll lose the warning or error, and that makes it harder to figure out what is wrong!

Instead, edit your code and re-save.

If you are having problems and you’ve lost your way, it is sometimes useful to ask figwheel to clean and recompile everything:

cljs.user=> (reset-autobuild)

will typically get you back on track.

Sometimes stuff just fails for reasons we fail to understand. There are times when you may want to completely kill your REPL, clean the project with lein clean, and start again. Make sure all of the generated Javascript is removed when you clean, or things might not clear up.

It is also true that problems in your project configuration may cause problems that are very difficult to understand. If this happens to you (especially if you’ve never run a project with the current project setup) then it is good to look at things like dependency problems with lein deps :tree and fix those.

In general, if you see a conflict on versions it will work to place the newest version of the conflicted dependency into your own dependency list. This can cause problems as well, but is less likely to fail than using an older version of a library that doesn’t have some needed feature of bug fix.

Untangled uses Om’s defui to build React components. This macro emits React components that work as 100% raw React components (i.e. once you compile them to Javascript they could be used from other native React code).

Om also supplies factory functions for generating all standard HTML5 DOM elements in React in the om.dom namespace.

The basic code to build a simple component has the following form:

(defui ComponentName
  Object
  ; object lifecycle and render methods
  (render [this]
     (dom/div #js {:className "a"}
        (dom/p nil "Hello"))))

for our purposes we won’t be saying much about the React lifecycle methods, though they can be added. The basic intention of this macro’s syntax is to declare a component, and then extend various interfaces (in the above case, Object (extend the basic javascript object to have a render method that takes one parameter: this).

Technically, you can add whatever other native methods you might want to this object:

(defui ComponentName
  Object
  (my-method [this]
    (js/console.log "Hi!"))
  (render [this]
    (.my-method this) ; call my-method on this
    (dom/div #js {:className "a"}
       (dom/p nil "Hello"))))

You can convince yourself that you get a plain javascript object by going to the developer’s console in Chrome:

> new app.basic_ui.Root().my_method();
Hi!

though you do have to understand how the names might get munged (e.g. hyphens become underscores).

The render method can do whatever work you need, but it should return a react element (see React Components, Elements, and Instances).

Luckily, there are factory methods for all of HTML5 in om.dom. These functions generally take a Javascript map as their first argument (for things like classname and event handlers) and any children. There are two ways to generate the Javascript map: with the reader tag #js or with clj→js. Thus the following two are functionally equivalent:

(dom/div #js {:className "a"} "Hi")
(dom/div (clj->js {:className "a"}) "Hi")

However, the former happens in the reader (before compile) and generates more efficient runtime code, but the latter is useful when you’ve computed attributes in regular clj data structures and need to convert it at runtime.

React components receive their data through props and state. In Untangled we generally recommend using props. This ensures that various other features work well. The data passed to a component can be accessed (as a cljs map) by calling om/props on this.

So, let’s define a Person component to display details about a person. We’ll assume that we’re going to pass in name and age as properties:

(defui Person
  Object
  (render [this]
    (let [{:keys [person/name person/age]} (om/props this)]
      (dom/div nil
        (dom/p nil "Name: " name)
        (dom/p nil "Age: " age)))))

Now, in order to use this component we need an element factory. An element factory lets us use the component within our React UI tree. Name confusion can become an issue (Person the component vs. person the factory?) we recommend prefixing the factory with ui-:

(def ui-person (om/factory Person))
(defui Root
  Object
  (render [this]
    (ui-person {:person/name "Joe" :person/age 22})))

If you reload your browser page, you should see the updated UI.

Part of our quick development story is getting hot code reload to update the UI whenever we change the source. At the moment this is broken in your app (you’re having to reload the page to see changes). Actually, hot code reload is working, but the UI refresh isn’t.

There are two steps to make this work.

  1. Make sure the definition of the UI components is marked with :once metadata:

  2. Force React to re-render the entire UI (Om optimizes away refresh when the app state hasn’t changed). The trick here is to change the React key on the root element (which forces React to throw away the prior tree and generate a whole new one). Untangled helps by sending your root component a property named :ui/react-key that only changes on (re)mount and forced refresh.

So, changing your current application to this:

(ns app.basic-ui
  (:require [untangled.client.core :as uc]
            [om.dom :as dom]
            [om.next :as om :refer [defui]]))

(defonce app-1 (atom (uc/new-untangled-client)))

(defui ^:once Person
  Object
  (render [this]
    (let [{:keys [person/name person/age]} (om/props this)]
      (dom/div nil
        (dom/p nil "Name: " name)
        (dom/p nil "Age: " age)))))

(def ui-person (om/factory Person))

(defui ^:once Root
  Object
  (render [this]
    (let [{:keys [ui/react-key]} (om/props this)]
      (dom/div #js {:key react-key}
        (ui-person {:person/name "Joe" :person/age 22})))))

and reloading your page (just once more, to clear out the old stuff) should now cause changes you make to the code to appear in the UI without having to reload the page. Try editing the UI of Person and save.

You should already be getting the picture that your UI is going to be a tree composed from a root element. The way data is passed (via props) should also be giving you the picture that supplying data to your UI (through root) means you need to supply an equivalently structured tree of data. This is true of basic React, and since we’ve only seen basic React stuff at this point, it is a true statement in general. However, just to drive the point home let’s make a slightly more complex UI and see it in detail:

Replace your basic_ui.cljs content with this:

(ns app.basic-ui
  (:require [untangled.client.core :as uc]
            [om.dom :as dom]
            [om.next :as om :refer [defui]]))

(defonce app-1 (atom (uc/new-untangled-client)))

(defui ^:once Person
  Object
  (render [this]
    (let [{:keys [person/name person/age]} (om/props this)]
      (dom/li nil
        (dom/h5 nil name (str "(age: " age ")"))))))

(def ui-person (om/factory Person {:keyfn :person/name}))

(defui ^:once PersonList
  Object
  (render [this]
    (let [{:keys [person-list/label person-list/people]} (om/props this)]
      (dom/div nil
        (dom/h4 nil label)
        (dom/ul nil
          (map ui-person people))))))

(def ui-person-list (om/factory PersonList))

(defui ^:once Root
  Object
  (render [this]
    (let [{:keys [ui/react-key]} (om/props this)
          ui-data {:friends {:person-list/label "Friends" :person-list/people
                                                [{:person/name "Sally" :person/age 32}
                                                 {:person/name "Joe" :person/age 22}]}
                   :enemies {:person-list/label "Enemies" :person-list/people
                                                [{:person/name "Fred" :person/age 11}
                                                 {:person/name "Bobby" :person/age 55}]}}]
      (dom/div #js {:key react-key}
        (ui-person-list (:friends ui-data))
        (ui-person-list (:enemies ui-data))))))

So that the UI graph looks like this:

      +--------+
      |  Root  |
      ++-----+-+
       |     |
 +-----+--+ ++-------+
 |  List  | |  List  |
 +---+----+ +----+---+
     |           |
 +---+----+ +----+---+
 | Person | | Person |
 |--------| |--------|
 | Person | | Person |
 +--------+ +--------+

and the data graph matches the same structure, with map keys acting as the graph "edges":

{ LIST-1-KEY { PEOPLE-KEY [PERSON PERSON]
  LIST-2-KEY { PEOPLE-KEY [PERSON PERSON] }
      +--------+
      |  Root  |
      ++-----+-+
enemies|     |friends
 +-----+--+ ++-------+
 |  List  | |  List  |
 +---+----+ +----+---+
     |people     |people
 +---+----+ +----+---+
 | Person | | Person | 0
 |--------| |--------|
 | Person | | Person | 1
 +--------+ +--------+

Obviously it isn’t going to be desirable to hand-manage such a hairy beast in this manner for anything but the most trivial application. At best it does give us a persistent data structure that represents the current "view" of the application (which has many benefits), but at worst it requires us to "think globally" about our application. We want local reasoning. We also want to be able to easily re-compose our UI as needed, and a static data graph like this would have to be updated every time we made a change! Almost equally as bad: if two different parts of our UI want to show the same data, then we’d have to find and update a bunch of copies spread all over the data tree.

So, how do we solve this?

This is certainly a possibility; however, it leads to other complications. What is the data model? How do you interact with remotes to fill your data needs? Om Next has a very nice cohesive story for these questions, while systems like Re-frame end up with complications like event handler middleware, coeffect accretion, and signal graphs…​not to mention that the sideband solution says nothing definitive about server interactions with said data model.

In Untangled, there is a way to construct the initial tree of data in a way that allows for local reasoning: co-locate the initial desired part of the tree with the component that uses it. This allows you to compose the state tree in exactly the same way as the UI tree.

Untangled defines a protocol InitialAppState with a single method named initial-state. The defui macro will allow us to add that implementation to the generated component class by adding static in front of the protocol name we want to implement.

It looks like this:

(defui ^:once Person
  static uc/InitialAppState
  (initial-state [comp-class {:keys [name age] :as params}] {:person/name name :person/age age})
  Object
  (render [this]
    (let [{:keys [person/name person/age]} (om/props this)]
      (dom/li nil
        (dom/h5 nil name (str "(age: " age ")"))))))

(def ui-person (om/factory Person {:keyfn :person/name}))

(defui ^:once PersonList
  static uc/InitialAppState
  (initial-state [comp-class {:keys [label]}]
    {:person-list/label  label
     :person-list/people (if (= label "Friends")
                           [(uc/get-initial-state Person {:name "Sally" :age 32})
                            (uc/get-initial-state Person {:name "Joe" :age 22})]
                           [(uc/get-initial-state Person {:name "Fred" :age 11})
                            (uc/get-initial-state Person {:name "Bobby" :age 55})])})
  Object
  (render [this]
    (let [{:keys [person-list/label person-list/people]} (om/props this)]
      (dom/div nil
        (dom/h4 nil label)
        (dom/ul nil
          (map ui-person people))))))

(def ui-person-list (om/factory PersonList))

(defui ^:once Root
  static
  uc/InitialAppState
  (initial-state [c params] {:friends (uc/get-initial-state PersonList {:label "Friends"})
                             :enemies (uc/get-initial-state PersonList {:label "Enemies"})})
  Object
  (render [this]
    (let [{:keys [ui/react-key]} (om/props this)
          {:keys [friends enemies]} (uc/get-initial-state Root {})]
      (dom/div #js {:key react-key}
        (ui-person-list friends)
        (ui-person-list enemies)))))

Now this is just for demonstration purposes. Data like this would almost certainly come from a server, but it serves to illustrate that we can localize the initial data needs of a component to the component, and then compose that into the parent in an abstract way (by calling get-initial-state on that child).

There are several benefits of this so far:

  1. It generates the exact tree of data needed to feed the UI

  2. It restores local reasoning (and easy refactoring). Moving a component just means local reasoning about the component being moved and the component it is being moved from/to.

In fact, at the figwheel REPL you can see the tree by running:

dev:cljs.user=> (untangled.client.core/get-initial-state app.basic-ui/Root {})
{:friends
 {:person-list/label "Friends",
  :person-list/people
  [{:person/name "Sally", :person/age 32}
   {:person/name "Joe", :person/age 22}]},
 :enemies
 {:person-list/label "Enemies",
  :person-list/people
  [{:person/name "Fred", :person/age 11}
   {:person/name "Bobby", :person/age 55}]}}
Note
Technically, in cljs you can call untangled.client.core/initial-state directly, but this doesn’t work right when doing server-side rendering, so it is good to get in the habit of calling static protocol methods with the helper function.

Behind the scenes Untangled has detected this initial state and actually automatically used it to initialize your application state, but at the moment we’re accessing it directly, but you can check out the application’s current state (which is held in an atom) with:

dev:cljs.user=> @(om.next/app-state (get @app.basic-ui/app-1 :reconciler)

Let’s see how we program our UI to access the data in the application state!

Om Next unifies the data access story using a co-located query on each component. This sets up data access for both the client and server, and also continues our story of local reasoning and composition.

Queries go on a component in the same way as initial state: as static implementations of a protocol.

The query notation is relatively light, and we’ll just concentrate on two bits of query syntax: props and joins.

Queries form a tree just like the UI and data. Obtaining a value at the current node in the tree traversal is done using the keyword for that value. Walking down the graph (a join) is represented as a map with a single entry whose key is the keyword for that nested bit of state.

So, a data tree like this:

{:friends
 {:person-list/label "Friends",
  :person-list/people
  [{:person/name "Sally", :person/age 32}
   {:person/name "Joe", :person/age 22}]},
 :enemies
 {:person-list/label "Enemies",
  :person-list/people
  [{:person/name "Fred", :person/age 11}
   {:person/name "Bobby", :person/age 55}]}}

would have a query that looks like this:

[{:friends
    [ :person-list/label
      {:person-list/people
         [:person/name :person/age]}]}]

This query reads "At the root you’ll find :friends, which joins to a nested entity that has a label and people, which in turn has nested properties name and age.

  • A vector always means "get this stuff at the current node"

  • :friends is a key in a map, so at the root of the application state the query engine would expect to find that key, and would expect the value to be nested state (because maps mean joins on the tree)

  • The value in the :friends join must be a vector, because we have to indicate what we want out of the nested data.

Joins are automatically to-one if the data found in the state is a map, and to-many if the data found is a vector.

The namespacing of keywords in your data (and therefore your query) is highly encouraged, as it makes it clear to the reader what kind of entity you’re working against (it also ensures that over-rendering doesn’t happen on refreshes later).

You can try this query stuff out in your REPL. Let’s say you just want the friends list label. The Om function db→tree can take an application database (which we can generate from initial state) and run a query against it:

dev:cljs.user=>  (om.next/db->tree [{:friends [:person-list/label]}] (untangled.client.core/get-initial-state app.basic-ui/Root {}) {})
{:friends {:person-list/label "Friends"}}

So, we want our queries to have the same nice local-reasoning as our initial data tree. The get-query function works just like the get-initial-state function, and can pull the query from a component. In this case, you should not ever call query directly. The get-query function augments the subqueries with metadata that is important at a later stage.

So, the Person component queries for just the properties it needs:

(defui ^:once Person
  static om/IQuery
  (query [this] [:person/name :person/age])
  static uc/InitialAppState
  (initial-state [comp-class {:keys [name age] :as params}] {:person/name name :person/age age})
  Object
  (render [this]
    (let [{:keys [person/name person/age]} (om/props this)]
      (dom/li nil
        (dom/h5 nil name (str "(age: " age ")"))))))

Notice that the entire rest of the component did not change.

Next up the chain, we compose the Person query into PersonList:

(defui ^:once PersonList
  static om/IQuery
  (query [this] [:person-list/label {:person-list/people (om/get-query Person)}])
  static uc/InitialAppState
  (initial-state [comp-class {:keys [label]}]
    {:person-list/label  label
     :person-list/people (if (= label "Friends")
                           [(uc/get-initial-state Person {:name "Sally" :age 32})
                            (uc/get-initial-state Person {:name "Joe" :age 22})]
                           [(uc/get-initial-state Person {:name "Fred" :age 11})
                            (uc/get-initial-state Person {:name "Bobby" :age 55})])})
  Object
  (render [this]
    (let [{:keys [person-list/label person-list/people]} (om/props this)]
      (dom/div nil
        (dom/h4 nil label)
        (dom/ul nil
          (map ui-person people))))))

again, nothing else changes.

Finally, we compose to Root:

(defui ^:once Root
  static om/IQuery
  (query [this] [:ui/react-key  ; IMPORTANT: have to ask for react-key from the database
                 {:friends (om/get-query PersonList)}
                 {:enemies (om/get-query PersonList)}])
  static
  uc/InitialAppState
  (initial-state [c params] {:friends (uc/get-initial-state PersonList {:label "Friends"})
                             :enemies (uc/get-initial-state PersonList {:label "Enemies"})})
  Object
  (render [this]
    ; NOTE: the data now comes in through props!!!
    (let [{:keys [ui/react-key friends enemies]} (om/props this)]
      (dom/div #js {:key react-key}
        (ui-person-list friends)
        (ui-person-list enemies)))))

and now the magic happens! Notice that the render method of root will now receive the entire query result in props (our prior example was generating the data from initial-state itself), and it will pick the bits out it knows about (:friends and :enemies) and pass those to the children associated with rendering them.

Notice that everything you think about when looking at any one of those components is the data it needs to render itself, or (in the abstract) its direct children. Re-arranging the UI is similarly done in a way the preserves this reasoning.

Also, you now have application state that can evolve (the query is running against the active application database stored in an atom)!

Important
You should always think of the query as "running from root". You’ll notice that Root still expects to receive the entire data tree for the UI (even though it doesn’t have to know much about what is in it, other than the names of direct children), and it still picks out those sub-trees of data and passes them on. In this way an arbitrary component in the UI tree is not querying for it’s data directly in a side-band sort of way, but is instead being composed in from parent to parent all the way to the root. Later, we’ll learn how Om can optimize this and pull the data from the database for a specific component, but the reasoning will remain the same.

The queries on component describe what data the component wants from the database; however, you’re not allowed to put code in the database, and sometimes a parent might compute something it needs to pass to a child.

Om can optimize away the refresh of components if their data has not changed, meaning that it can use a component’s query to directly re-supply its render method for refresh. Doing so skips the rendering call from the parent, and would lead to losing these "extra" bits of data passed from the parent.

Let’s say we want to render a delete button on our individual people in our UI. This button will mean "remove the person from this list"…​but the person itself has no idea which list it is in. Thus, the parent will need to pass in a function that the child can call to affect the delete properly:

(defui ^:once Person
  static om/IQuery
  (query [this] [:person/name :person/age])
  static uc/InitialAppState
  (initial-state [comp-class {:keys [name age] :as params}] {:person/name name :person/age age})
  Object
  (render [this]
    (let [{:keys [person/name person/age onDelete]} (om/props this)]  ; (3)
      (dom/li nil
        (dom/h5 nil name (str "(age: " age ")") (dom/button #js {:onClick #(onDelete name)} "X")))))) ; (4)

(def ui-person (om/factory Person {:keyfn :person/name}))

(defui ^:once PersonList
  static om/IQuery
  (query [this] [:person-list/label {:person-list/people (om/get-query Person)}])
  static uc/InitialAppState
  (initial-state [comp-class {:keys [label]}]
    {:person-list/label  label
     :person-list/people (if (= label "Friends")
                           [(uc/get-initial-state Person {:name "Sally" :age 32})
                            (uc/get-initial-state Person {:name "Joe" :age 22})]
                           [(uc/get-initial-state Person {:name "Fred" :age 11})
                            (uc/get-initial-state Person {:name "Bobby" :age 55})])})
  Object
  (render [this]
    (let [{:keys [person-list/label person-list/people]} (om/props this)
          delete-person (fn [name] (js/console.log label "asked to delete" name))] ; (1)
      (dom/div nil
        (dom/h4 nil label)
        (dom/ul nil
          (map (fn [person] (ui-person (assoc person :onDelete delete-person))) people)))))) ; (2)
  1. A function acting in as a stand-in for our real delete

  2. Adding the callback into the props (WRONG)

  3. Pulling the onDelete from the passed props (WRONG)

  4. Invoking the callback when delete is pressed.

This method of passing a callback will work, but not consistently. The problem is that Om can optimize away a re-render of a parent when it can figure out how to pull just the data of the child on a refresh, and in that case the callback will get lost because only the database data will get supplied to the child! Your delete button will work on the initial render (from root), but may stop working at a later time after a UI refresh.

There is a special helper function that can record the computed data like callbacks onto the child that receives them such that an optimized refresh will still know them.

The change is so small it is easy to miss:

(defui ^:once Person
  ...
  Object
  (render [this]
    (let [{:keys [person/name person/age]} (om/props this)
          onDelete (om/get-computed this :onDelete)] ; (2)
      (dom/li nil
        (dom/h5 nil name (str "(age: " age ")") (dom/button #js {:onClick #(onDelete name)} "X"))))))

(def ui-person (om/factory Person {:keyfn :person/name}))

(defui ^:once PersonList
  ...
  Object
  (render [this]
    (let [{:keys [person-list/label person-list/people]} (om/props this)
          delete-person (fn [name] (js/console.log label "asked to delete" name))]
      (dom/div nil
        (dom/h4 nil label)
        (dom/ul nil
          (map (fn [person] (ui-person (om/computed person {:onDelete delete-person}))) people)))))) ; (1)
  1. The om/computed function is used to add the computed data to the props being passed.

  2. The child pulls the computed data via om/get-computed.

Now you can be sure that your callbacks (or other parent-computed data) won’t be lost to render optimizations.

Now the real fun begins: Making things dynamic.

In general you don’t have to think about how the UI updates, because most changes are run within the context that needs refreshed. But for general knowledge UI Refresh is triggered in two ways:

  • Running a data modification transaction on a component (which will re-render the subtree of that component), and refresh only the DOM for those bits that had actual changes.

  • Telling Om that some specific data changed (e.g. :person/name).

The former is most common, but the latter is often needed when a change executed in one part of the application modifies data that some UI component elsewhere in the tree needs to respond to.

So, if we run the code that affects changes from the component that will need to refresh (a very common case) we’re covered. If a child needs to make a change that will affect a parent (as in our earlier example), then the modification should run from the parent via a callback so that refresh will not require further interaction.

Every change to the application database must go through a transaction processing system. This has two goals:

  • Abstract the operation (like a function)

  • Treat the operation like data (which allows us to generalize to the remote interactions)

The operations are written as quoted data structures. Specifically as a vector of mutation invocations. The entire transaction is just data. It is not something run in the UI, but instead passed into the underlying system for processing.

You essentially just "make up" names for the operations you’d like to do to your database, just like function names.

(om/transact! this `[(ops/delete-person {:list-name "Friends" :person "Fred"})])

is asking the underlying system to run the mutation ops/delete-person (where ops can be an alias established in the ns). Of course, you’ll typically use unquote to embed data from local variables:

(om/transact! this `[(ops/delete-person {:list-name ~name :person ~person})])

When a transaction runs in Untangled, it passes things off to a multimethod. This multi-method is described in more detail in the Om documentation and the Untangled Developer’s Guide, but Untangled provides a macro that makes building (and using) them easier: defmutation.

Let’s create a new namespace called app.operations in src/app/operations.cljs

A mutation looks a bit like a method. It can have a docstring, and the argument list will always receive a single argument (params) that will be a map (which then allows destructuring).

The body of the mutation looks like the layout of a protocol implementation, with one or more methods. The one we’re interested in at the moment is action, which is what to do locally. The action method will be passed the application database’s app-state atom, and it should change the data in that atom to reflect the new "state of the world" indicated by the mutation.

For example, delete-person must find the list of people on the list in question, and filter out the one that we’re deleting:

(ns app.operations
  (:require [untangled.client.mutations :as m :refer [defmutation]]))

(defmutation delete-person
  "Mutation: Delete the person with name from the list with list-name"
  [{:keys [list-name name]}] ; (1)
  (action [{:keys [state]}] ; (2)
    (let [path     (if (= "Friends" list-name)
                     [:friends :person-list/people]
                     [:enemies :person-list/people])
          old-list (get-in @state path)
          new-list (vec (filter #(not= (:person/name %) name) old-list))]
      (swap! state assoc-in path new-list))))
  1. The argument list for the mutation itself

  2. The thing to do, which receives the app-state atom as an argument.

Then all that remains is to change basic-ui in the following ways:

  1. Add a require and alias for app.operations to the ns

  2. Change the callback to run the transaction

(ns app.basic-ui
  (:require [untangled.client.core :as uc]
            [om.dom :as dom]
            ; ADD THIS:
            [app.operations :as ops] ; (1)
            [om.next :as om :refer [defui]]))

...

(defui ^:once PersonList
  ...
  Object
  (render [this]
    (let [{:keys [person-list/label person-list/people]} (om/props this)
          delete-person (fn [name]
                          (js/console.log label "asked to delete" name)
                          ; AND THIS
                          (om/transact! this `[(ops/delete-person {:list-name ~label :name ~name})]))] ; (2)
  1. The require ensures that the mutations are loaded, and also gives us an alias to the namespace of the mutation’s symbol.

  2. Running the transaction in the callback.

Note that our mutation’s symbol is actually app.operations/delete-person, but the syntax quoting will fix it. Also realize that the mutation is not running in the UI, it is instead being handled "behind the scenes". This allows a snapshot of the state history to be kept, and also a more seamless integration to full-stack operation over a network to a server (in fact, the UI code here is already full-stack capable without any changes!).

This is where the power starts to show: all of the minutiae above is leading us to some grand unifications when it comes to writing full-stack applications.

But first, we should address a problem that many of you may have already noticed: The mutation code is tied to the shape of the UI tree!!!

This breaks our lovely model in several ways:

  1. We can’t refactor our UI without also rewriting the mutations (since the data tree would change shape)

  2. We can’t locally reason about any data. Our mutations have to understand things globally!

  3. Our mutations could get rather large and ugly as our UI gets big

  4. If a fact appears in more than one place in the UI and data tree, then we’ll have to update all of them in order for things to be correct. Data duplication is never your friend.

Fortunately, we have a very good solution to this problem, and it is one that has been around for decades: database normalization!

Here’s what we’re going to do:

Each UI component represents some conceptual entity with data (assuming it has state and a query). In a fully normalized database, each such concept would have its own table, and related things would refer to it through some kind of foreign key. In SQL land this looks like:

                                 +-------------------------------------+
                                 |                                     |
PersonList                       |     Person                          |
+---------------------------+    |     +----------------------------+  |
| ID  | Label               |    |     |ID | Name         | List ID |  |
|---------------------------|    |     |----------------------------|  |
| 1   | Friends             |<---+     |1  | Joe          |    1    |--+
+---------------------------+          |----------------------------|  |
                                       |2  | Sally        |    1    |--+
                                       +----------------------------+

In a graph database (like Datomic) a reference can have a to-many arity, so the direction can be more natural:

PersonList                             Person
+---------------------------+          +------------------+
| ID  | Label   | People    |          |ID | Name         |
|---------------------------|          |------------------|
| 1   | Friends | #{1,2}    |----+---->|1  | Joe          |
+---------------------------+    |     |------------------|
                                 +---->|2  | Sally        |
                                       +------------------+

Since we’re storing things in a map, we can represent "tables" as an entry in the map where the key is the table name, and the value is a map from ID to entity value. So, the last diagram could be represented as:

{ :PersonList { 1  { :label "Friends"
                     :people #{1, 2} }}
  :Person { 1 {:id 1 :name "Joe" }
            2 {:id 2 :name "Sally"}}}

This is close, but not quite good enough. The set in :person-list/people is a problem. There is no schema, so there is no way to know what kind of thing "1" and "2" are!

The solution is rather easy: make a foreign reference include the name of the table to look in (to-many relations are stored in a vector as well, which results in the doubly-nested vector):

{ :PersonList { 1  { :label "Friends"
                     :people [ [:Person 1] [:Person 2] ] }}
  :Person { 1 {:id 1 :name "Joe" }
            2 {:id 2 :name "Sally"}}}

A foreign key as a vector pair of [TABLE ID] is known as an Ident.

So, now that we have the concept and implementation, let’s talk about conventions:

  1. Properties are usually namespaced (as shown in earlier examples)

  2. Table names are usually namespaced with the entity type, and given a name that indicates how it is indexed. For example: :person/by-id, :person-list/by-name, etc.

Fortunately, you don’t have to hand-normalize your data. The components have almost everything they need to do it for you, other than the actual value of the Ident. So, we’ll add one more (static) method to your components (and we’ll add IDs to the data at this point, for easier implementation):

...
(defui ^:once Person
  static om/Ident ; (1)
  (ident [this props] [:person/by-id (:db/id props)])
  static om/IQuery
  (query [this] [:db/id :person/name :person/age]) ; (2)
  static uc/InitialAppState
  (initial-state [comp-class {:keys [id name age] :as params}] {:db/id id :person/name name :person/age age}) ; (3)
  Object
  (render [this]
    (let [{:keys [db/id person/name person/age]} (om/props this)
          onDelete (om/get-computed this :onDelete)]
      (dom/li nil
        (dom/h5 nil name (str "(age: " age ")") (dom/button #js {:onClick #(onDelete id)} "X")))))) ; (4)

(def ui-person (om/factory Person {:keyfn :person/name}))

(defui ^:once PersonList
  static om/Ident
  (ident [this props] [:person-list/by-id (:db/id props)]) ; (5)
  static om/IQuery
  (query [this] [:db/id :person-list/label {:person-list/people (om/get-query Person)}]) ; (5)
  static uc/InitialAppState
  (initial-state [comp-class {:keys [id label]}]
    {:db/id              id ; (5)
     :person-list/label  label
     :person-list/people (if (= id :friends)
                           [(uc/get-initial-state Person {:id 1 :name "Sally" :age 32}) ; (3)
                            (uc/get-initial-state Person {:id 2 :name "Joe" :age 22})]
                           [(uc/get-initial-state Person {:id 3 :name "Fred" :age 11})
                            (uc/get-initial-state Person {:id 4 :name "Bobby" :age 55})])})
  Object
  (render [this]
    (let [{:keys [db/id person-list/label person-list/people]} (om/props this)
          delete-person (fn [person-id]
                          (js/console.log label "asked to delete" name)
                          (om/transact! this `[(ops/delete-person {:list-id ~id :person-id ~person-id})]))] (4)
      (dom/div nil
        (dom/h4 nil label)
        (dom/ul nil
          (map (fn [person] (ui-person (om/computed person {:onDelete delete-person}))) people))))))

(def ui-person-list (om/factory PersonList))

(defui ^:once Root
  static om/IQuery
  (query [this] [:ui/react-key
                 {:friends (om/get-query PersonList)}
                 {:enemies (om/get-query PersonList)}])
  static
  uc/InitialAppState
  (initial-state [c params] {:friends (uc/get-initial-state PersonList {:id :friends :label "Friends"}) ; (5)
                             :enemies (uc/get-initial-state PersonList {:id :enemies :label "Enemies"})})
  Object
  (render [this]
    ; NOTE: the data now comes in through props!!!
    (let [{:keys [ui/react-key friends enemies]} (om/props this)]
      (dom/div #js {:key react-key}
        (ui-person-list friends)
        (ui-person-list enemies)))))
  1. Adding an ident function allows Untangled to know how to build a FK reference to a person (given its props)

  2. We will be using IDs now, so we need to add :db/id to the query. This is just a convention for the ID attribute

  3. The state of the entity will also need the ID

  4. The callback can now delete people by their ID, which is more reliable.

  5. The list will have an ID, and an Ident as well

If you reload the web page (needed to reinitialize the database state), then you can look at the newly normalized database at the REPL:

dev:cljs.user=> @(om.next/app-state (-> app.basic-ui/app-1 deref :reconciler))
{:friends [:person-list/by-id :friends], ; The TOP-LEVEL data keys, pointing to table entries now
 :enemies [:person-list/by-id :enemies],
 :ui/locale "en-US",
 :person/by-id ; The PERSON table
 {1 {:db/id 1, :person/name "Sally", :person/age 32},
  2 {:db/id 2, :person/name "Joe", :person/age 22},
  3 {:db/id 3, :person/name "Fred", :person/age 11},
  4 {:db/id 4, :person/name "Bobby", :person/age 55}},
 :person-list/by-id ; The PERSON LIST Table
 {:friends
  {:db/id :friends,
   :person-list/label "Friends",
   :person-list/people [[:person/by-id 1] [:person/by-id 2]]}, ; FKs to the PERSON table
  :enemies
  {:db/id :enemies,
   :person-list/label "Enemies",
   :person-list/people [[:person/by-id 3] [:person/by-id 4]]}}}

Note that db→tree understands (prefers) this normalized form, and can still convert it (via a query) to the proper data tree (note the repetition of the app state is necessary now). At the REPL, try this:

dev:cljs.user=> (def current-db @(om.next/app-state (-> app.basic-ui/app-1 deref :reconciler)))
#'cljs.user/current-db
dev:cljs.user=> (om.next/db->tree (om.next/get-query app.basic-ui/Root) current-db current-db)
{:friends
 {:db/id :friends,
  :person-list/label "Friends",
  :person-list/people
  [{:db/id 1, :person/name "Sally", :person/age 32}
   {:db/id 2, :person/name "Joe", :person/age 22}]},
 :enemies
 {:db/id :enemies,
  :person-list/label "Enemies",
  :person-list/people
  [{:db/id 3, :person/name "Fred", :person/age 11}
   {:db/id 4, :person/name "Bobby", :person/age 55}]}}

We have now made it possible to fix the problems with our mutation. Now, instead of removing a person from a tree, we can remove a FK from a TABLE entry!

This is not only much easier to code, but it is complete independent of the shape of the UI tree:

(ns app.operations
  (:require [untangled.client.mutations :as m :refer [defmutation]]))

(defmutation delete-person
  "Mutation: Delete the person with name from the list with list-name"
  [{:keys [list-id person-id]}]
  (action [{:keys [state]}]
    (let [ident-to-remove [:person/by-id person-id] ; (1)
          strip-fk (fn [old-fks]
                     (vec (filter #(not= ident-to-remove %) old-fks)))] ; (2)
      (swap! state update-in [:person-list/by-id list-id :person-list/people] strip-fk)))) ; (3)
  1. References are always idents, meaning we know the value to remove from the FK list

  2. By defining a function that can filter the ident from (1), we can use update-in on the person list table’s people.

  3. This is a very typical operation in a mutation: swap on the application state, and update a particular thing in a table (in this case the people to-many ref in a specific person list).

If we were to now wrap the person list in any amount of addition UI (e.g. a nav bar, sub-pane, modal dialog, etc) this mutation will still work perfectly, since the list itself will only have one place it ever lives in the database.

It is good to know how an arbitrary tree of data (the one in InitialAppState) can be converted to the normalized form. Understanding how this is accomplished can help you avoid some mistakes later.

When you compose your query (via om/get-query), the get-query function adds metadata to the query fragment that names which component that query fragment came from.

For example, try this at the REPL:

dev:cljs.user=> (meta (om.next/get-query app.basic-ui/PersonList))
{:component app.basic-ui/PersonList}

The get-query function adds the component itself to the metadata for that query fragment. We already know that we can call the static methods on a component (in this case we’re interested in ident).

So, Om includes a function called tree→db that can simultaneously walk a data tree (in this case initial-state) and a component-annotated query. When it reaches a data node whose query metadata names a component with an Ident, it places that data into the approprite table (by calling your ident function on it to obtain the table/id), and replaces the data in the tree with its FK ident.

Once you realize that the query and the ident work together to do normalization, you can more easily figure out what mistakes you might make that could cause auto-normalization to fail (e.g. stealing a query from one component and placing it on another, writing the query of a sub-component by-hand instead of pulling it with get-query, etc.).

  • An Initial app state sets up a tree of data for startup to match the UI tree

  • Component query and ident are used to normalize this initial data into the database

  • The query is used to pull data from the normalized db into the props of the active Root UI

  • Transactions invoke abstract mutations

    • Mutations modify the (normalized) db

    • The transaction’s subtree of components re-renders

Believe it or not, there’s not much to add/change on the client to get it talking to a server, and there is also a relatively painless way to get a server up and running.

There are two server namespaces in Untangled: untangled.server and untangled.easy-server. The former has composable bits for making a server that has a lot of your own extensions, while the latter is a pre-baked server that covers many of the common bases and is less work to get started with. You can always get started with the easy one, and upgrade to a more enhanced one later.

To add a server to our project just requires a few small additions:

  • The server itself

  • Some tweaks to allow us to rapidly restart the server with code refresh for quick development.

In dev/user.clj, we’ll add the following for development use:

(ns user
  (:require
    [figwheel-sidecar.system :as fig]
    app.server
    [clojure.tools.namespace.repl :as tools-ns :refer [set-refresh-dirs]]
    [com.stuartsierra.component :as component]))

; start-figwheel as before...


; Set what clojure code paths are refreshed.
; The resources directory is on the classpath, and the cljs compiler copies code there, so we have to be careful
; that these extras don't get re-scanned when refreshing the server.
(set-refresh-dirs "src/dev" "src/main")

(def system (atom nil))
(declare reset)

(defn refresh
  "Refresh the live code. Use this if the server is stopped. Otherwise, use `reset`."
  [& args]
  (if @system
    (println "The server is running. Use `reset` instead.")
    (apply tools-ns/refresh args)))

(defn stop
  "Stop the currently running server."
  []
  (when @system
    (swap! system component/stop))
  (reset! system nil))

(defn go
  "Start the server. Optionally supply a path to your desired config. Relative paths will scan classpath. Absolute
  paths will come from the filesystem. The default is config/dev.edn."
  ([] (go :dev))
  ([path]
   (if @system
     (println "The server is already running. Use reset to stop, refresh, and start.")
     (letfn [(start []
              (swap! system component/start))
            (init [path]
              (when-let [new-system (app.server/make-system "config/dev.edn")]
                (reset! system new-system)))]
      (init path)
      (start)))))

(defn reset
  "Stop the server, refresh the code, and restart the server."
  []
  (stop)
  (refresh :after 'user/go))

These functions will be used at the clj REPL for managing your running server.

The server itself requires very little code. In src/main/app/server.clj:

(ns app.server
  (:require [untangled.easy-server :as easy]
            [untangled.server :as server ]
            [taoensso.timbre :as timbre]))

(defn make-system [config-path]
  (easy/make-untangled-server
    :config-path config-path
    :parser (server/untangled-parser)))

The make-untangled-server function needs to know where to find the server config file, and what to use to process the incoming client requests (the parser). Untangled comes with a parser that you can use to get going. You may also supply your own Om parser here.

Finally, you need two configuration files. Place these in resources/config:

defaults.edn:

{:port 4050}

dev.edn:

{}

The first file is always looked for by the server, and should contain all of the default settings you think you want independent of where the server is started.

The server (for safety reasons in production) will not start if there isn’t a user-specified file containing potential overrides.

Basically, it will deep-merge the two and have the latter override things in the former. This makes mistakes in production harder to make. If you read the source of the go function in the user.clj file you’ll see that we supply this development config file as an argument. In production systems you’ll typically want this file to be on the filesystem when an admin can tweak it.

If you now start a local Clojure REPL (with no special options), you should be in the user namespace to start.

user=> (go)

should start the server. The console should tell you the URL, and if you browse there you should see your index.html file.

When you add/change code on the server you will want to see those changes in the live server without having to restart your REPL.

user=> (reset)

will do this.

If there are compiler errors, then the user namespace might not reload properly. In that case, you should be able to recover using:

user=> (tools-ns/refresh)
user=> (go)
Warning
Don’t call refresh while the server is running. It will refresh the code, but it will lose the reference to the running server, meaning you won’t be able to stop it and free up the network port. If you do this, you’ll have to restart your REPL.

Figwheel comes with a server that we’ve been using to serve our client. When you want to build a full-stack app you must serve your client from your own server. Thus, if you load your page with the figwheel server (which is still available on an alternate port) you’ll see your app, but the server interactions won’t succeed.

One might ask: "If I don’t use figwheel’s server, do I lose hot code reload on the client?"

The answer is no. When figwheel compiles your application it embeds it’s own websocket code in your application for hot code reload. When you load that compiled code (in any way) it will try to connect to the figwheel websocket.

So your network topology was:

+----------+
| Browser  |                  +-------------------+
|  app     +-----+            |                   |
|          |     |            |  port 3449        |
+----------+     | http load  |  +-------------+  |
                 +----------->|  | Figwheel    |  |
                 |            |  |             |  |
                 +----------->|  |             |  |
                ws hot code   |  +-------------+  |
                              +-------------------+

where both the HTML/CSS/JS resources and the hot code were coming from different connections to the same server.

The networking picture during full-stack development just splits these like this:

                           localhost
                           +-------------------+
                           |                   |
                           |  port 4050        |
              app requests |  +-------------+  |
+----------+     +-------->|  |Your Server  |  |
| Browser  |     |         |  +-------------+  |
|  app     +-----+         |                   |
|          |     |         |  port 3449        |
+----------+     |         |  +-------------+  |
                 +-------->|  | Figwheel    |  |
             ws hot code   |  +-------------+  |
                           |                   |
                           +-------------------+

Untangled’s client will automatically route requests to the /api URI of the source URL that was used to load the page, and Untangled’s server is built to watch for communications at this endpoint.

It is very handy to be able to look at your applications state to see what might be wrong. We’ve been manually dumping application state at the REPL using a rather long expression. Let’s simplify that. In user.cljs (make sure it is the CLJS file!) add:

(defn dump
  [& keys]
  (let [state-map        @(om.next/app-state (-> app-1 deref :reconciler))
        data-of-interest (if (seq keys)
                           (get-in state-map keys)
                           state-map)]
    data-of-interest))

now you should be able to examine the entire app state or a particular key-path with:

dev:cljs.user=> (dump)
dev:cljs.user=> (dump :person/by-id 1)

Now we will start to see more of the payoff of our UI co-located queries and auto-normalization. Our application so far is quite unrealistic: the people we’re showing should be coming from a server-side database, they should not be embedded in the code of the client. Let’s remedy that.

Untangled provides a few mechanisms for loading data, but every possible load scenario can be done using the untangled.client.data-fetch/load function.

It is very important to remember that our application database is completely normalized, so anything we’d want to put in that application state will at most be 3 levels deep (the table name, the ID of the thing in the table, and the field within that thing).

Thus, there really are not very many scenarios!

  • Load something into the root of the application state

  • Load something into a particular field of an existing thing

  • Load some pile of data, and shape it into the database (e.g. load all of the people, and then separate them into a list of friends and enemies).

Let’s try out these different scenarios with our application.

First, let’s correct our application’s initial state so that no people are there:

(defui ^:once PersonList
  ...
  static uc/InitialAppState
  (initial-state [comp-class {:keys [id label]}]
    {:db/id              id
     :person-list/label  label
     :person-list/people []}) ; REMOVE the initial people
  ...

If you now reload your page you should see two empty lists.

When you load something you will use a query from something on your UI (it is rare to load something you don’t want to show). Since those components (should) have a query and ident, the result of a load can be sent from the server as a tree, and the client can auto-normalize that tree just like it did for our initial state!

This case is less common, but it is a simple starting point. It is typically used to obtain something that you’d want to access globally (e.g. the user info about the current session). Let’s assume that our Person component represents the same kind of data as the "logged in" user. Let’s write a load that can ask the server for the "current user" and store that in the root of our database under the key :current-user.

Loads, of course, can be triggered at any time (startup, event, timeout). Loading is just a function call.

For this example, let’s trigger the load just after the application has started.

To do this, we can add an option to our client. In app.basic-ui change app-1:

(ns app.basic-ui
  (:require [untangled.client.core :as uc]
            [om.dom :as dom]
            [app.operations :as ops]
            [om.next :as om :refer [defui]]
            [untangled.client.data-fetch :as df] ; (1)
            [untangled.client.mutations :as m]))

...

(defonce app-1 (atom (uc/new-untangled-client
                       :started-callback (fn [app]
                                           (df/load app :current-user Person))))) ; (2)
  1. Require the data-fetch namespace

  2. Issue the load in the application’s started-callback

Of course hot code reload does not restart the app (if just hot patches the code), so to see this load trigger we must reload the browser page.

If you do that at the moment, you should see an error in the developer console related to the load.

Important
Make sure your application is running from your server (port 4050) and not the figwheel one!

Technically, load is just writing a query for you (in this case [{:current-user (om/get-query Person)}]) and sending it to the server. The server will receive exactly that query as a CLJ data structure.

In vanilla Om Next you would now be tasked with converting the raw CLJ query into a response. You can read more about that in the developer’s guide; however, remember that we’re using Untangled’s built-in request parser. This makes our job much easier.

Create a new namespace in src/main/operations.clj (NOT the cljs file…​that was for the client operations):

(ns app.operations
  (:require
    [untangled.server :as server :refer [defquery-root defquery-entity defmutation]]
    [taoensso.timbre :as timbre]))

(def people-db (atom {1  {:db/id 1 :person/name "Bert" :person/age 55 :person/relation :friend}
                      2  {:db/id 2 :person/name "Sally" :person/age 22 :person/relation :friend}
                      3  {:db/id 3 :person/name "Allie" :person/age 76 :person/relation :enemy}
                      4  {:db/id 4 :person/name "Zoe" :person/age 32 :person/relation :friend}
                      99 {:db/id 99 :person/name "Me" :person/role "admin"}}))

Since we’re on the server and we’re going to be supplying and manipulating people, we’ll just make a single atom-based in-memory database. This could easily be stored in a database of any kind.

To handle the incoming "current user" request, we can use a macro to write the handler:

(defquery-root :current-user
  "Queries for the current user and returns it to the client"
  (value [env params]
    (get @people-db 99)))

This actually augments a multimethod, which means we need to make sure this namespace is loaded by our server.

So, be sure to edit user.clj and add this to the requires:

(ns user
  (:require
    [figwheel-sidecar.system :as fig]
    app.server
    app.operations ; Add this so your operations get loaded into the multimethod request handler
    [clojure.tools.namespace.repl :as tools-ns :refer [set-refresh-dirs]]
    [com.stuartsierra.component :as component]))

...

You should now refresh the server at the SERVER REPL:

user=> (reset)

If you’ve done everything correctly, then reloading your application should successfully load your current user. You can verify this by examining the network data, but it will be even more convincing if you look at your client database:

dev:cljs.user=> (dump)
{:current-user [:person/by-id 99],
 :person/by-id {99 {:db/id 99, :person/name "Me", :person/role "admin"}},
...
}

Notice that the top-level key is a normalized FK reference to the person, which has been placed into the correct database table.

Of course, the question is now "how do I use that in some arbitrary component?" We won’t completely explore that right now, but the answer is easy: The query syntax has a notation for "query something at the root". It looks like this: [ {[:current-user '_] (om/get-query Person)} ]. You should recognize this as a query join, but on something that looks like an ident without an ID (implying there is only one, at root).

We’ll just use it on the Root UI node, where we don’t need to "jump to the top":

(defui ^:once Root
  static om/IQuery
  (query [this] [:ui/react-key
                 :ui/person-id
                 {:current-user (om/get-query Person)} ; (1)
                 {:friends (om/get-query PersonList)}
                 {:enemies (om/get-query PersonList)}])
  static
  uc/InitialAppState
  (initial-state [c params] {:friends (uc/get-initial-state PersonList {:id :friends :label "Friends"})
                             :enemies (uc/get-initial-state PersonList {:id :enemies :label "Enemies"})})
  Object
  (render [this]
    ; NOTE: the data now comes in through props!!!
    (let [{:keys [ui/react-key current-user friends enemies]} (om/props this)] ; (2)
      (dom/div #js {:key react-key}
        (dom/h4 nil (str "Current User: " (:person/name current-user))) ; (3)
        (ui-person-list friends)
        (ui-person-list enemies)))))
  1. Add the current user to the query

  2. Pull of from the props

  3. Show something about it in the UI

Now reload the page to re-execute the load and it should fill in correctly.

The next common scenario is loading something into some other existing entity in your database. Remember that since the database is normalized this will cover all of the other loading cases (except for the one where you want to convert what the server tells you into a different shape (e.g. paginate, sort, etc.)).

Untangled’s load method accomplishes this by loading the data into the root of the database, normalizing it, then (optionally) allowing you to re-target the top-level FK to a different location in the database.

The load looks very much like what we just did, but with one addition:

source

(df/load app :my-friends Person {:target [:person-list/by-id :friends :person-list/people]})

The :target option indicates that once the data is loaded and normalized (which will leave the FK reference at the root as we saw in the last section) this top-level reference will be moved into the key-path provided. Since our database is normalized, this means a 3-tuple (table, id, target field).

Warning
It is important to choose a keyword for this load that won’t stomp on real data in your database’s root. We already have the top-level keys :friends and :enemies as part of our UI graph from root. So, we’re making up :my-friends as the load key. One could also namespace the keyword with something like :server/friends.

Since friend and enemies are the same kind of query, let’s add both into the started callback:

(defonce app-1 (atom (uc/new-untangled-client
                       :started-callback (fn [app]
                                           ...
                                           (df/load app :my-enemies Person {:target [:person-list/by-id :enemies :person-list/people]})
                                           (df/load app :my-friends Person {:target [:person-list/by-id :friends :person-list/people]})))))

The server query processing is what you would expect from the last example (in operations.clj):

(def people-db ...) ; as before

(defn get-people [kind keys]
  (->> @people-db
    vals
    (filter #(= kind (:person/relation %)))
    vec))

(defquery-root :my-friends
  "Queries for friends and returns them to the client"
  (value [{:keys [query]} params]
    (get-people :friend query)))

(defquery-root :my-enemies
  "Queries for enemies and returns them to the client"
  (value [{:keys [query]} params]
    (get-people :enemy query)))

A refresh of the server and reload of the page should now populate your lists from the server!

user=> (reset)

It is somewhat common for a server to return data that isn’t quite what we want in our UI. So far we’ve just been placing the data returned from the server directly in our UI. Untangled’s load mechanism allows a post mutation of the loaded data once it arrives, allowing you to re-shape it into whatever form you might desire.

For example, you may want the people in your lists to be sorted by name. You’ve already seen how to write client mutations that modify the database, and that is really all you need. The client mutation for sorting the people in the friends list could be (in operations.cljs):

(defn sort-friends-by*
  "Sort the idents in the friends person list by the indicated field. Returns the new app-state."
  [state-map field]
  (let [friend-idents  (get-in state-map [:person-list/by-id :friends :person-list/people] [])
        friends        (map (fn [friend-ident] (get-in state-map friend-ident)) friend-idents)
        sorted-friends (sort-by field friends)
        new-idents     (mapv (fn [friend] [:person/by-id (:db/id friend)]) sorted-friends)]
    (assoc-in state-map [:person-list/by-id :friends :person-list/people] new-idents)))

(defmutation sort-friends [no-params]
  (action [{:keys [state]}]
    (swap! state sort-friends-by* :person/name)))

Of course this mutation could be triggered anywhere you could run a transact!, but since we’re interested in morphing just-loaded data, we’ll add it there (in basic-ui):

(df/load app :my-friends Person {:target [:person-list/by-id :friends :person-list/people]
                                :post-mutation `ops/sort-friends})

Notice the syntax quoting. The post mutation has to be the symbol of the mutation. Remember that our require has app.operations aliased to ops, and syntax quoting will expand that for us.

If you reload your UI you should now see the people sorted by name. Hopefully you can see how easy it is to change this sort order to something like "by age". Try it!

Once things are loaded from the server they are immediately growing stale (unless you’re pushing updates with websockets). It is very common to want to re-load a particular thing in your database. Of course, you can trigger a load just like we’ve been doing, but in that case we reloading a whole bunch of things. What if we just wanted to refresh a particular person (e.g. in preparation for editing it).

The load function can be used for that as well. Just replace the keyword with an ident, and you’re there!

Load can take the app or any component’s this as the first argument, so from within the UI we can trigger a load using this:

(df/load this [:person/by-id 3] Person)

Let’s embed that into our UI at the root:

(defui ^:once Root
  ...
  Object
  (render [this]
    (let [{:keys [ui/react-key current-user friends enemies]} (om/props this)]
      (dom/div #js {:key react-key}
        (dom/h4 nil (str "Current User: " (:person/name current-user)))
        ; Add a button:
        (dom/button #js {:onClick (fn [] (df/load this [:person/by-id 3] Person))} "Refresh Person with ID 3")
        ...

The incoming query will have a slightly different form, so there is an alternate macro for making a handler for entity loading. Let’s add this in operations.clj:

(defquery-entity :person/by-id
  "Server query for allowing the client to pull an individual person from the database"
  (value [env id params]
    (timbre/info "Query for person" id)
    ; the update is just so we can see it change in the UI
    (update (get @people-db id) :person/name str " (refreshed)")))

The defquery-entity takes the "table name" as the dispatch key. The value method of the query handler will receive the server environment, the ID of the entity to load, and any parameters passed with the query (see the :params option of load).

In the implementation above we’re augmenting the person’s name with "(refreshed)" so that you can see it happen in the UI.

Remember to (reset) your server to load this code.

Your UI should now have a button, and when you press it you should see one person update!

There is a special case that is somewhat common: you want to trigger a refresh from an event on the item that needs the refresh. The code for that is identical to what we’ve just presented (a load with an ident and component); however, the data-fetch namespace includes a convenience function for it.

So, say we wanted a refresh button on each person. We could leverage df/refresh for that:

(defui ^:once Person
  ... as before
  (render [this]
    (let [{:keys [db/id person/name person/age]} (om/props this)]
      (dom/li nil
        (dom/h5 nil name (str "(age: " age ")")
          (dom/button #js {:onClick #(df/refresh! this)} "Refresh"))))))

This should already work with your server, so once the browser hot code reload has happened this button should just work!

Untangled’s load system covers a number of additional bases that bring the story to completion. There are load markers (so you can show network activity), UI refresh add-ons (when you modify data that isn’t auto-detected, e.g. through a post mutation), server query parameters, and error handling. See the developers guide, doc strings, or source for more details.

Mutations are handled on the server using the server’s defmutation macro (if you’re using Untangled’s request parser).

This has the identical syntax to the client version!

Important
You want to place your mutations in the same namespace on the client and server since the defmutation macros namespace the symbol into the current namespace.

So, this is really why we duplicated the namespace name in Clojure earlier and created an operations.clj file right next to our operations.cljs.

So, we can now add an implementation for our server-side delete-person:

(defmutation delete-person
  "Server Mutation: Handles deleting a person on the server"
  [{:keys [person-id]}]
  (action [{:keys [state]}]
    (timbre/info "Server deleting person" person-id)
    (swap! people-db dissoc person-id)))

Refresh the code on your server with (reset) at the REPL.

Mutations are simply optimistic local updates by default. To make them full-stack, you need to add a method-looking section to your defmutation handler:

(defmutation delete-person
  "Mutation: Delete the person with person-id from the list with list-id"
  [{:keys [list-id person-id]}]
  (action [{:keys [state]}]
    (let [ident-to-remove [:person/by-id person-id]
          strip-fk        (fn [old-fks]
                            (vec (filter #(not= ident-to-remove %) old-fks)))]
      (swap! state update-in [:person-list/by-id list-id :person-list/people] strip-fk)))
  (remote [env] true)) ; This one line is it!!!

The syntax for the addition is:

(remote-name [env] boolean-or-ast)

where remote is the value of the default remote-name. You can have any number of network remotes. The default one talks to the page origin at /api. What is this AST we speak of? It is the abstract syntax tree of the mutation itself (as data). Using a boolean true means "send it just as the client specified". If you wish you can pull the AST from the env, augment it (or completely change it) and return that instead. See the developers guide for more details.

Now that you’ve got the UI in place, try deleting a person. It should disappear from the UI as it did before; however, now if you’re watching the network you’ll see a request to the server. If you server is working right, it will handle the delete.

Try reloading your page from the server. That person should still be missing, indicating that it really was removed from the server.

Working with legacy REST APIs is a simple, though tedious, task. Basically you need to add an additional remote to the Untangled Client that knows how to talk via JSON instead of EDN.

The basic steps are:

  1. Implement UntangledNetwork. See the untangled.client.network namespace for the protocol and built-in implementation.

    1. Your send method will be passed the query/mutations the client wants to do. You must translate them to a REST call and translate the REST response into the desired tree of client data, which you then pass to the ok callback that send is given.

  2. Install your network handler on the client (using the :networking option)

  3. Add the :remote option to your loads, or use your remote name as the remote side of a mutation

For this example we’re going to use the following public REST API endpoint: http://jsonplaceholder.typicode.com/posts which returns a list of posts (try it to make sure it is working).

It should return an array of JSON maps, with strings as keys.

Basically, when you run a transaction (read or write) the raw transaction that is intended to go remote is passed into the send method of a networking protocol. The networking can send that unchanged, or it can choose to modify it in some way. Since REST servers don’t understand our Untangled requests, we have to add a layer at the network to convert one to the other, and back (for the response).

First, let’s talk about the UI code for dealing with these posts, since the UI defines the queries. Here is a very simple UI we can add to our program:

(defui Post ; (1)
  static om/Ident
  (ident [this props] [:posts/by-id (:db/id props)])
  static om/IQuery
  (query [this] [:db/id :post/user-id :post/body :post/title])
  Object
  (render [this]
    (let [{:keys [post/title post/body]} (om/props this)]
      (dom/div nil
        (dom/h4 nil title)
        (dom/p nil body)))))

(def ui-post (om/factory Post {:keyfn :db/id}))

(defui Posts ; (2)
  static uc/InitialAppState
  (initial-state [c params] {:posts []})
  static om/Ident
  (ident [this props] [:post-list/by-id :the-one])
  static om/IQuery
  (query [this] [{:posts (om/get-query Post)}])
  Object
  (render [this]
    (let [{:keys [posts]} (om/props this)]
      (dom/ul nil
        (map ui-post posts)))))

(def ui-posts (om/factory Posts))

(defui ^:once Root
  static om/IQuery
  (query [this] [:ui/react-key
                 :ui/person-id
                 {:current-user (om/get-query Person)}
                 {:blog-posts (om/get-query Posts)} ; (3)
                 {:friends (om/get-query PersonList)}
                 {:enemies (om/get-query PersonList)}])
  static
  uc/InitialAppState
  (initial-state [c params] {:blog-posts (uc/get-initial-state Posts {}) ; (4)
                             :friends    (uc/get-initial-state PersonList {:id :friends :label "Friends"})
                             :enemies    (uc/get-initial-state PersonList {:id :enemies :label "Enemies"})})
  Object
  (render [this]
    ; NOTE: the data now comes in through props!!!
    (let [{:keys [ui/react-key blog-posts current-user friends enemies]} (om/props this)] ; (5)
      (dom/div #js {:key react-key}
        (dom/h4 nil (str "Current User: " (:person/name current-user)))
        (dom/button #js {:onClick (fn [] (df/load this [:person/by-id 3] Person))} "Refresh User with ID 3")
        (ui-person-list friends)
        (ui-person-list enemies)
        (dom/h4 nil "Blog Posts") ; (6)
        (ui-posts blog-posts)))))
  1. A component to represent the post itself

  2. A component to represent the list of the posts

  3. Composing the Posts UI into root query

  4. Composing the Posts UI into root initial data

  5. Pull the resulting app db data from props

  6. Render the list

Of course, there are no posts yet, so all you’ll see is the heading. Notice that there is nothing new here. The UI is completely network agnostic, as it should be.

Now for the networking code. This bit is a little longer, but most of it is the details around network communcation itself, rather than the work you have to do. Create a new namespace src/main/app/rest.cljs:

(ns app.rest
  (:refer-clojure :exclude [send])
  (:require [untangled.client.logging :as log]
            [untangled.client.network :as net]
            [cognitect.transit :as ct]
            [goog.events :as events]
            [om.transit :as t]
            [clojure.string :as str]
            [clojure.set :as set]
            [om.next :as om])
  (:import [goog.net XhrIo EventType]))

(defn make-xhrio [] (XhrIo.))

(defrecord Network [url request-transform global-error-callback complete-app transit-handlers]
  net/NetworkBehavior
  (serialize-requests? [this] true)
  net/IXhrIOCallbacks
  (response-ok [this xhr-io valid-data-callback]
    ;; Implies:  everything went well and we have a good response
    ;; (i.e., got a 200).
    (try
      (let [read-handlers (:read transit-handlers)
            ; STEP 3: Convert the JSON response into a proper tree structure to match the query
            response      (.getResponseJson xhr-io)
            edn           (js->clj response) ; convert it to clojure
            ; Rename the keys from strings to the desired UI keywords
            posts         (mapv #(set/rename-keys % {"id"     :db/id
                                                     "title"  :post/title
                                                     "userId" :post/user-id
                                                     "body"   :post/body})
                            edn)
            ; IMPORTANT: structure of the final data we send to the callback must match the nesting structure of the query
            ; [{:posts [...]}] or it won't merge correctly:
            fixed-response      {:posts posts}]
        (js/console.log :converted-response fixed-response)
        ; STEP 4; Send the fixed up response back to the client DB
        (when (and response valid-data-callback) (valid-data-callback fixed-response)))
      (finally (.dispose xhr-io))))
  (response-error [this xhr-io error-callback]
    ;; Implies:  request was sent.
    ;; *Always* called if completed (even in the face of network errors).
    ;; Used to detect errors.
    (try
      (let [status                 (.getStatus xhr-io)
            log-and-dispatch-error (fn [str error]
                                     ;; note that impl.application/initialize will partially apply the
                                     ;; app-state as the first arg to global-error-callback
                                     (log/error str)
                                     (error-callback error)
                                     (when @global-error-callback
                                       (@global-error-callback status error)))]
        (if (zero? status)
          (log-and-dispatch-error
            (str "UNTANGLED NETWORK ERROR: No connection established.")
            {:type :network})
          (log-and-dispatch-error (str "SERVER ERROR CODE: " status) {})))
      (finally (.dispose xhr-io))))
  net/UntangledNetwork
  (send [this edn ok error]
    (let [xhrio       (make-xhrio)
          ; STEP 1: Convert the request(s) from Om query notation to REST...
          ; some logic to morph the incoming request into REST (assume you'd factor this out to handle numerous kinds)
          request-ast (-> (om/query->ast edn) :children first)
          uri         (str "/" (name (:key request-ast)))   ; in this case, posts
          url         (str "http://jsonplaceholder.typicode.com" uri)]
      (js/console.log :REQUEST request-ast :URI uri)
      ; STEP 2: Send the request
      (.send xhrio url "GET")
      ; STEP 3 (see response-ok above)
      (events/listen xhrio (.-SUCCESS EventType) #(net/response-ok this xhrio ok))
      (events/listen xhrio (.-ERROR EventType) #(net/response-error this xhrio error))))
  (start [this app]
    (assoc this :complete-app app)))

(defn make-rest-network [] (map->Network {}))

The steps you need to customize are annotated in the comments of the code. There are just a few basic steps:

  1. Om comes with a handy function that can convert a query into an AST, which is easier to process. We don’t really care too much about the whole query, we just want to detect what is being asked for (we’re going to ask for :posts).

  2. Once we’ve understood what is wanted, we create a REST URL and GET the data from the REST server.

  3. When we get a successful response we need to convert the JSON into the proper EDN that the client expects. In this case we’re looking for { :posts [ {:db/id 1 :post/body "…​" :post/title "…​" ] …​ }.

  4. Once we have the properly structure tree of data to match the query, we simply pass it to the ok callback that our send was given.

In a more complete program, you’d put hooks at steps (2) and (3) to handle all of the different REST requests, so that the majority of this code would be a one-time thing.

Untangled lets you set up networking yourself. We’d still like to talk to our server, but now we also want to be able to talk to the REST server. The modification is done in our basic-ui namespace where we create the client:

(ns app.basic-ui
  (:require [untangled.client.core :as uc]
            [om.dom :as dom]
            [app.operations :as ops]
            [om.next :as om :refer [defui]]
            [app.rest :as rest] ; <-------ADD THIS
            [untangled.client.data-fetch :as df]
            [untangled.client.mutations :as m]
            [untangled.client.network :as net]))

...

(defonce app-1 (atom (uc/new-untangled-client
                       ; Set up two networking handlers (:remote is an explicit creation of the "default" that we still want)
                       :networking {:remote (net/make-untangled-network "/api" :global-error-callback (constantly nil))
                                    :rest   (rest/make-rest-network)}
                       :started-callback ...)))

All the hard stuff is done. Loading is now triggered just like you would have before, except with a :remote option to specify which network to talk over:

(defonce app-1 (atom (uc/new-untangled-client
                       ...
                       :started-callback (fn [app]

                                           (df/load app :posts Post {:remote :rest :target [:post-list/by-id :the-one :posts]})

                                           ... as before ...

The same technique is used. Everything you’ve read is accurate for mutations as well (you’ll see the mutation come into the send function). To trigger a mutation, just add another section to your client mutation (a mutation can be sent to any number of remotes, in fact):

(defmutation delete-post
  [{:keys [id]}]
  (action [env] ...stuff to affect local db...)
  ; you could also include this: (remote [env] true)
  (rest [env] true)) ; tell the :rest networking to send this mutation

So, action names the local (optimistic) effect. Each other method name must match a remote’s name as configured in the :networking of the client. If you return true (or an AST) from one of these "remote" sections, it will trigger the mutation to be sent to that network handler.

Just for reference the complete project for this guide is on Github at https://github.com/awkay/untangled-getting-started