### Clojure: Example of Starting a UI State Machine Instance
Source: https://book.fulcrologic.com/index
Provides practical examples of how to use `uism/begin!` to start a state machine. It demonstrates configuring the `actor-map` with singleton components and with dynamic actors using `with-actor-class` or `this` (component instance).
```Clojure
(uism/begin! this login-machine ::loginsm {:actor/dialog Dialog
:actor/session Session
:actor/form LoginForm})
```
```Clojure
(uism/begin! this person-editing-machine ::personsm {:person (uism/with-actor-class [:person/id 3] Person)
:editor this
:dialog Dialog})
```
--------------------------------
### Verify Clojure CLI Installation
Source: https://book.fulcrologic.com/index
This command demonstrates how to start the Clojure REPL (Read-Eval-Print Loop) using the `clj` command-line tool, confirming a successful installation of Clojure CLI Tools. The expected output shows the Clojure version and the `user=>` prompt, indicating readiness for Clojure development.
```Shell
$ clj
Clojure 1.10.0
user=>
```
--------------------------------
### Initialize Node.js Project and Install Dependencies
Source: https://book.fulcrologic.com/index
Commands to create the basic project directory structure, initialize a new npm project, and install essential JavaScript dependencies like `shadow-cljs`, `react`, and `react-dom`.
```bash
mkdir app
cd app
mkdir -p src/main src/dev resources/public
npm init
npm install shadow-cljs react react-dom --save
```
--------------------------------
### Querying from a Pre-Selected Starting Entity
Source: https://book.fulcrologic.com/index
This example shows an alternative way to use `fdn/db->tree` by pre-selecting a specific entity from the `sample-db` (e.g., `[:person/id 1]`) and providing it as the `starting-entity`. This allows querying properties relative to that specific entity, demonstrating how Fulcro performs localized refreshes by pulling an entity and then applying its query.
```Clojure
(let [starting-entity (get-in sample-db [:person/id 1])]
(fdn/db->tree [:person/name] starting-entity sample-db))
=> #:person{:name "Bob"}
```
--------------------------------
### Basic Fulcro Server Setup for Server-Side Rendering (Clojure)
Source: https://book.fulcrologic.com/index
This Clojure code defines a basic Fulcro server that supports server-side rendering (SSR) for dynamic routes. It includes functions to build the initial UI tree, perform the actual SSR into an HTML string, and configure the `make-fulcro-server` with extra routes using Ring responses. Critical steps for installing and routing to dynamic components are highlighted.
```Clojure
(ns ssr-dynamic-routing.server
(:require
[ring.util.response :as resp]
[fulcro.easy-server :refer [make-fulcro-server]]
[fulcro.server-render :as ssr]
[ssr-dynamic-routing.ui.other :as other]
[ssr-dynamic-routing.ui.root :as root]
[com.fulcrologic.fulcro.dom-server :as dom]
[com.fulcrologic.fulcro.components :as comp]
[com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
[com.fulcrologic.fulcro.routing.legacy-ui-routers :as r]))
(defn build-ui-tree [match]
(let [client-db (ssr/build-initial-state (comp/get-initial-state root/Root {}) root/Root)
final-db (-> client-db
;; CRITICAL: Install the routes, or their state won't be in the db
(r/install-route* :main root/Main)
(r/install-route* :other other/Other)
(r/route-to* match))]
;; CRITICAL: Pass the final database to get-query!!! Or you won't get the updated dynamic query
(fdn/db->tree (comp/get-query root/Root final-db) final-db final-db)))
(defn server-side-render [env {:keys [handler] :as match}]
(let [ui-tree (build-ui-tree match)
html (dom/render-to-str ((comp/factory root/Root) ui-tree))]
(-> (resp/response (str
"
"
html
"
"))
(resp/content-type "text/html"))))
(defn build-server
[{:keys [config] :or {config "config/dev.edn"}}]
(make-fulcro-server
:parser-injections #{:config}
;; Quick way to hack a couple of pages in with different match handlers
:extra-routes {:routes ["/" {"main.html" :main
"other.html" :other}]
:handlers {:main server-side-render
:other server-side-render}}
:config-path config))
```
--------------------------------
### APIDOC: uism/begin! - Start a Fulcro UI State Machine Instance
Source: https://book.fulcrologic.com/index
Documents the `uism/begin!` function used to initialize and start a UI state machine instance in Fulcro. It explains parameters like `app-or-component`, `machine-def`, `instance-id`, and `actor-map`, and notes its transactional nature and storage mechanism.
```APIDOC
(uism/begin! app-or-component machine-def instance-id actor-map)
app-or-component: The Fulcro application or component context.
machine-def: The definition of the state machine.
instance-id: A unique keyword identifier for the state machine instance.
actor-map: A map defining UI components (actors) the state machine interacts with.
```
--------------------------------
### Complete Fulcro UI Routing and Data Loading Example
Source: https://book.fulcrologic.com/index
A comprehensive Fulcro example showcasing client-side UI routing and server-side data resolution. It includes namespace declarations, a Pathom resolver for settings data, and client-side components (`SomeSetting`, `SettingsTab`, `MainTab`, `UITabs`) demonstrating how data is fetched and displayed based on UI navigation.
```Clojure
(ns book.demos.loading-in-response-to-UI-routing
(:require
[com.fulcrologic.fulcro.routing.legacy-ui-routers :as r]
[com.fulcrologic.fulcro.mutations :as m]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.data-fetch :as df]
[com.wsscode.pathom.connect :as pc]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(pc/defresolver all-settings-resolver [env input]
{::pc/output [{::all-settings [:id :value]}]}
{::all-settings [{:id 1 :value "Gorgon"}
{:id 2 :value "Thraser"}
{:id 3 :value "Under"}]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defsc SomeSetting [this {:keys [id value]}]
{:query [:ui/fetch-state :id :value]
:ident [:setting/by-id :id]}
(dom/p nil "Setting " id " from server has value: " value))
(def ui-setting (comp/factory SomeSetting {:keyfn :id}))
(defsc SettingsTab [this {:keys [settings-content settings]}]
{:initial-state {:kind :settings
:settings-content "Settings Tab"
:settings []}
; This query 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)
:query [:kind :settings-content {:settings (comp/get-query SomeSetting)}]}
(dom/div nil
settings-content
(if (seq settings)
(mapv ui-setting settings)
(dom/div "No settings."))))
(defsc MainTab [this {:keys [main-content]}]
{:initial-state {:kind :main :main-content "Main Tab"}
:query [:kind :main-content]}
(dom/div nil main-content))
(r/defsc-router UITabs [this props]
{:router-id :ui-router
:ident (fn [] [(:kind props) :tab])
:default-route MainTab
:router-targets {:main MainTab
})
```
--------------------------------
### Complete Fulcro Application with Single Counter Pre-merge Example
Source: https://book.fulcrologic.com/index
A comprehensive example demonstrating a Fulcro application with both server-side (Pathom resolver) and client-side components. It showcases how a single counter component uses the `:pre-merge` hook for data initialization and how it integrates with data loading (`df/load!`) and state management.
```Clojure
(ns book.demos.pre-merge.countdown
(:require
[com.fulcrologic.fulcro.data-fetch :as df]
[book.demos.util :refer [now]]
[com.fulcrologic.fulcro.mutations :as m]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.wsscode.pathom.connect :as pc]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def all-counters
[{::counter-id 1 ::counter-label "A"}])
(pc/defresolver counter-resolver [env {::keys [counter-id]}]
{::pc/input #{::counter-id}
::pc/output [::counter-id ::counter-label]}
(let [{:keys [id]} (-> env :ast :params)]
(first (filter #(= id (::counter-id %)) all-counters))))
(def resolvers [counter-resolver])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defsc Countdown [this {::keys [counter-label]
:ui/keys [count]}]
{:ident [::counter-id ::counter-id]
:query [::counter-id ::counter-label :ui/count]
:pre-merge (fn [{:keys [current-normalized data-tree]}]
(merge
{:ui/count 5}
current-normalized
data-tree))}
(dom/div
(dom/h4 counter-label)
(let [done? (zero? count)]
(dom/button {:disabled done?
:onClick #(m/set-value! this :ui/count (dec count))}
(if done? "Done!" (str count))))))
(def ui-countdown (comp/factory Countdown {:keyfn ::counter-id}))
(defsc Root [this {:keys [counter]}]
{:initial-state (fn [_] {})
:query [{:counter (comp/get-query Countdown)}]}
(dom/div
(dom/h3 "Counters")
(if (seq counter)
(ui-countdown counter)
(dom/button {:onClick #(df/load! this [::counter-id 1] Countdown {:target [:counter]})}
"Load one counter"))))
(defn initialize
"To be used in :started-callback to pre-load things."
[app])
```
--------------------------------
### Install and Navigate Dynamic Routes in Fulcro 3
Source: https://book.fulcrologic.com/index
This snippet demonstrates how to dynamically install routes for a Fulcro 3 application and then immediately navigate to a specific route. It utilizes `comp/transact!` to perform the operations, registering `:new-user` and `:login` routes with their respective components and then routing to the `:login` page.
```Clojure
(comp/transact! app [(r/install-route {:target-kw :new-user :component NewUser})
(r/install-route {:target-kw :login :component Login})
(r/route-to {:handler :login})])
```
--------------------------------
### Fulcro Network Activity Simulation and UI Example
Source: https://book.fulcrologic.com/index
A comprehensive Clojure example demonstrating Fulcro's network activity tracking. It includes a simulated Pathom resolver, an `ActivityIndicator` component to display active remotes, and a `Root` component that triggers a data load, allowing observation of the network status in real-time.
```Clojure
(ns book.server.network-activity
(:require
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
[com.wsscode.pathom.connect :as pc]
[taoensso.timbre :as log]
[clojure.pprint :refer [pprint]]
[com.fulcrologic.fulcro.data-fetch :as df]))
;; Simulated server
(pc/defresolver silly-resolver [_ _]
{::pc/output [::data]}
{::data 42})
;; Client
(defsc ActivityIndicator [this props]
{:query [[::app/active-remotes '_]]
:ident (fn [] [:component/id ::activity])
:initial-state {}}
(let [active-remotes (::app/active-remotes props)]
(dom/div
(dom/h3 "Active Remotes")
(dom/pre (pr-str active-remotes)))))
(def ui-activity-indicator (comp/factory ActivityIndicator {:keyfn :id}))
(defsc Root [this {:keys [indicator]}]
{:query [{:indicator (comp/get-query ActivityIndicator)}]
:initial-state {:indicator {}}}
(dom/div {}
(dom/p {} "Use the server controls to slow down the network, so you can see the activity")
(dom/button {:onClick #(df/load! this ::data nil)} "Trigger a Load")
(ui-activity-indicator indicator)))
```
--------------------------------
### Fulcro Pathom Mock Server and API Definitions
Source: https://book.fulcrologic.com/index
This code defines a mock server-side database and Pathom Connect resolvers and mutations for a login system. It includes `account-resolver` to fetch account details, `session-resolver` to get the current session, `server-login` to authenticate users, and `server-logout` to clear the session. This setup simulates backend API interactions directly within the client for demonstration purposes.
```Clojure
(ns book.raw.raw-uism
(:require
[com.fulcrologic.fulcro.algorithms.data-targeting :as dt]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom :refer [div p input button h2 label]]
[com.fulcrologic.fulcro.dom.events :as evt]
[com.fulcrologic.fulcro.mutations :as m]
[com.fulcrologic.fulcro.raw.components :as rc]
[com.fulcrologic.fulcro.react.hooks :as hooks]
[com.fulcrologic.fulcro.ui-state-machines :as uism]
[com.wsscode.pathom.connect :as pc]
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Mock Server and database, in Fulcro client format for ease of use in demo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defonce pretend-server-database
(atom
{:account/id {1000 {:account/id 1000
:account/email "bob@example.com"
:account/password "letmein"}}}))
;; For the UISM DEMO
(defonce session-id (atom 1000)) ; pretend like we have server state to remember client
(pc/defresolver account-resolver [_ {:account/keys [id]}]
{::pc/input #{:account/id}
::pc/output [:account/email]}
(select-keys (get-in @pretend-server-database [:account/id id] {}) [:account/email]))
(pc/defresolver session-resolver [_ {:account/keys [id]}]
{::pc/output [{:current-session [:account/id]}]}
(if @session-id
{:current-session {:account/id @session-id}}
{:current-session {:account/id :none}}))
(pc/defmutation server-login [_ {:keys [email password]}]
{::pc/sym `login
::pc/output [:account/id]}
(let [accounts (vals (get @pretend-server-database :account/id))
account (first
(filter
(fn [a] (and (= password (:account/password a)) (= email (:account/email a))))
accounts))]
(when (log/spy :info "Found account" account)
(reset! session-id (:account/id account))
account)))
(pc/defmutation server-logout [_ _]
{::pc/sym `logout}
(reset! session-id nil))
(def resolvers [account-resolver session-resolver server-login server-logout])
```
--------------------------------
### Require Correct Target in Application Entry Points
Source: https://book.fulcrologic.com/index
These examples show how the main entry point for each platform (e.g., `app.browser.main` or `app.native.main`) requires its corresponding target namespace (`app.targets.browser` or `app.targets.native`). This ensures that the correct platform-specific API implementations are initialized and available when the application starts.
```Clojure
(ns app.browser.main
(:require
app.targets.browser))
...
```
```Clojure
(ns app.native.main
(:require
app.targets.native))
...
```
--------------------------------
### Fulcro 3 Autocomplete Root Component Example
Source: https://book.fulcrologic.com/index
Demonstrates how to instantiate and use the `Autocomplete` component within a parent `AutocompleteRoot` component. It defines the initial state and query necessary for embedding the child component, providing a simple example of its usage in a UI.
```ClojureScript
(defsc AutocompleteRoot [this {:keys [airport-input]}]
{:initial-state (fn [p] {:airport-input (comp/get-initial-state Autocomplete {:id :airports})})
:query [{:airport-input (comp/get-query Autocomplete)}]}
(dom/div
(dom/h4 "Airport Autocomplete")
(ui-autocomplete airport-input)))
```
--------------------------------
### Start State Machine with Initial Configuration (Clojure)
Source: https://book.fulcrologic.com/index
Demonstrates how to initiate a state machine using `uism/begin!`, passing an `actor-map` that contains initial configuration data. This data can then be accessed and stored within the state machine's lifecycle.
```Clojure
(uism/begin! this SM ::sm-id actor-map {:other-machine :machine-id})
```
--------------------------------
### LOAD CACHE NOT INSTALLED! Did you remember to use with-load-cache on your app?
Source: https://book.fulcrologic.com/index
-
```APIDOC
LOAD CACHE NOT INSTALLED! Did you remember to use with-load-cache on your app?
```
--------------------------------
### Live Demo of Post Mutations - Morphing Example
Source: https://book.fulcrologic.com/index
A comprehensive example demonstrating data morphing using Fulcro. This snippet includes namespace declarations, component definitions (`CategoryQuery`, `ItemQuery`, `ToolbarItem`, `ToolbarCategory`, `Toolbar`), sample server response, query definitions, and mutation logic to transform loaded data for UI display.
```Clojure
(ns book.server.morphing-example
(:require
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom]
[book.macros :refer [defexample]]
[com.fulcrologic.fulcro.mutations :refer [defmutation]]
[com.fulcrologic.fulcro.algorithms.normalize :as fnorm]
[com.fulcrologic.fulcro.algorithms.merge :as merge]))
(defsc CategoryQuery [this props]
{:query [:db/id :category/name]
:ident [:categories/by-id :db/id]})
(defsc ItemQuery [this props]
{:query [:db/id :item/name {:item/category (comp/get-query CategoryQuery)}]
:ident [:items/by-id :db/id]})
(def sample-server-response
{:all-items [{:db/id 5 :item/name "item-42" :item/category {:db/id 1 :category/name "A"}}
{:db/id 6 :item/name "item-92" :item/category {:db/id 1 :category/name "A"}}
{:db/id 7 :item/name "item-32" :item/category {:db/id 1 :category/name "A"}}
{:db/id 8 :item/name "item-52" :item/category {:db/id 2 :category/name "B"}}]})
(def component-query [{:all-items (comp/get-query ItemQuery)}])
(def hand-written-query [{:all-items [:db/id :item/name
{:item/category [:db/id :category/name]}]}]
(defsc ToolbarItem [this {:keys [item/name]}]
{:query [:db/id :item/name]
:ident [:items/by-id :db/id]}
(dom/li name))
(def ui-toolbar-item (comp/factory ToolbarItem {:keyfn :db/id}))
(defsc ToolbarCategory [this {:keys [category/name category/items]}]
{:query [:db/id :category/name {:category/items (comp/get-query ToolbarItem)}]
:ident [:categories/by-id :db/id]}
(dom/li
name
(dom/ul
(mapv ui-toolbar-item items))))
(def ui-toolbar-category (comp/factory ToolbarCategory {:keyfn :db/id}))
(defmutation group-items-reset [params]
(action [{:keys [app state]}]
(swap! state (fn [s]
(-> s
(dissoc :categories/by-id :toolbar/categories)
(merge/merge* component-query sample-server-response))))))
(defn add-to-category
"Returns a new db with the given item added into that item's category."
[db item]
(let [category-ident (:item/category item)
item-location (conj category-ident :category/items)]
(update-in db item-location (fnil conj []) (comp/ident ItemQuery item))))
(defn group-items*
"Returns a new db with all of the items sorted by name and grouped into their categories."
[db]
(let [sorted-items (->> db :items/by-id vals (sort-by :item/name))
category-ids (-> db (:categories/by-id) keys)
clear-items (fn [db id] (assoc-in db [:categories/by-id id :category/items] []))
db (reduce clear-items db category-ids)
db (reduce add-to-category db sorted-items)
all-categories (->> db :categories/by-id vals (mapv #(comp/ident CategoryQuery %)))]
(assoc db :toolbar/categories all-categories)))
(defmutation ^:intern group-items [params]
(action [{:keys [state]}]
(swap! state group-items*)))
(defsc Toolbar [this {:keys [toolbar/categories]}]
{:query [{:toolbar/categories (comp/get-query ToolbarCategory)}]}
(dom/div
(dom/button {:onClick #(comp/transact! this [(group-items {})])} "Trigger Post Mutation")
(dom/button {:onClick #(comp/transact! this [(group-items-reset {})])} "Reset")
(dom/ul
(mapv ui-toolbar-category categories))))
(defexample "Morphing Data" Toolbar "morphing-example" :initial-db (fnorm/tree->db component-query sample-server-response true))
```
--------------------------------
### Fulcro Simple Data Targeting Example
Source: https://book.fulcrologic.com/index
Demonstrates a simple data targeting mechanism in Fulcro using `df/load!`. This example shows how to load data for `:all-people` and direct the results to a specific path `[:list/id :people :list/people]` in the local UI graph, effectively replacing existing data at that target.
```Clojure
(df/load comp :all-people Person {:target [:list/id :people :list/people]})
```
--------------------------------
### Verify npm Installation and Local Packages
Source: https://book.fulcrologic.com/index
This command lists the npm packages installed in the current directory. It is used to confirm that Node.js and npm are correctly set up, which are required by the `shadow-cljs` compiler for managing JavaScript dependencies in Fulcro projects. An empty output indicates no local packages, which is expected in a fresh directory.
```Shell
$ npm list
/your/directory
└── (empty)
```
--------------------------------
### Fulcro Dynamic Query Example with Toggleable Component State
Source: https://book.fulcrologic.com/index
A comprehensive example demonstrating dynamic queries in Fulcro. It defines a `Leaf` component whose query can be toggled between `[:x]` and `[:y]` at runtime using `comp/set-query!`. The `Root` component initializes and renders the `Leaf`, showcasing how component state adapts based on the active query.
```ClojureScript
(ns book.queries.dynamic-queries
(:require
[com.fulcrologic.fulcro.dom :as dom]
[goog.object]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.mutations :as m]))
(declare ui-leaf)
; This component allows you to toggle the query between [:x] and [:y]
(defsc Leaf [this {:keys [x y]}]
{:initial-state (fn [params] {:x 1 :y 42})
:query (fn [] [:x]) ; avoid error checking so we can destructure both :x and :y in props
:ident (fn [] [:LEAF :ID])} ; there is only one leaf in app state
(dom/div
(dom/button {:onClick (fn [] (comp/set-query! this ui-leaf {:query [:x]}))} "Set query to :x")
(dom/button {:onClick (fn [] (comp/set-query! this ui-leaf {:query [:y]}))} "Set query to :y")
; If the query is [:x] then x will be defined, otherwise it will not.
(dom/button {:onClick (fn [e] (if x
(m/set-value! this :x (inc x))
(m/set-value! this :y (inc y))))}
(str "Count: " (or x y))) ; only one will be defined at a time
" Leaf"))
(def ui-leaf (comp/factory Leaf {:qualifier :x}))
(defsc Root [this {:keys [root/leaf] :as props}]
{:initial-state (fn [p] {:root/leaf (comp/get-initial-state Leaf {})})
:query (fn [] [{:root/leaf (comp/get-query ui-leaf)}])}
(dom/div (ui-leaf leaf)))
```
--------------------------------
### Fulcro 3 Parallel vs. Sequential Loading Example
Source: https://book.fulcrologic.com/index
A comprehensive Clojure/ClojureScript example demonstrating the difference between parallel and sequential network loads in Fulcro 3. It includes a server-side resolver (`long-query-resolver`) and a client-side component (`Child`) with buttons to trigger parallel and sequential `load-field!` calls, illustrating the impact on loading times and the use of markers for loading status.
```Clojure
(ns book.demos.parallel-vs-sequential-loading
(:require
[com.fulcrologic.fulcro.data-fetch :as df]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.wsscode.pathom.connect :as pc]
[taoensso.timbre :as log]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SERVER:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(pc/defresolver long-query-resolver [_ _]
{::pc/output [:background/long-query]}
{:background/long-query 42})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CLIENT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defsc Child [this {:keys [id name background/long-query] :as props}]
{:query (fn [] [:id :name :background/long-query [df/marker-table '_]])
:ident [:background.child/by-id :id]}
(let [status (get-in props [df/marker-table [:fetching id]])]
(dom/div {:style {:display "inline" :float "left" :width "200px"}}
(dom/button {:onClick #(df/load-field! this :background/long-query {:parallel true
:marker [:fetching id]})} "Load stuff parallel")
(dom/button {:onClick #(df/load-field! this :background/long-query {:marker [:fetching id]})} "Load stuff sequential")
(dom/div
name
(if (df/loading? status)
(dom/span "Loading...")
(dom/span long-query))))))
(def ui-child (comp/factory Child {:keyfn :id}))
(defsc Root [this {:keys [children] :as props}]
; cheating a little...raw props used for child, instead of embedding them there.
{:initial-state (fn [params] {:children [{:id 1 :name "A"} {:id 2 :name "B"} {:id 3 :name "C"}]})
:query [{:children (comp/get-query Child)}]}
(dom/div
(mapv ui-child children)
(dom/br {:style {:clear "both"}}) (dom/br)))
```
--------------------------------
### Fulcro Data Loading with `df/load!`
Source: https://book.fulcrologic.com/index
Illustrates how to use `data-fetch/load` in Fulcro to query the server for data. This specific example targets the `:all-numbers` query and places the result into a specific path within the application's state tree.
```Clojure
(df/load! app :all-numbers PhoneDisplayRow {:target [:screen/phone-list :tab :phone-numbers]})
```
--------------------------------
### Start shadow-cljs Development Server for Fulcro 3
Source: https://book.fulcrologic.com/index
Command to launch the shadow-cljs development server. This server provides a web-based user interface for managing builds, enabling live compilation, and serving the application's static files during development.
```Shell
$ npx shadow-cljs server
```
--------------------------------
### Programmatic Route Change with Dynamic Router
Source: https://book.fulcrologic.com/index
A simple example demonstrating how to programmatically change the application's route using Fulcro's dynamic router.
```Clojure
(dr/change-route app ["main"]))
```
--------------------------------
### Full Example: Normalizing Tree Data to Fulcro Database
Source: https://book.fulcrologic.com/index
This comprehensive Fulcro example demonstrates how to define interconnected components with `ident` and `query` definitions. It includes mutations to normalize a tree-structured data (`:from`) into the Fulcro application state database using `fnorm/tree->db` and displays the normalized result, showcasing a common data transformation pattern.
```Clojure
(ns book.tree-to-db
(:require
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom]
[devcards.util.edn-renderer :refer [html-edn]]
[com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
[com.fulcrologic.fulcro.algorithms.normalize :as fnorm]))
(defsc SubQuery [t p]
{:ident [:sub/by-id :id]
:query [:id :data]})
(defsc TopQuery [t p]
{:ident [:top/by-id :id]
:query [:id {:subs (comp/get-query SubQuery)}]})
(defmutation normalize-from-to-result [ignored-params]
(action [{:keys [state]}]
(let [result (fnorm/tree->db TopQuery (:from @state) true)]
(swap! state assoc :result result))))
(defmutation reset [ignored-params] (action [{:keys [state]}] (swap! state dissoc :result)))
(defsc Root [this {:keys [from result]}]
{:query [:from :result]
:initial-state (fn [params]
; some data we're just shoving into the database from root...***not normalized***
{:from {:id :top-1 :subs [{:id :sub-1 :data 1} {:id :sub-2 :data 2}]}})}
(dom/div
(dom/div
(dom/h4 "Pretend Incoming Tree")
(html-edn from))
(dom/div
(dom/h4 "Normalized Result (click below to normalize)")
(when result
(html-edn result)))
(dom/button {:onClick (fn [] (comp/transact! this [(normalize-from-to-result {})]))} "Normalized (Run tree->db)")
(dom/button {:onClick (fn [] (comp/transact! this [(reset {})]))} "Clear Result")))
```
--------------------------------
### Complete Fulcro Application with Data Merging and Mutations
Source: https://book.fulcrologic.com/index
A comprehensive example of a Fulcro application demonstrating component definition, state management, data merging using `merge-component!`, and client-side mutations. It includes UI elements for displaying and interacting with counter entities.
```Clojure
(ns book.merge-component
(:require
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
[com.fulcrologic.fulcro.data-fetch :as df]
[com.fulcrologic.fulcro.algorithms.merge :as merge]))
(defsc Counter [this {:keys [counter/id counter/n] :as props} {:keys [onClick] :as computed}]
{:query [:counter/id :counter/n]
:ident [:counter/by-id :counter/id]}
(dom/div :.counter
(dom/span :.counter-label
(str "Current count for counter " id ": "))
(dom/span :.counter-value n)
(dom/button {:onClick #(onClick id)} "Increment")))
(def ui-counter (comp/factory Counter {:keyfn :counter/id}))
; the * suffix is just a notation to indicate an implementation of something..in this case the guts of a mutation
(defn increment-counter*
"Increment a counter with ID counter-id in a Fulcro database."
[database counter-id]
(update-in database [:counter/by-id counter-id :counter/n] inc))
(defmutation increment-counter [{:keys [id] :as params}]
; The local thing to do
(action [{:keys [state] :as env}]
(swap! state increment-counter* id))
; The remote thing to do. True means "the same (abstract) thing". False (or omitting it) means "nothing"
(remote [env] true))
(defsc CounterPanel [this {:keys [counters]}]
{:initial-state (fn [params] {:counters []})
:query [{:counters (comp/get-query Counter)}]
:ident (fn [] [:panels/by-kw :counter])}
(let [click-callback (fn [id] (comp/transact! this
[(increment-counter {:id id}) :counter/by-id]))]
(dom/div
; embedded style: kind of silly in a real app, but doable
(dom/style ".counter { width: 400px; padding-bottom: 20px; }
button { margin-left: 10px; }")
; computed lets us pass calculated data to our component's 3rd argument. It has to be
; combined into a single argument or the factory would not be React-compatible (not would it be able to handle
; children).
(mapv #(ui-counter (comp/computed % {:onClick click-callback})) counters))))
(def ui-counter-panel (comp/factory CounterPanel))
(defonce timer-id (atom 0))
(declare sample-of-counter-app-with-merge-component-fulcro-app)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The code of interest...
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn add-counter
"NOTE: A function callable from anywhere as long as you have a reconciler..."
[app counter]
(merge/merge-component! app Counter counter
:append [:panels/by-kw :counter :counters]))
(defsc Root [this {:keys [panel]}]
{:query [{:panel (comp/get-query CounterPanel)}]
:initial-state {:panel {}}}
(dom/div {:style {:border "1px solid black"}}
; NOTE: A plain function...pretend this is happening outside of the UI...we're doing it here so we can embed it in the book...
(dom/button {:onClick #(add-counter (comp/any->app this) {:counter/id 4 :counter/n 22})} "Simulate Data Import")
(dom/hr)
"Counters:"
(ui-counter-panel panel)))
```
--------------------------------
### Denormalize Full Database with EQL Query
Source: https://book.fulcrologic.com/index
This example demonstrates using `fdn/db->tree` to denormalize the `sample-db` from its root. It queries for `:people` and their `:person/name`, showing how idents are resolved into a tree structure.
```Clojure
(let [starting-node sample-db]
(fdn/db->tree [{:people [:person/name]}] starting-node sample-db))
=> {:people [#:person{:name "Bob"} #:person{:name "Judy"}]}
```
--------------------------------
### Defining Fulcro Components and Routers for IDE-Navigable Paths
Source: https://book.fulcrologic.com/index
Illustrates how to structure Fulcro components with `:route-segment` metadata and define `defrouter` instances. This setup enables the use of component references for routing, improving code readability and IDE navigation.
```Clojure
(ns com.example.root)
(defsc MyIndex [_ _]
{:route-segment ["root"]}
;; composes in settings router
...)
(defrouter RootRouter [this props] {:router-targets [MyIndex]})
...
(ns com.example.settings)
(defsc Settings [_ _]
{:route-segment ["settings"]}
; composes in SettingPanesRouter
...)
(defrouter SettingsRouter [this props] {:router-targets [Settings]})
...
(ns com.example.settings-pane)
(defsc UserPane [_ _]
{:route-segment ["user" :user-id]} ...)
(defrouter SettingPanesRouter [this props] {:router-targets [UserPane]})
```
--------------------------------
### Define Server-Side Pathom Mutation
Source: https://book.fulcrologic.com/index
An example of a server-side mutation defined using Pathom's `pc/defmutation` in Clojure. The `env` parameter provides context that can be augmented during parser setup, such as database connections.
```Clojure
(ns my-app.mutations
(:require
[com.wsscode.pathom.connect :as pc]))
;; `env` can be augmented in parser setup to include things like database connection, etc.
(pc/defmutation do-something [env params]
{}
...))
```
--------------------------------
### Fulcro Startup: Initial State and Keyframe Render
Source: https://book.fulcrologic.com/index
Demonstrates the initial state setup and the first keyframe render process in Fulcro. It copies the initial component tree state, normalizes it using the application's query, and performs a single keyframe render to display the initial DOM.
```Clojure
(let [tree (get-initial-state Root {})
Q (get-query Root)
database (tree->db Q tree)]
(reset! state-atom database))
(let [Q (get-query Root)
tree (db->tree @state-atom Q)]
(react-render Root tree))
```
--------------------------------
### Initialize Fulcro Application and Set Initial Route Early
Source: https://book.fulcrologic.com/index
This snippet demonstrates a recommended approach for starting a Fulcro application. It initializes the application (`fulcro-app`), sets the root component, immediately calls `dr/change-route!` to establish an initial route, and then mounts the application, ensuring a consistent user experience by setting up routing before rendering.
```Clojure
(defonce SPA (app/fulcro-app ...))
(defn start []
(app/set-root! SPA ui/Root {:initialize-state? true})
(dr/change-route! SPA ["landing-page"])
(app/mount! app ui/Root "app" {:initialize-state? false}))
```
--------------------------------
### Build and Evolve Initial Application State on Server
Source: https://book.fulcrologic.com/index
Demonstrates how to use `build-initial-state` from `com.fulcrologic.fulcro.algorithms.server-render` to create a normalized client application database on the server. It then shows how to evolve this state using custom mutation implementations and `assoc` to incorporate dynamic data like user information, preparing it for server-side rendering.
```Clojure
(let [base-state (ssr/build-initial-state (my-app/get-initial-state) my-app/Root)
user (get-current-user (:session req))
user-ident (comp/get-ident my-app/User user)]
(-> base-state
(todo-check-item-impl* 3 true) ; some combo of mutation impls
(assoc :current-user user-ident) ; put normalized user into root
(assoc-in user-ident user)))
```
--------------------------------
### Fulcro Mutation Progress Update Example
Source: https://book.fulcrologic.com/index
An example of a Fulcro mutation that updates a component's UI with overall network progress using `http-remote/overall-progress` within its `progress-action`.
```Clojure
(defmutation large-mutation [_]
(progress-action [{:keys [state] :as env}]
(swap! state assoc-in [:component/id :thing :ui/progress] (http-remote/overall-progress env)))
(remote [_] true))
```
--------------------------------
### Example Incoming Data Tree
Source: https://book.fulcrologic.com/index
An illustrative example of a data tree structure that Fulcro might receive, typically from a server. This tree contains a collection of 'people' entities, each with a unique database ID and other attributes.
```Clojure
{ :people [ {:db/id 1 :person/name "Joe" ...} {:db/id 2 ...} ... ] }
```
--------------------------------
### Creating a Fulcro Server System with Configuration Path
Source: https://book.fulcrologic.com/index
Demonstrates how to define a `make-system` function that initializes a Fulcro server using `server/make-fulcro-server`. It explicitly sets the `:config-path` to a specific EDN file for server configuration. This snippet shows the basic structure for integrating server setup with configuration loading.
```Clojure
(defn make-system []
(server/make-fulcro-server
:config-path "/usr/local/etc/app.edn"
...
```
--------------------------------
### Initialize Pre-loaded Routes in Fulcro Application
Source: https://book.fulcrologic.com/index
This ClojureScript code demonstrates how to set up pre-loaded routes (`:new-user`, `:login`) using `comp/transact!` and `r/install-route` within the `application-loaded!` function. This function is invoked via `:client-did-mount` during application startup to ensure routes are available immediately. It also includes a call to `loader/set-loaded!` for the entry point.
```ClojureScript
(defn application-loaded! [app]
; Let the dynamic router know that two of the routes are already loaded.
(comp/transact! app `[(r/install-route {:target-kw :new-user :component ~NewUser})
(r/install-route {:target-kw :login :component ~Login})
(r/route-to {:handler :login})])
; Clojurescript requires you call this on every successfully "loaded" module:
(loader/set-loaded! :entry-point))
...
(make-fulcro-client Root {:client-did-mount application-loaded!})
```
--------------------------------
### Initialize Fulcro App and Inspect Normalized State
Source: https://book.fulcrologic.com/index
This Clojure snippet demonstrates the foundational steps for setting up a Fulcro application. It defines two components, `Person` and `Root`, specifying their queries and initial states. The application is then mounted in a headless environment, and its internal normalized state is inspected, showcasing how Fulcro structures data with references and tables.
```Clojure
(defsc Person [this props]
{:query [:person/id :person/name]
:ident :person/id
:initial-state (fn [params] {:person/id (:id params)
:person/name (:name params)})})
(defsc Root [this props]
{:query [{:root/people (comp/get-query Person)}]
:initial-state (fn [_] {:root/people [(comp/get-initial-state Person {:id 1 :name "Bob"})
(comp/get-initial-state Person {:id 2 :name "Judy"})]})})
(def app (app/fulcro-app))
(app/mount! app Root :headless)
(app/current-state app)
=>
{:fulcro.inspect.core/app-id "user/Root",
:root/people [[:person/id 1] [:person/id 2]],
:person/id {1 #:person{:id 1, :name "Bob"}, 2 #:person{:id 2, :name "Judy"}}}
```
--------------------------------
### Query Specific Table with db→tree
Source: https://book.fulcrologic.com/index
This example uses `db→tree` to query the `:person/id` table directly. Similar to the previous example, it shows that without a join specified in the EQL, the returned data for the table will contain unresolved idents.
```Clojure
(let [starting-node sample-db]
(fdn/db->tree [:person/id] starting-node sample-db))
=> #:person{:id {1 #:person{:name "Bob"}, 2 #:person{:name "Judy"}}}
```
--------------------------------
### Defining Fulcro UI Components and App Initialization
Source: https://book.fulcrologic.com/index
This snippet demonstrates how to define Fulcro UI components (`LargeList`, `Root`) and an `initialize` function for application startup. It shows the use of `dom` for UI elements, `comp/factory` for component creation, `comp/get-initial-state` and `comp/get-query` for state and query definitions, and `comp/transact!` for triggering state changes and page navigation.
```Clojure
(dom/button {:onClick #(comp/transact! this [(goto-page {:page-number (inc number)})])} "Next Page")
(ui-list-page current-page))))
(def ui-list (comp/factory LargeList))
(defsc Root [this {:keys [pagination/list]}]
{:initial-state (fn [params] {:pagination/list (comp/get-initial-state LargeList {})})
:query [{:pagination/list (comp/get-query LargeList)}]}
(dom/div (ui-list list)))
(defn initialize
"To be used as started-callback. Load the first page."
[{:keys [app]}]
(comp/transact! app [(goto-page {:page-number 1})]))
```