Skip to content

Instantly share code, notes, and snippets.

@IwanKaramazow
Created June 14, 2016 07:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IwanKaramazow/81c9df568e400d50bbf82aad04f71b40 to your computer and use it in GitHub Desktop.
Save IwanKaramazow/81c9df568e400d50bbf82aad04f71b40 to your computer and use it in GitHub Desktop.
Query Diffing + tempids
(ns listdetail.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]
[clojure.set :as set]))
(enable-console-print!)
(def app-state {})
(defmulti read om/dispatch)
(defmethod read :projects
[{:keys [state ast query query-root]} k _]
(let [st @state]
(if-let [x (get-in st [:active-props k])]
{:value (om/db->tree query x st)}
{:remote (assoc ast :query-root true)})))
(defmethod read :project/detail
[{:keys [ast state query target] :as env} key {:keys [id]}]
(let [st @state
id (if id id (:project/selected st))]
(when id
(let [val (om/db->tree query [:project/by-id id] st)
available (keys val)
diffed-query (->
(into [] (set/difference (set query) (set available)))
(conj :id))]
(if (= available query)
{:value val}
{:value val
:remote (-> ast
(assoc :query diffed-query)
(assoc :query-root true)) })))))
(defmulti mutate om/dispatch)
(defmethod mutate 'project/select
[{:keys [state]} key {:keys [id]}]
{:action #(swap! state assoc :project/selected id)})
(defmethod mutate 'project/submit
[{:keys [state ast]} key project]
(let [temp-id (:id project)]
{:remote true
:action #(swap! state (fn [st]
(-> st
(update-in [:active-props :projects]
(fn [projects]
(conj projects [:project/by-id temp-id])))
(assoc-in [:project/by-id temp-id] project))))}))
(defmethod read :active-props
[{:keys [parser query ast target] :as env} key _]
(let [remote (parser env query target)]
(if (and target (not-empty remote))
{:remote (update-in ast [:query] (fn [query] remote))}
{:value (parser env query)})))
;; components
(defui ProjectListItem
static om/Ident
(ident [this {:keys [id]}]
[:project/by-id id])
static om/IQuery
(query [_]
[:id :title])
Object
(render [this]
(let [{:keys [id title]} (om/props this)
{:keys [select] } (om/get-computed this)]
(dom/li #js {:onClick (fn [e]
(select id))} title))))
(def projectListItem (om/factory ProjectListItem {:keyfn #(:id %)}))
(defn project-list [projects select]
(dom/div nil
(dom/h3 nil "Project list:")
(dom/ul nil
(map #(projectListItem (om/computed % {:select select})) projects))))
(defui ProjectDetail
static om/Ident
(ident [this {:keys [id]}]
[:project/by-id id])
static om/IQuery
(query [_]
[:id :title :description])
Object
(render [this]
(let [{:keys [id title description]} (om/props this)]
(dom/div nil
(dom/h3 nil "Project Detail")
(if (nil? id)
(dom/div nil "No project selected.")
(dom/div nil
(dom/div nil "Selected project: " id)
(dom/div nil "Title: " title)
(dom/div nil "Description: " description)))))))
(def project-detail (om/factory ProjectDetail))
(defn select-project [component]
(fn [id]
(om/transact! component `[(project/select {:id ~id})])
(om/update-query! component
(fn [{:keys [query params] :as q}]
(update q :params assoc :id id)) )))
(defn change [c e key]
(let [text (.. e -target -value)]
(om/update-state! c assoc key text )))
(defn empty-project []
{:id (om/tempid) :title "" :description ""})
(defn new-project [c]
(let [{:keys [title description] :as project} (om/get-state c)]
(dom/div nil
(dom/h3 nil "Add a new project: ")
(dom/input #js {:value title
:onChange #(change c % :title)})
(dom/input #js {:value description
:onChange #(change c % :description)})
(dom/button #js {:onClick #(do
(om/transact! c `[(project/submit ~project)])
(om/set-state! c (empty-project))
(doto % (.preventDefault) (.stopPropagation)))} "Submit!"))))
(defui Wrapper
static om/IQueryParams
(params [this]
{:id nil}) ;; =>
static om/IQuery
(query [_]
`[{:projects ~(om/get-query ProjectListItem)}
({:project/detail ~(om/get-query ProjectDetail)} {:id ~'?id})])
Object
(initLocalState [this]
(empty-project))
(render [this]
(let [{:keys [projects project/detail]} (om/props this)
{:keys [title description]} (om/get-state this)]
(dom/div nil
(project-list projects (select-project this))
(project-detail detail)
(new-project this)))))
(def wrapper (om/factory Wrapper))
(defui Root
static om/IQueryParams
(params
[this]
{:active-query []})
static om/IQuery
(query
[this]
'[{:active-props ?active-query}])
Object
(componentWillMount
[this]
(om/set-query! this
{:params {:active-query (om/get-query Wrapper)}}))
(render
[this]
(let [{:keys [active-props] :as props} (om/props this)]
(dom/div {} (wrapper active-props)))))
;; fake backend
(def fake-database (atom {:projects [[:project/by-id 1] [:project/by-id 2]], :project/by-id {1 {:id 1, :title "Learn Haskell" :description "How do I get started with Haskell?"}, 2 {:id 2, :title "Learn Scheme" :description "Lisp rocks!"}}}))
(defmulti backend-read om/dispatch)
(defmulti backend-mutate om/dispatch)
(defmethod backend-read :projects
[{:keys [database query]} key {:keys [id]}]
(let [db @database]
{:value (om/db->tree query (:projects db) db)}))
(defmethod backend-read :project/detail
[{:keys [database query]} key {:keys [id]}]
(let [db @database]
{:value (om/db->tree query [:project/by-id id] db)}))
(defn generate-real-id []
"Fake function to generate a fake real id"
(rand-int 100000))
(defn save-project [database {:keys [id] :as new-project}]
(swap! database (fn [db]
(-> db
(update-in [:projects]
(fn [projects]
(conj projects [:project/by-id id])))
(assoc-in [:project/by-id id] new-project)))))
(defmethod backend-mutate 'project/submit
[{:keys [database query]} key project]
(let [tempid (:id project) ;; get tempid from the client
realid (generate-real-id) ;; get the real id (here we fake a random number)
new-project (assoc project :id realid) ;; assoc the real id to the project
_ (save-project database new-project)] ;; save the new project to the db
{:value {:tempids {tempid realid}}}))
;; tempids => a map containing: `tempids` as keys `realids` as values
;; this is the format for our custom migrate function
;; Om.Next default-migrate wants another format: (from tempid ident to realid ident)
;; {:tempids {[project/by-id tempid] [project/by-id realid]}
;; default-migrate won't work here, no idea exactly why
(def backend-parser (om/parser {:read backend-read :mutate backend-mutate}))
;; fake a send to the fake backend
(defn send [{:keys [remote]} merge]
(let [{:keys [query rewrite]} (om/process-roots remote)
response (backend-parser {:database fake-database} query)]
(println "incoming query" query)
(println "response" response)
(merge (rewrite response))))
;; do not throw away the app state when merging
(defn deep-merge [& xs]
"Merges nested maps without overwriting existing keys."
(if (every? map? xs)
(apply merge-with deep-merge xs)
(last xs)))
;; merge tempids without throwing away app state
(defn tempid-migrate [app-state _ tempids _]
(clojure.walk/prewalk #(if (-> % type (= om.tempid/TempId))
(get tempids %) %)
app-state))
;;reconciler
(def reconciler
(om/reconciler
{:state app-state
:parser (om/parser {:read read :mutate mutate})
:send send
:merge-tree deep-merge
:migrate tempid-migrate
:id-key :id}))
(om/add-root! reconciler
Root (gdom/getElement "app"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment