Skip to content

Latest commit

 

History

History
764 lines (584 loc) · 18 KB

om-next.org

File metadata and controls

764 lines (584 loc) · 18 KB

Summary

Reconciler

The reconciler is what ties all state together. It is passed a state store with: :state.

It has a :parser which manages the reading from state and the writing to (mutating) state.

Components

Components can specify an om/IQuery which is the data that they need to read. A query expression is always a vector. The result of parsing (reading) the state returns a map into om/props.

A component can also have om/IQueryParms, which are parameters for the query.

You can programmatically get a component with om/class->any.

Parser

A parser is made up of :read and :mutate functions. These functions are passed three parameters:

  • environment: {:state app-state}
  • key: the key from the component query
  • params: any parameters for the query

If we had the following code

(defn read [{:keys [state]} key _] {:value (key @state)})
(def my-parser (om/parser {:read read}))
(def my-state (atom {:count 0}))
(my-parser {:state my-state} [:count])
;; => {:count 0}

we can see that a reader should return a map with :value key. The value of the :value key will become the value in the (om/props this) map for the query key.

Mutating

We mutate by calling (om/transact ...) The first argument should be this i.e. the component calling transact. The second argument is the mutate key to dispatch which mutation to run. It might be called like:

(om/transact! this '[(increment)])

and then be intercepted like:

(defn mutate [env key parms]
  (if (= 'increment key)

Mutating with params

Calling:

(om/transact!
 this
 `[(todos/toggle-all
    {:value ~(not checked?)})
   :todos/list])

We can access the value of :value by doing:

(defmethod mutate 'todos/toggle-all
  [_ _ {:keys [value]}]

Mutate outside component

Can create a dummy function to see the effect of mutating without needing a component:

(defn tm []
  (let [parm "abc"]
    (om/transact!
     reconciler
     `[(product/description {:value ~parm})])))

repl snapshot:

pcw.core> (tm)
{product/description
 {:keys [:pc/product-description],
  :result
  {:db-before {1 :app/count, 1 :app/title},
   :db-after {1 :app/count, 1 :app/title, 2 :product/description},
   :tx-data [#datascript/Datom [2 :product/description "blah" 536870914 true]],
   :tempids {:db/current-tx 536870914},
   :tx-meta nil}}}
pcw.core> 

Development

To speed development it is nice to develop part of your application that is intended to run on the client on the server. One reason being we can use the cider debugger on server side.

Also datascript runs server side as well, so all reads/mutations can be developed server side.

Reads:

(defn read [{:keys [state]} key _] {:value (key @state)})
(def my-parser (om/parser {:read read}))
(def my-state (atom {:count 0}))
(my-parser {:state my-state} [:count])
;; => {:count 0}

Writes:

(om/transact!
     reconciler
     `[(product/description {:value ~parm})])

Basics

Reference:

https://github.com/omcljs/om/wiki/Quick-Start-(om.next)

The HTML

<project-root>/resources/public/index.html:

<!DOCTYPE html>
<html>
    <head lang="en">
        <meta charset="UTF-8">
        <title>Om Tutorial!</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="js/main.js"></script>
    </body>
</html>

The UI component:

<project-root>/src/oma/core.cljs:

(ns oma.core
  (:require [goog.dom :as gdom]
            [om.next :as om :refer-macros [defui]]
            [om.dom :as dom]))

(defui HelloWorld
  Object
  (render [this]
    (dom/div nil "Hello, world!")))

(def reconciler
  (om/reconciler {}))

(om/add-root!
 reconciler
 HelloWorld
 (gdom/getElement "app"))

That was the basic hello world app. Now lets add reading from a global state atom.

Add in the state atom:

(def my-state (atom {:count 0}))

Now we create a read function that allows us to read from this.

(defn read
  [{:keys [state] :as env} key params]
  (let [st @state]
    (if-let [[_ v] (find st key)]
      {:value v}
      {:value :not-found})))

Now we modify the reconciler to see the state and the reader function:

(def reconciler
  (om/reconciler
   {:state my-state
    :parser (om/parser {:read read})}))

Finally modify the component to read the data:

(defui HelloWorld
  static om/IQuery
  (query [this] [:count])
  Object
  (render
   [this]
   (let [{:keys [count]} (om/props this)]
     (dom/div nil (str "The count" count)))))

First lets discuss the read function. Its params are: env key params. When we pull out the :state key from env, we get my-state as set in the reconciler.

Find returns a map-entry for the connected to the key provided. So (find @state :count) -> [:count 0], which is destructured to just get the value with: [_ v].

Then our read function must return a map with a :value key.

The key is what is passed by our om/IQuery.

Parser

(def my-parser (om/parser {:read read}))
(def my-state (atom {:count 0}))
(my-parser {:state my-state} [:count :title])
;; => {:count 0, :title :not-found}

A query expression is always a vector. The result of parsing a query expression is always a map.

So we have a pretty good sense how to read from state, lets now look at how to write or mutate state.

Writing (mutating)

To write to state we need to supply a function that will mutate the state.

(defn mutate
  [{:keys [state] :as env} key params]
  (if (= 'increment key)
    {:value {:keys [:count]}
     :action #(swap! state update-in [:count] inc)}
    {:value :not-found}))

So here again we are passed in the :state, and the key.

:action is a function that takes no arguments and should transition the state to a new state.

We must return a map, with two keys: :value and :action. :value should be a map with a key :keys. :keys should be a list of keys that need to be re-read after the mutation is run because they are now stale. Since our action updates the value of our :count key, thats what we put in the {:value {:keys [:count]}} returned map.

(def my-parser (om/parser {:read read :mutate mutate}))
(my-parser {:state my-state} '[(increment)])
@my-state
;; => {:count 1}

Finally lets call this mutation from our UI component.

(dom/div
 nil
 (dom/span nil (str "Count: " count))
 (dom/button
  #js {:onClick
       (fn [e] (om/transact! this '[(increment)]))}
  "Click me!"))

Update our parser to have a mutator function as well:

(om/parser {:read read :mutate mutate})

Idents, normalization

om-next wants to have data be denormalized. Imagine the following data:

(def init-data
  {:list/one [{:name "Mary" :points 0}]
   :list/two [{:name "Mary" :points 0 :age 27}]})

We would like this data to look like:

{:list/one [[:person/by-name "Mary"]],
 :list/two [[:person/by-name "Mary"]],
 :person/by-name {"Mary" {:name "Mary", :points 0, :age 27}},
 :om.next/tables #{:person/by-name}}

So we identify the tables: :person/by-email. Now we can get data from that table. [:person/by-name "Mary"] specifies the table name and the key to extract the entity.

How did :person/by-name become the key? See this:

(defui Person
  static om/Ident
  (ident [this {:keys [name]}]
    [:person/by-name name])
  static om/IQuery
  (query [this]
    '[:name :points]))

The om/Ident specifies that the key is the name field, and we can lookup a given person with :person/by-name name.

remotes

First we create a reconciler with a send key like so:

(defn send-to-chan [c]
  (fn [{:keys [search]} cb]
    (when search
      (let [{[search] :children} (om/query->ast search)
            query (get-in search [:params :query])]
        (put! c [query cb])))))

(def send-chan (chan))

(def reconciler
  (om/reconciler
   { ;; ...
    :send    (send-to-chan send-chan)
    ;; ...
    }))

So :send is a function that takes two arguments. The first argument is a map whose keys are a list of remotes. The values are pending messages to be sent.

Our component has a query that looks like:

'[(:search/results {:query ?query})]

We have a read function that looks like:

(defmethod read :search/results
  [{:keys [state ast] :as env} k {:keys [query]}]
  (merge
    {:value (get @state k [])}
    (when-not (or (string/blank? query)
                  (< (count query) 3))
      {:search ast})))

Where does the state get updated? Not above.

We create a send channel with: (def send-chan (chan)). This gets passed into the construction of our send-to-chan function.

Our read function returns more than just a :value key when the query contains >= 3 chars. It also returns the name of a :remote identified in our list of remotes as wired up in our reconciler.

(def reconciler
  (om/reconciler
   { :send    (send-to-chan send-chan)
     :remotes [:remote :search]
    ; ...
    }))

Since the read function returns a registered remote, :search, we call our :send function. The send function is passed as its first argument a map whose key is a remote name and value, that which is specified in the read function. Our read function returned: {:search ast ...}

In our send-to-chan function we pull out the characters of the query from the ast. The second arg to the :send function is a callback that should be called with the results of the response from the server. The :send function in our example puts both the query string and the callback into the send-chan channel.

Meanwhile we have a loop that is watching that channel. It pulls out the callback and the query string, does the query, and calls the callback passing in a map whose key is :search/results, and value a data structure that the component is looking for.

First lets examine this piece of code:

(defn search-field [ac query]
  (dom/input
    #js {:key "search-field"
         :value query
         :onChange
         (fn [e]
           (om/set-query! ac
             {:params {:query (.. e -target -value)}}))}))

So there is an input field, that on change updates the parameters for the auto-completer component to have the values in the search box.

We add an AutoCompleter to the page with:

(om/add-root! reconciler AutoCompleter
  (gdom/getElement "app"))

We startup the search-loop code passing in a channel.

So as keys are entered into the input field, the om/IQueryParams value gets updated with the letters. As the query params change, the query :search/results gets re-run. This is defined in:

(defmethod read :search/results

When the query is not blank AND is 3 chars or more, the read function also returns a remote :search with the ast Abstract Syntax Tree. Since we wired this into the reconciler with the :remotes key as a remote, the :send function gets run.

datascript integration

data access layer

In the first step we’ll sort out our data access to the datascript database. We can test this all server side since both om and datascript are written in cljc files, so we dont need any figwheel. Just the plain old cider and clojure debugger.

First lets create a file called src/cljc/pcw/datascr.cljc

Lets imagine we want to do the classic todo app. The data structure will look like:

(ns pcw.datascr
  (:require [datascript.core :as d]))

(def schema {:todos {:db/type :db.type/ref
                     :db/cardinality :db.cardinality/many}
             :app-id {:db/unique :db.unique/identity}}) 

(def conn (d/create-conn schema))

(d/transact!
 conn
 [{:db/id -2
   :title "get milk"
   :done false
   :visible true}
  {:db/id -3
   :title "fix car"
   :done false
   :visible true}
  {:db/id -1
   :app-id :todo-list
   :todos [-2 -3]}])

Here we simply define a little what the data should look like. We sayy todos are of type ref and are a list, i.e. cardinality many.

We also created a property :app-id so we can just grab the list of todo’s by the name :todo-list.

Now lets add a function to display all visible todos.

(defn get-todos []
  (d/q '[:find (pull ?tds [*])
         :where
         [[:app-id :todo-list] :todos ?tds]
         [?tds :visible true]]
       @conn))

Sample REPL interaction:

pcw.datascr> (get-todos)
([{:db/id 2, :done false, :title "fix car", :visible true}]
 [{:db/id 1, :done false, :title "get milk", :visible true}])
pcw.datascr> 

Okay we’ve created the basic todo scenario. Now lets add functions to ‘delete’ or make invisible a todo, and the other CRUD functions:

(defn delete-todo [id]
  (d/transact!
   conn
   [{:db/id id :visible false}])
  nil)

(defn add-todo [title]
  (d/transact!
   conn
   [{:db/id -1 :visible true :done false :title title}
    [:db/add [:app-id :todo-list] :todos -1]])
  nil)

(defn update-todo [id title]
  (d/transact!
   conn
   [{:db/id id :title title}])
  nil)

Some repl’ing:

pcw.datascr> (get-todos)
([{:db/id 2, :done false, :title "fix car", :visible true}]
 [{:db/id 1, :done false, :title "get milk", :visible true}])
pcw.datascr> (delete-todo 1)
nil
pcw.datascr> (get-todos)
([{:db/id 2, :done false, :title "fix car", :visible true}])
pcw.datascr> (add-todo "eat chocolate")
nil
pcw.datascr> (get-todos)
([{:db/id 4,
   :done false,
   :title "eat chocolate",
   :visible true}]
 [{:db/id 2, :done false, :title "fix car", :visible true}])
pcw.datascr> (update-todo 4 "eat food")
nil
pcw.datascr> (get-todos)
([{:db/id 4, :done false, :title "eat food", :visible true}]
 [{:db/id 2, :done false, :title "fix car", :visible true}])
pcw.datascr> 

Component

Now lets look at how we might want to access this data from a component. First we need to fetch our data with a query.

(defui TodoList
  static om/IQuery
  (query [this] '[{:app-id :todo-list}])
  Object
  (render [this] (dom/div "Hello")))

This only interesting part here is the ‘[{:app-id :todo-list}] part. Basically specifying the attribute and value of the entity we want.

Lets wire up the rest of the required parts of an om.next app now too.

We want our file structure to look like:

src
 |-- cljc
 |   `-- pcw
 |       |-- datascr.cljc
 |       `-- parser.cljc
 `-- cljs
     `-- pcw
         |-- component.cljs
         `-- core.cljs

core wires together the parser and the components. While parser sits in front of datascr. Since component, something that will render in a browser is only valid in the context of a browser, it needs to be a cljs file. core, because it sees component also needs to be in a cljs file. parser tho, can be developed server side, but later used client side.

Here is a basic, almost non function parser

(ns pcw.parser
  (:require [pcw.datascr :as ds]
            [om.next :as om]))

(defmulti readr om/dispatch)

(defmethod readr :default
 [env keyz parms]
  {:value 1})

(def parser (om/parser {:read readr}))

(def reconciler
  (om/reconciler
   {:state ds/conn
    :parser parser}))

a simplistic component file:

(ns pcw.component
  (:require
   [om.next :as om :refer-macros [defui]]
   [om.dom :as dom]))

(defui TodoList
  static om/IQuery
  (query [this] '[:app-id])
  Object
  (render [this] (dom/div nil "Hello")))

And finally a core file to tie it all together:

(ns pcw.core
  (:require
   [goog.dom :as gdom]
   [om.next :as om]
   [pcw.parser :as p]
   [pcw.component :as com]))

(om/add-root!
 p/reconciler
 com/TodoList
 (gdom/getElement "app"))

Fire up figwheel and make sure you can see the component: “Hello”.

See commit with comment: “basic component only”. Hash: 2ba3325

Up to this point now we can see that the key passed to the reader is: :app-id. Now if we were to use this directly against the datascript db we might think of doing the following:

So lets play around with the parser:

pcw.parser> (parser {:state ds/conn} [:app-id])
{:app-id 1}
pcw.parser> (parser {:state ds/conn} [:blah])
{:blah 1}
pcw.datascr> (gen-get :app-id)
([{:db/id 3,
   :app-id :todo-list,
   :todos [#:db{:id 1} #:db{:id 2}]}])
pcw.datascr> 

read more

https://github.com/omcljs/om/wiki/Components,-Identity-&-Normalization

for how to have sub components like the actual todos…

tricks

The following operate on the following code:

(ns omn1.core
  (:require
   [om.next :as om :refer-macros [defui]]
   [om.dom :as dom :refer [div]]
   [goog.dom :as gdom]))

(defui MyComponent
  static om/IQuery
  (query [this] [:user])
  Object
  (render
   [this]
   (let [data (om/props this)]
     (div nil (str data)))))

(def app-state (atom {:user {:name "Fenton"}}))

(defn reader [{q :query st :state} _ _]
  (.log js/console (str "q: " q))
  {:value (om/db->tree q @app-state @app-state)})

(def parser (om/parser {:read reader}))

(def reconciler
  (om/reconciler
   {:state app-state
    :parser parser}))

(om/add-root! reconciler MyComponent (gdom/getElement "app"))

To test what data a query returns:

(om/db->tree query derefed-app-state derefed-app-state)
(om/db->tree [:current/user] @dat/app-state @dat/app-state)
{:current/user {:user/name "Fenton"}}

Test what parser returns

(parser {:state app-state} [:current/user])

Components

Get component

(om/component? (om/class->any reconciler MyComponent)) ; => true

Get its query

(om/get-query MyComponent)

bug report