-
-
Save IwanKaramazow/81c9df568e400d50bbf82aad04f71b40 to your computer and use it in GitHub Desktop.
Query Diffing + tempids
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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