-
Notifications
You must be signed in to change notification settings - Fork 364
Thinking With Links!
"There is no Tree, only Graph" — The Ancients
When building larger systems on top of React you are often faced with the problem of structuring your application data so that you can feed the right information into all of your components. This problem is greatly exacerbated if you would prefer to embrace an immutable application model. Om Next is designed out of the box to fix this problem by making links (also called idents) a first class concept.
We assume you are now familiar with the previous tutorial setups. We
will not cover that material other than the your project.clj
, which
should look like the following:
(defproject om-tutorial "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.170"]
[org.omcljs/om "1.0.0-alpha24"]
[figwheel-sidecar "0.5.0-SNAPSHOT" :scope "test"]])
Your ns form should look like the following and you might want to enable printing for debugging purposes:
(ns om-tutorial.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]))
(enable-console-print!)
Let's look at a very typical problem in UI programs.
Imagine our application data is structured like the following:
(def init-data
{:current-user {:email "[email protected]"}
:items [{:id 0 :title "Foo"}
{:id 1 :title "Bar"}
{:id 2 :title "Baz"}]})
We hold everything about the current user in a hash map at the top level of the application state. We will be rendering subviews and these subviews will need this piece of global information. This information has no reification in a database, and we don't actually want to pollute our data with component view needs.
What to do?!?
Links to the rescue!
Before we do that let's define our read parsing functions:
(defmulti read om/dispatch)
(defmethod read :items
[{:keys [query state]} k _]
(let [st @state]
{:value (om/db->tree query (get st k) st)}))
If you've been through the earlier tutorials this is pretty boring stuff.
Let's see our first example of embedding links into query expressions.
(defui Item
static om/Ident
(ident [_ {:keys [id]}]
[:item/by-id id])
static om/IQuery
(query [_]
'[:id :title [:current-user _]])
Object
(render [this]
(let [{:keys [title current-user]} (om/props this)]
(dom/li nil
(dom/div nil title)
(dom/div nil (:email current-user))))))
Here we see a new pattern for the very first time. We are already familiar with the 2 element vector representation of idents, but we've never embedded them directly into query expressions.
When resolving a query, parsing will perform lookups at the root of the
application every time it encounters an ident. In this case the
second part of the ident doesn't matter because it is a unique value
in the application state - _
signifies this and a lookup will be
performed using just the key :current-user
. If instead we had a
proper ident with an id component we would do a proper table lookup.
Besides this one novelty everything else should be familiar.
The remainder of the application looks like the following:
(def item (om/factory Item))
(defui SomeList
static om/IQuery
(query [_]
[{:items (om/get-query Item)}])
Object
(render [this]
(println "Render")
(dom/div
(dom/h2 nil "A List!")
(dom/ul nil
(map item (-> this om/props :items))))))
(def reconciler
(om/reconciler
{:state init-data
:parser (om/parser {:read read})}))
(om/add-root! reconciler SomeList (gdom/getElement "app"))
Add your own links and play around with the various possibilities. You can use links in joins and subselect properties if you wish.
By embracing links you no longer have to write contorted logic to feed information to children deep in your view hierarchy.
Astute readers will wonder if this causes problems for remoting since this ident in this case is a client only concern.
The answer is "No".
Modify your read function to the following:
(defmethod read :items
[{:keys [query state ast]} k _]
(let [st @state]
{:value (om/db->tree query (get st k) st)
:remote (update-in ast [:query] #(into [] (remove om/ident?) %))}))
At the REPL try the following:
(parser {:state state} (om/get-query LinkList) :remote)
;; => [{:items [:id :title]}]
No problems.
Stop thinking in terms of trees start thinking in terms of graphs and links.
The entire source for this tutorial follows:
(ns om-tutorial.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]))
(enable-console-print!)
(def init-data
{:current-user {:email "[email protected]"}
:items [{:id 0 :title "Foo"}
{:id 1 :title "Bar"}
{:id 2 :title "Baz"}]})
(defmulti read om/dispatch)
(defmethod read :items
[{:keys [query state]} k _]
(let [st @state]
{:value (om/db->tree query (get st k) st)}))
(defui Item
static om/Ident
(ident [_ {:keys [id]}]
[:item/by-id id])
static om/IQuery
(query [_]
'[:id :title [:current-user _]])
Object
(render [this]
(let [{:keys [title current-user]} (om/props this)]
(dom/li nil
(dom/div nil title)
(dom/div nil (:email current-user))))))
(def item (om/factory Item))
(defui SomeList
static om/IQuery
(query [_]
[{:items (om/get-query Item)}])
Object
(render [this]
(println "Render")
(dom/div
(dom/h2 nil "A List!")
(dom/ul nil
(map item (-> this om/props :items))))))
(def reconciler
(om/reconciler
{:state init-data
:parser (om/parser {:read read})}))
(om/add-root! reconciler SomeList (gdom/getElement "app"))