Skip to content

Instantly share code, notes, and snippets.

@jichon

jichon/gist.clj Secret

Created June 13, 2016 16:45
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 jichon/1bf901b95a9d0562900e816ce4cd95f7 to your computer and use it in GitHub Desktop.
Save jichon/1bf901b95a9d0562900e816ce4cd95f7 to your computer and use it in GitHub Desktop.
nested tabbed interface
(ns app.core
(:require
app.mutations
[untangled.client.core :as uc]
[untangled.i18n :refer-macros [tr trf]]
[untangled.client.data-fetch :as df]
[om.next :as om]))
(def initial-state
(atom {
; These are database tables. We need this structure for the ident to work properly (it looks up something
; in a table when followed in the db). Of course, the tabs sub-structure can be completely different
; since the UI query can be different for each. That's the point. We *do* need data *inside* these objects
; that can be pulled by the ident function of the TabUnion so it can generate the proper ident on the fly.
; Thus, the :id and :which-tab fields.
:main {:tab {:id :tab
:which-tab :main
:main-content "Main tab"
:inner-tab1 {:tab {:id :tab
:which-tab :inner-tab1
:inner-tab1-content "Main1 tab"}}
:inner-tab2 {:tab {:id :tab
:which-tab :inner-tab2
:inner-tab2-content "Settings1 tab"}}
:current-inner-tab [:inner-tab1 :tab]
}}
:settings {:tab {:id :tab
:which-tab :settings
:settings-content "Settings tab"
}}
; The following is the top-level key that can be queried to find which tab to show.
; The first element of the ident is the "type" of tab, and will choose the query from
; the union in the UI (TabUnion)
:current-tab [:main :tab]}))
(defonce app (atom (uc/new-untangled-client :initial-state initial-state)))
(ns app.ui
(:require [om.dom :as dom]
[om.next :as om :refer-macros [defui]]
[untangled.i18n :refer-macros [tr trf]]
yahoo.intl-messageformat-with-locales
[untangled.client.data-fetch :as df]))
(defui ^:once InnerTab1
static om/IQuery
(query [this] [:which-tab :inner-tab1-content {[:tab-data-query '_] [:text]}])
Object
(render [this]
(let [{:keys [settings-content tab-data-query]} (om/props this)]
(dom/div nil
settings-content
(dom/p nil (:text tab-data-query))))))
(def ui-inner-tab1 (om/factory InnerTab1))
(defui ^:once InnerTab2
static om/IQuery
(query [this] [:which-tab :inner-tab2-content])
Object
(render [this]
(let [{:keys [main-content]} (om/props this)]
(dom/div nil main-content))))
(def ui-inner-tab2 (om/factory InnerTab2))
(defui ^:once InnerTabUnion
static om/IQuery
(query [this] {:inner-tab1 (om/get-query InnerTab1) :inner-tab2 (om/get-query InnerTab2)})
static om/Ident
(ident [this props] [(:which-tab props) :tab])
Object
(render [this]
(let [{:keys [which-tab] :as props} (om/props this)]
(dom/div nil
(case which-tab
:inner-tab1 (ui-inner-tab1 props)
:inner-tab2 (ui-inner-tab2 props)
(dom/p nil "Missing tab!"))))))
; This UI component uses a "link"...a special ident with '_ as the ID. This indicates the item is at the database
; root, not inside of the "settings" database object. This is not needed as a matter of course...it is only used
; for convenience (since it is trivial to load something into the root of the database)
(defui ^:once SettingsTab
static om/IQuery
(query [this] [:which-tab :settings-content {[:tab-data-query '_] [:text]}])
Object
(render [this]
(let [{:keys [settings-content tab-data-query]} (om/props this)]
(dom/div nil
settings-content
(dom/p nil (:text tab-data-query))))))
(def ui-settings-tab (om/factory SettingsTab))
(defui ^:once MainTab
static om/IQuery
(query [this] [:which-tab :main-content {:current-inner-tab (om/get-query InnerTabUnion)}])
Object
(render [this]
(let [{:keys [main-content]} (om/props this)]
(dom/div nil main-content))))
(def ui-main-tab (om/factory MainTab))
; This is the main trick: a component that serves to pick which UI/query to render based on which thing
; the ident of :current-tab points to in the database. The :current-tab name means nothing, of course, it
; was just a convenient name (see Root, which queried for it and passed the result here).
; IMPORTANT:
; 1. query must be a union, which is a map keyed by "object type" with the query to use for that object
; 2. The ident MUST derive the correct db ident for whatever you got in props
; 3. You are responsible for calling the correct UI from render to properly render the thing you got
(defui ^:once TabUnion
static om/IQuery
(query [this] {:main (om/get-query MainTab) :settings (om/get-query SettingsTab)})
static om/Ident
(ident [this props] [(:which-tab props) :tab])
Object
(render [this]
(let [{:keys [which-tab] :as props} (om/props this)]
(dom/div nil
(case which-tab
:main (ui-main-tab props)
:settings (ui-settings-tab props)
(dom/p nil "Missing tab!"))))))
(def ui-tabs (om/factory TabUnion))
(defui ^:once Root
static om/IQuery
(query [this] [:ui/react-key {:current-tab (om/get-query TabUnion)}])
Object
(render [this]
(let [{:keys [ui/react-key current-tab] :or {ui/react-key "ROOT"} :as props} (om/props this)]
(println "Root" props)
(dom/div #js {:key react-key}
; The selection of tabs can be rendered in a child, but the transact! must be done from the parent (to
; ensure proper re-render of the tab body). See om/computed for passing callbacks.
(dom/ul nil
(dom/li #js {:onClick #(om/transact! this '[(app/choose-tab {:tab :main})])} "Main")
(dom/li #js {:onClick #(om/transact! this '[(app/choose-tab {:tab :settings})
; extra mutation: sample of what you would do to lazy load the tab content
(app/lazy-load-tab {:tab :settings})])} "Settings"))
(ui-tabs current-tab)))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment