├── .gitignore ├── src └── app │ ├── client_app.cljs │ ├── routing.cljs │ ├── model │ ├── session.clj │ └── session.cljs │ ├── server.clj │ ├── ui │ └── dynamic_menu.cljs │ └── client.cljs ├── package.json ├── resources └── public │ └── index.html ├── shadow-cljs.edn ├── dev └── user.clj ├── deps.edn └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .shadow-cljs 2 | node_modules 3 | resources/public/js 4 | .cpcache 5 | .idea 6 | target 7 | .nrepl-port 8 | /video-series.iml 9 | -------------------------------------------------------------------------------- /src/app/client_app.cljs: -------------------------------------------------------------------------------- 1 | (ns app.client-app 2 | (:require [com.fulcrologic.fulcro.networking.http-remote :as http] 3 | [com.fulcrologic.fulcro.application :as app])) 4 | 5 | (defonce APP (app/fulcro-app {:remotes {:remote (http/fulcro-http-remote {})}})) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "react": "^16.9.0", 6 | "react-dom": "^16.9.0", 7 | "shadow-cljs": "^2.8.52", 8 | "big.js": "^5.2.2", 9 | "react-number-format": "^4.2.0", 10 | "semantic-ui-react": "^0.88.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Loading...
8 | 9 | 10 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps {:aliases [:dev]} 2 | :nrepl {:port 9000} 3 | 4 | :dev-http {8000 "resources/public"} 5 | 6 | :builds {:main {:target :browser 7 | :output-dir "resources/public/js/main" 8 | :asset-path "/js/main" 9 | 10 | :modules {:main {:init-fn app.client/start}} 11 | :devtools {:after-load app.client/refresh 12 | :preloads [com.fulcrologic.fulcro.inspect.preload]}}}} 13 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [org.httpkit.server :as http] 3 | [app.server :refer [middleware]] 4 | [clojure.tools.namespace.repl :as tools-ns] 5 | [taoensso.timbre :as log])) 6 | 7 | ;; make sure not to find things in places like resources 8 | (tools-ns/set-refresh-dirs "src" "dev") 9 | 10 | (defonce server (atom nil)) 11 | 12 | (defn start [] 13 | (let [result (http/run-server middleware {:port 3000})] 14 | (log/info "Started web server on port 3000") 15 | (reset! server result) 16 | :ok)) 17 | 18 | (defn stop [] 19 | (when @server 20 | (log/info "Stopped web server") 21 | (@server))) 22 | 23 | (defn restart [] 24 | (stop) 25 | (log/info "Reloading code") 26 | (tools-ns/refresh :after 'user/start)) 27 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | 3 | :deps {com.fulcrologic/fulcro {:mvn/version "3.5.26"} 4 | com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.0"} 5 | ring/ring-core {:mvn/version "1.9.6"} 6 | kibu/pushy {:mvn/version "0.3.8"} 7 | http-kit/http-kit {:mvn/version "2.3.0"} 8 | com.wsscode/pathom {:mvn/version "2.3.1"}} 9 | 10 | :aliases {:dev {:extra-paths ["dev"] 11 | :extra-deps {org.clojure/clojurescript {:mvn/version "1.10.914"} 12 | org.clojure/tools.namespace {:mvn/version "1.3.0"} 13 | thheller/shadow-cljs {:mvn/version "2.16.11"} 14 | binaryage/devtools {:mvn/version "1.0.6"}}}}} 15 | -------------------------------------------------------------------------------- /src/app/routing.cljs: -------------------------------------------------------------------------------- 1 | (ns app.routing 2 | (:require 3 | [app.client-app :refer [APP]] 4 | [taoensso.timbre :as log] 5 | [com.fulcrologic.fulcro.routing.dynamic-routing :as dr] 6 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]] 7 | [clojure.string :as str] 8 | [pushy.core :as pushy])) 9 | 10 | (defonce history (pushy/pushy 11 | (fn [p] 12 | (let [route-segments (vec (rest (str/split p "/")))] 13 | (dr/change-route APP route-segments))) 14 | identity)) 15 | 16 | (defn start! [] 17 | (pushy/start! history)) 18 | 19 | (defn route-to! 20 | "Change routes to the given route-string (e.g. \"/home\"." 21 | [route-string] 22 | (pushy/set-token! history route-string)) 23 | 24 | (defmutation route-to 25 | "Mutation to go to a specific route" 26 | [{:keys [route-string]}] 27 | (action [_] 28 | (route-to! route-string))) 29 | -------------------------------------------------------------------------------- /src/app/model/session.clj: -------------------------------------------------------------------------------- 1 | (ns app.model.session 2 | (:require 3 | [com.wsscode.pathom.connect :as pc] 4 | [com.fulcrologic.fulcro.server.api-middleware :refer [augment-response]] 5 | [taoensso.timbre :as log])) 6 | 7 | (def users (atom {1 {:user/id 1 8 | :user/email "foo@bar.com" 9 | :user/password "letmein"}})) 10 | 11 | (pc/defresolver user-resolver [env {:user/keys [id]}] 12 | {::pc/input #{:user/id} 13 | ::pc/output [:user/email]} 14 | {:user/id id 15 | :user/email (get-in @users [id :user/email])}) 16 | 17 | (pc/defresolver current-user-resolver [env _] 18 | {::pc/output [{:session/current-user [:user/id]}]} 19 | (let [{:user/keys [id email]} (log/spy :info (-> env :request :session))] 20 | {:session/current-user 21 | (if id 22 | {:user/email email 23 | :user/id id 24 | :user/valid? true} 25 | {:user/id :nobody 26 | :user/valid? false})})) 27 | 28 | (pc/defmutation login [env {:user/keys [email password]}] 29 | {::pc/params #{:user/email :user/password} 30 | ::pc/output [:user/id :user/valid?]} 31 | (log/info "Login " email) 32 | (Thread/sleep 500) 33 | (let [subject (first (filter (fn [u] (= (:user/email u) email)) (vals @users)))] 34 | (if (and subject (= password (:user/password subject))) 35 | (augment-response 36 | {:user/email email 37 | :user/id (:user/id subject) 38 | :user/valid? true} 39 | (fn [ring-resp] (assoc ring-resp :session subject))) 40 | {:user/valid? false}))) 41 | 42 | (pc/defmutation logout [env {:user/keys [email password]}] 43 | {::pc/params #{:user/email :user/password} 44 | ::pc/output [:user/id :user/valid?]} 45 | (augment-response 46 | {:user/id :nobody 47 | :user/valid? false} 48 | (fn [ring-resp] (assoc ring-resp :session {})))) 49 | 50 | (def resolvers [user-resolver current-user-resolver login logout]) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Series File 2 | 3 | This repository contains snapshots of the repository as it existed at various interesting points 4 | in the video series. These are represented as tags (see the "Branch" menu at the top of the file list). 5 | 6 | NOTE: I consider these files part of a "performance" (the video recordings). They are *not* meant to be perfect examples of how you should build an application, but instead attempts to help you understand concepts and APIs. Being the product of a performance I *know* there are imperfections, and I do *not* intend to correct them, because that could put things out of sync between the videos and these files. You should *not* attempt to change the versions of any libraries used in these files, nor should you submit PRs to me to correct things like console warnings about things like React keys. Code perfection is not the objective here: you should be concentrating on learning broad concepts. 7 | 8 | Tags of interest for the files at the end of: 9 | 10 | * `intro` : The intro video 11 | * `normalization` : The normalization video 12 | * `initial-state` : The initial state/mutations video. 13 | * `dom+react` : The components, dom, and react video. 14 | * `how-rendering-works` : The How Rendering Works video. 15 | * `full-stack-1` : The first full-stack video. 16 | * `full-stack-over-time` : Parts 7 and 8. 17 | * `tx-processing` : Transaction fundamentals video. 18 | * `forms-part-1` : Part 10, Form inputs and state. 19 | * `forms-part-2` : Part 11, Form state tracking support. 20 | * `forms-part-3` : Part 12, Subforms and entity ownership 21 | * `forms-final` : Part 13, Finishing up the forms 22 | * `form-save-sql`: Part 13, final part of video. 23 | * `rendering-part-2`: Part 14, Rendering Revisited. 24 | * `sessions-and-routing`: Part 15, sessions and routing 25 | * `dynamic-ui-as-data`: Part 16, Driving dynamic UI with data. 26 | * `ui-state-machine`: Parts 17 + 18, UI State Machines 27 | 28 | If you are relatively new to Git, you would access these as 29 | follows: 30 | 31 | 1. Clone this repository 32 | 33 | ``` 34 | git clone https://github.com/fulcrologic/video-series.git 35 | ``` 36 | 37 | 2. Check out the tag you are interested in working against. 38 | 39 | ``` 40 | git checkout dom+react 41 | ``` 42 | -------------------------------------------------------------------------------- /src/app/server.clj: -------------------------------------------------------------------------------- 1 | (ns app.server 2 | (:require 3 | [clojure.core.async :as async] 4 | [com.fulcrologic.fulcro.server.api-middleware :as fmw :refer [not-found-handler handle-api-request]] 5 | [com.wsscode.pathom.connect :as pc] 6 | [com.wsscode.pathom.core :as p] 7 | [app.model.session :as session] 8 | [ring.middleware.session :refer [wrap-session]] 9 | [ring.middleware.content-type :refer [wrap-content-type]] 10 | [ring.middleware.not-modified :refer [wrap-not-modified]] 11 | [ring.middleware.resource :refer [wrap-resource]] 12 | [clojure.string :as str])) 13 | 14 | (def my-resolvers [session/resolvers]) 15 | 16 | ;; setup for a given connect system 17 | (def parser 18 | (p/parallel-parser 19 | {::p/env {::p/reader [p/map-reader 20 | pc/parallel-reader 21 | pc/open-ident-reader] 22 | ::pc/mutation-join-globals [:tempids]} 23 | ::p/mutate pc/mutate-async 24 | ::p/plugins [(pc/connect-plugin {::pc/register my-resolvers}) 25 | (p/post-process-parser-plugin p/elide-not-found) 26 | p/error-handler-plugin]})) 27 | 28 | (defn all-routes-to-index [handler] 29 | (fn [{:keys [uri] :as req}] 30 | (if (or 31 | (= "/api" uri) 32 | (str/ends-with? uri ".css") 33 | (str/ends-with? uri ".map") 34 | (str/ends-with? uri ".jpg") 35 | (str/ends-with? uri ".png") 36 | (str/ends-with? uri ".js")) 37 | (handler req) 38 | (handler (assoc req :uri "/index.html"))))) 39 | 40 | (defn wrap-api 41 | [handler] 42 | (let [parser (fn [env query] (async/ not-found-handler 49 | (wrap-api) 50 | (fmw/wrap-transit-params) 51 | (fmw/wrap-transit-response) 52 | (wrap-session) 53 | (wrap-resource "public") 54 | wrap-content-type 55 | wrap-not-modified 56 | (all-routes-to-index))) 57 | -------------------------------------------------------------------------------- /src/app/ui/dynamic_menu.cljs: -------------------------------------------------------------------------------- 1 | (ns app.ui.dynamic-menu 2 | (:require 3 | [app.routing :as r] 4 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 5 | [com.fulcrologic.fulcro.mutations :refer [defmutation]] 6 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]] 7 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-menu :refer [ui-dropdown-menu]] 8 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-item :refer [ui-dropdown-item]] 9 | [com.fulcrologic.fulcro.dom :as dom])) 10 | 11 | (defn link 12 | ([title mutation mutation-params] 13 | {:type :link 14 | :label title 15 | :mutation mutation 16 | :mutation-params mutation-params}) 17 | ([title mutation] (link title mutation {}))) 18 | 19 | (defn dropdown 20 | ([title & items] 21 | {:type :dropdown 22 | :label title 23 | :items (vec items)})) 24 | 25 | (defn dropdown-item 26 | ([title mutation mutation-params] 27 | {:label title 28 | :mutation mutation 29 | :mutation-params mutation-params}) 30 | ([title mutation] (dropdown-item title mutation {}))) 31 | 32 | (defn menu [& items] 33 | {:dynamic-menu/items (vec items)}) 34 | 35 | 36 | (defsc DynamicMenu [this {:keys [:dynamic-menu/items] :as props}] 37 | {:query [:dynamic-menu/items] 38 | :initial-state {:dynamic-menu/items []} 39 | :ident (fn [] [:component/id ::dynamic-menu])} 40 | (comp/fragment 41 | (map (fn [{:keys [type label mutation mutation-params items]}] 42 | (if (= type :link) 43 | (dom/div :.item {:onClick #(comp/transact! this [(list mutation (or mutation-params {}))])} 44 | (dom/div :.content label)) 45 | (ui-dropdown {:item true 46 | :text label} 47 | (ui-dropdown-menu {} 48 | (map (fn [{:keys [label mutation mutation-params]}] 49 | (ui-dropdown-item {:onClick #(comp/transact! this [(list mutation (or mutation-params {}))])} 50 | label)) 51 | items))))) 52 | items))) 53 | 54 | (def ui-dynamic-menu (comp/factory DynamicMenu)) 55 | 56 | (defmutation set-menu [menu] 57 | (action [{:keys [state]}] 58 | (swap! state assoc-in [:component/id ::dynamic-menu] menu)) 59 | (refresh [_] 60 | [:dynamic-menu/items])) 61 | 62 | (defn set-menu! [this menu] 63 | (comp/transact! this [(set-menu menu)])) 64 | 65 | (defn clear-menu! [this] 66 | (comp/transact! this [(set-menu (menu))])) 67 | -------------------------------------------------------------------------------- /src/app/model/session.cljs: -------------------------------------------------------------------------------- 1 | (ns app.model.session 2 | (:require 3 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]] 4 | [com.fulcrologic.fulcro.dom :as dom :refer [div a]] 5 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 6 | [com.fulcrologic.fulcro.dom.html-entities :as ent] 7 | [com.fulcrologic.fulcro.routing.dynamic-routing :as dr] 8 | [com.fulcrologic.fulcro.algorithms.merge :as merge] 9 | [com.fulcrologic.fulcro.ui-state-machines :as uism :refer [defstatemachine]] 10 | [app.routing :as routing] 11 | [taoensso.timbre :as log])) 12 | 13 | (defn handle-login [{::uism/keys [event-data] :as env}] 14 | (let [user-class (uism/actor-class env :actor/user)] 15 | (-> env 16 | (uism/trigger-remote-mutation :actor/login-form `login 17 | (merge event-data 18 | {:com.fulcrologic.fulcro.mutations/returning user-class 19 | :com.fulcrologic.fulcro.algorithms.data-targeting/target [:session/current-user] 20 | ::uism/ok-event :event/ok 21 | ::uism/error-event :event/error})) 22 | (uism/activate :state/checking-credentials)))) 23 | 24 | (def main-events 25 | {:event/logout {::uism/handler (fn [env] 26 | (routing/route-to! "/login") 27 | (-> env 28 | (uism/trigger-remote-mutation :actor/login `logout {}) 29 | (uism/apply-action assoc-in [::session :current-user] {:user/id :nobody :user/valid? false})))} 30 | :event/login {::uism/handler handle-login}}) 31 | 32 | (defstatemachine session-machine 33 | {::uism/actor-name 34 | #{:actor/user 35 | :actor/login-form} 36 | 37 | ::uism/aliases 38 | {:logged-in? [:actor/user :user/valid?]} 39 | 40 | ::uism/states 41 | {:initial 42 | {::uism/handler 43 | (fn [{::uism/keys [event-data] :as env}] 44 | (-> env 45 | (uism/store :config event-data) ; save desired path for later routing 46 | (uism/load :session/current-user :actor/user {::uism/ok-event :event/ok 47 | ::uism/error-event :event/error}) 48 | (uism/activate :state/checking-existing-session)))} 49 | 50 | :state/checking-existing-session 51 | {::uism/events 52 | {:event/ok {::uism/handler (fn [env] 53 | (let [logged-in? (uism/alias-value env :logged-in?)] 54 | (when-not logged-in? 55 | (routing/route-to! "/login")) 56 | (uism/activate env :state/idle)))} 57 | :event/error {::uism/handler (fn [env] (uism/activate env :state/server-failed))}}} 58 | 59 | :state/bad-credentials 60 | {::uism/events main-events} 61 | 62 | :state/idle 63 | {::uism/events main-events} 64 | 65 | :state/checking-credentials 66 | {::uism/events {:event/ok {::uism/handler (fn [env] 67 | (let [logged-in? (uism/alias-value env :logged-in?) 68 | {:keys [desired-path]} (uism/retrieve env :config)] 69 | (when (and logged-in? desired-path) 70 | (routing/route-to! desired-path)) 71 | (-> env 72 | (uism/activate (if logged-in? 73 | :state/idle 74 | :state/bad-credentials)))))} 75 | :event/error {::uism/handler (fn [env] (uism/activate env :state/server-failed))}}} 76 | 77 | :state/server-failed 78 | {::uism/events main-events}}}) 79 | 80 | (defsc CurrentUser [this {:keys [:user/id :user/email :user/valid?] :as props}] 81 | {:query [:user/id :user/email :user/valid?] 82 | :initial-state {:user/id :nobody :user/valid? false} 83 | :ident (fn [] [::session :current-user])} 84 | (dom/div :.item 85 | (if valid? 86 | (div :.content 87 | email ent/nbsp (a {:onClick 88 | (fn [] (uism/trigger! this ::sessions :event/logout))} 89 | "Logout")) 90 | (a {:onClick #(dr/change-route this ["login"])} "Login")))) 91 | 92 | (def ui-current-user (comp/factory CurrentUser)) 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/app/client.cljs: -------------------------------------------------------------------------------- 1 | (ns app.client 2 | (:require 3 | [app.client-app :refer [APP]] 4 | [app.routing :as routing] 5 | [app.ui.dynamic-menu :as dynamic-menu] 6 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown :refer [ui-dropdown]] 7 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-menu :refer [ui-dropdown-menu]] 8 | [com.fulcrologic.semantic-ui.modules.dropdown.ui-dropdown-item :refer [ui-dropdown-item]] 9 | [com.fulcrologic.fulcro.application :as app] 10 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 11 | [com.fulcrologic.fulcro.dom :as dom :refer [div ul li button h3 label a input table tr td th thead tbody tfoot]] 12 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]] 13 | [com.fulcrologic.fulcro.data-fetch :as df] 14 | [com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]] 15 | [app.model.session :as session :refer [CurrentUser ui-current-user]] 16 | [taoensso.timbre :as log] 17 | [com.fulcrologic.fulcro.dom.events :as evt] 18 | [app.routing :as r] 19 | [com.fulcrologic.fulcro.ui-state-machines :as uism])) 20 | 21 | (defsc LoginForm [this {:ui/keys [email password] :as props}] 22 | {:query [:ui/email :ui/password [::uism/asm-id '_]] 23 | :ident (fn [] [:component/id :login]) 24 | :route-segment ["login"] 25 | :initial-state {:ui/email "foo@bar.com" 26 | :ui/password "letmein"}} 27 | (let [current-state (uism/get-active-state this ::session/sessions) 28 | busy? (= :state/checking-credentials current-state) 29 | bad-credentials? (= :state/bad-credentials current-state) 30 | error? (= :state/server-failed current-state)] 31 | (div :.ui.container.segment 32 | (dom/div :.ui.form {:classes [(when (or bad-credentials? error?) "error")]} 33 | (div :.field 34 | (label "Username") 35 | (input {:value email 36 | :disabled busy? 37 | :onChange #(m/set-string! this :ui/email :event %)})) 38 | (div :.field 39 | (label "Password") 40 | (input {:type "password" 41 | :value password 42 | :disabled busy? 43 | :onKeyDown (fn [evt] 44 | (when (evt/enter-key? evt) 45 | (uism/trigger! this ::session/sessions :event/login {:user/email email 46 | :user/password password}))) 47 | :onChange #(m/set-string! this :ui/password :event %)})) 48 | (when bad-credentials? 49 | (div :.ui.error.message 50 | (div :.content 51 | "Invalid Credentials"))) 52 | (when error? 53 | (div :.ui.error.message 54 | (div :.content 55 | "There was a server error. Please try again."))) 56 | (button :.ui.primary.button 57 | {:classes [(when busy? "loading")] 58 | :onClick #(uism/trigger! this ::session/sessions :event/login {:user/email email 59 | :user/password password})} 60 | "Login"))))) 61 | 62 | (defsc Home [this props] 63 | {:query [:pretend-data] 64 | :ident (fn [] [:component/id :home]) 65 | :route-segment ["home"] 66 | :initial-state {}} 67 | (dom/div :.ui.container.segment 68 | (h3 "Home Screen"))) 69 | 70 | (defsc Settings [this props] 71 | {:query [:pretend-data] 72 | :ident (fn [] [:component/id :settings]) 73 | :route-segment ["settings"] 74 | :componentDidMount (fn [this] 75 | (dynamic-menu/set-menu! this (dynamic-menu/menu 76 | (dynamic-menu/link "Other" `other)))) 77 | :componentWillUnmount (fn [this] (dynamic-menu/clear-menu! this)) 78 | :initial-state {}} 79 | (dom/div :.ui.container.segment 80 | (h3 "Settings Screen"))) 81 | 82 | (defrouter MainRouter [_ _] {:router-targets [LoginForm Home Settings]}) 83 | 84 | (def ui-main-router (comp/factory MainRouter)) 85 | 86 | (defsc Root [this {:root/keys [router dynamic-menu] 87 | :session/keys [current-user]}] 88 | {:query [{:root/router (comp/get-query MainRouter)} 89 | [::uism/asm-id ::session/sessions] 90 | {:root/dynamic-menu (comp/get-query dynamic-menu/DynamicMenu)} 91 | {:session/current-user (comp/get-query CurrentUser)}] 92 | :initial-state (fn [_] 93 | {:root/router (comp/get-initial-state MainRouter) 94 | :session/current-user (comp/get-initial-state CurrentUser) 95 | :root/dynamic-menu (dynamic-menu/menu)})} 96 | (let [current-state (uism/get-active-state this ::session/sessions) 97 | ready? (not (contains? #{:initial :state/checking-existing-session} current-state)) 98 | logged-in? (:user/valid? (log/spy :info current-user))] 99 | (div 100 | (div :.ui.top.fixed.menu 101 | (div :.item 102 | (div :.content "My Cool App")) 103 | (when logged-in? 104 | (comp/fragment 105 | (div :.item 106 | (div :.content (a {:href "/home"} "Home"))) 107 | (div :.item 108 | (div :.content (a {:href "/settings"} "Settings"))) 109 | (dynamic-menu/ui-dynamic-menu dynamic-menu))) 110 | (div :.right.floated.item 111 | (ui-current-user current-user))) 112 | (when ready? 113 | (div :.ui.grid {:style {:marginTop "4em"}} 114 | (ui-main-router router)))))) 115 | 116 | (defn refresh [] 117 | (app/mount! APP Root "app")) 118 | 119 | (defn ^:export start [] 120 | (app/mount! APP Root "app") 121 | (routing/start!) 122 | (uism/begin! APP session/session-machine ::session/sessions 123 | {:actor/user session/CurrentUser 124 | :actor/login-form LoginForm} 125 | {:desired-path (some-> js/window .-location .-pathname)})) 126 | 127 | --------------------------------------------------------------------------------