By now, the brief introduction to Fulcro should have been presented to you. Keep the document at hand because you might want to refer to it again during the workshop.
-
DIY app exercises 1 - 9 (stop latest 10 min before the workshop’s end)
-
Review Summary - lessons learned
-
Q&A
-
Coding exercises - if time permits or do them at home after the workshop
-
Learn more about Fulcro
These interactive exercises introduce you to Fulcro concepts and the way of working with a Fulcro application
Start by opening the application in the browser and opening the browser Dev Tools.
Make sure that Calva is connected to the REPL, with the server started, as described in Readme: Running the app from VS Code with Calva.
Tip
|
Read this rendered, for example by looking at it in the browser: https://github.com/holyjak/fulcro-intro-wshop/blob/main/docs/Workshop.adoc |
It is best to lay out your windows so that you can always see the exercises and then either the webapp or Calva (except of the last exercise, where we need both Calva and the webapp). This layout works best for me:
Left window | Right window |
---|---|
Calva |
Webapp tab + Shadow-Cljs tab |
BEWARE: Open the browser Dev Tools after the application loaded - otherwise the React and Fulcro Inspect tabs might not show up there.
(Online workshops: You do not need to see the Zoom window during the exercises.)
Note: You have about 5 - 10 minutes per exercise.
Take good time to read and understand the data / code you are looking at.
-
Open the React dev tools' Components tab
-
Explore the tree of the components displayed
-
Select one of the
TodoItem
elements - can you see its props on the right side (or at the bottom, on a smaller screen)? (Answer: yes and no :-))
We see that the components are nested in each other, creating a tree (with a single "branch" in this case):
The following code is a simplified version of the code defining the UI:
(defsc TodoItem [_ props]
{...}
(dom/li (:item/label props)))
(defsc TodoList [_ props]
{...}
(dom/ul
(map #((com/factory TodoItem) %) ; (1)
(:list/items props))))
(defsc Root [_ props]
{...}
(dom/div ((comp/factory TodoList) (:root/current-list props))))
-
React requires us to turn a component class into an element using a factory before it can be rendered
LESSON: The UI is indeed a tree of components: Root > TodoList > TodoItem
.
React dev tools cannot show us the props of the component because it is a ClojureScript data structure. Let’s have a look at them using Fulcro Inspect!
-
Open now the Fulcro Inspect tab and its Element sub-tab.
-
Click the Pick Element in the top left corner and then click "Item 1" in the webapp
-
Beware: The element tab is showing a snapshot of the Fulcro component data how it was at the time when you picked it
-
-
You should now see the details of the TodoItem, including
-
its "ident", meaning "identifier", namely
[:item/id 1]
-
its props, including
:item/complete
and:item/label
(click the ▶ to expand them) -
its query - notice how the props mostly mirror the query
-
-
Mark "Item 1" as complete by clicking to the left of its label and change its text by double-clicking on it and typing something then clicking outside of the list. Pick the element again. How have the props changed?
-
Notice that for most purposes, having a prop with the value
false
and not having the prop at all are equivalent
-
-
Pick another list item element and compare its values with the original item
-
Select the whole
TodoList
and look at its ident, props, and query. Does it relate in any way to theTodoItem
's query and props? How?
LESSON: We have learned to use Fulcro Inspect - Element to explore a component and we have learned that it has props, ident, and a query. We saw that the props mirror what is specified in the query.
The query of a component declares its data needs and Fulcro uses it to build the props for the component. We have seen it using Fulcro Inspect - Element, now we will explore it using the REPL and Shadow Inspect.
-
Open
src/fulcro_todomvc/ui.cljs
, scroll to the bottom and inside the(comment …)
find Exercise 3.1 and evaluate theget-query
form (Alt - ENTER
orOption - ENTER
from the essential few Calva keybindings), read the resultNoteThe evaluation result is displayed both inline and inside the output.calva-repl
file (Go - Go to File… to reopen it) -
Now let’s look at what the props look like - at the same place, under Exercise 3.2, eval the
(→ … (comp/props) ..))
form (put your cursor on it andAlt - Enter
).-
Note: We used
tap>
so the data will not show up in the REPL but in a tap client. So:-
open the Shadow-cljs Inspect Latest in a new tab of the same browser window where the webapp is (Gitpod: Duplicate the webapp tab and replace 8181 in the url with 9630 to access Shadow Inspect.)
-
if this displays an empty window, go to http://localhost:9630/ and click the Inspect Latest tab there
-
-
eval the form again; the value should appear in Shadow Inspect
-
click Pretty-Print at the very bottom of Inspect to make it readable
-
-
Compare the props and the query (and notice it is the same thing we saw in the Element tab)
-
Notice some attributes from the query are missing from the props - that’s because client DB had no value for them (which is fine, since in Clojure
nil
is equivalent tofalse
)
-
-
Modify the Exercise 3.2 code to get the props of
TodoList
; for that you will need its ident - remember you can find it using the Fulcro Inspect - Element tab -
Modify also the Exercise 3.1 code to get the query of
TodoList
, then:We see that
TodoList
queries for these props (some omitted):[:list/id :list/items :list/title :list/filter]
.But it also elaborates what it wants for each of the
:list/items
elements, namely[:item/id :item/label :item/complete …]
. How does it do that? By combining the two using a map, to produce what we call a join:; :list/items + [:item/id :item/label ...] => {:list/items [:item/id :item/label :item/complete :ui/editing :ui/edit-text]} ; NOTE: #list{:items ...} == {:list/items ...}
and includes this in its query instead of just
:list/items
. Let’s visualize how the query composes all the way to the Root:Now look at the code to see how the child’s query is included - it is not simply pasted in the parent, it is included via
(comp/get-query TodoItem)
. This is important because it brings with it some important metadata. Let’s have a look at it: -
In
ui.cljs
, under Exercise 3.5, evaluate the form(binding …)
and explore the output in Shadow Inspect (no Pretty-Print available :( )-
The output is a string and thus displayed in a single line, making it hard to read. Fortunately you only need to notice the metadata maps
^{…}
preceding some query vectors
-
EQL Primer: An EQL query includes 1) properties (a.k.a. attributes) such as :list/label
, 2) joins of the form {<property or ident> <query>}
, 3) idents such as [:item/id 1]
to ask for the data of the entity with that ident (and we can again use a join to precise what data).
LESSON: Components declare their data needs using :query
, listing the properties they want. They join in the query of each child using get-query
to include the child’s needs and thus to specify what properties of a nested data entity to include. The query also includes metadata that Fulcro needs for its processing.
LESSON 2: This demonstrates locality - only a component knows what data it needs and it declares next to its body. It also demonstrates the simplicity of only caring about the Root - queries are propagated up so that Root’s query will get data for the whole page.
Despite a common misconception, Fulcro does not supply props to every and each component individually. It only supplies props to the root component - and it uses only the root query, which composes the queries of its children and so on, as we have seen. This picture from the introduction illustrates that:
Let’s have a look at the query and how it is turned into a props tree.
-
Open
src/fulcro_todomvc/ui.cljs
, scroll to the bottom and inside the(comment …)
evaluate theget-query Root
form marked Exercise 4.1 -
It is little long and hard to read so open the Shadow-cljs Inspect Latest in a new tab of the same browser and then evaluate the
(tap> …)
form marked Exercise 4.2. The value should appear in the Shadow Inspect; at the very bottom, click Pretty-Print -
Now let’s see how Fulcro fulfills that query from the client DB, using
fdn/db→tree
- evaluate the form marked Exercise 4.3 and observe the result in Shadow’s Inspect Latest. First use Pretty-Print on it then switch to Browse - you can click on any line to "drill in" and you can use the<
and<<
to go (all the way) back
LESSON: The Root query is turned to a props tree using the client DB. Shadow Inspect is a fine tool for looking at complex data.
LESSON 2: The simplicity of the functional design where UI represented by Root is a "pure" function of data. Data in the UI only comes from what you give to the Root ⇒ easy to inspect, easy to understand. No messy side-band data retrieval by components.
This is the Root query:
[{:root/current-list
[:list/id
:ui/new-item-text
{:list/items
[:item/id :item/label :item/complete :ui/editing :ui/edit-text]}
:list/title
:list/filter]}]
We will use it to navigate the client DB to see how Fulcro builds the props tree:
-
Switch to Fulcro Inspect - DB Explorer
-
At the very bottom, under Top-Level Keys (which are all the keys in the DB that are not "entity tables", such as
:item/id
), there is:root/current-list
- which is also the beginning of the Root’s query. Click on its value to "drill down". You will see the list 1 data map, displayed as table, with properties on the left and values on the right. Compare it to the query (shown above) then drill down to one of the items. (Notice that an item is not included in a list but referred to from the list using its ident.) -
Open Fulcro Inspect - DB and click on the little triangle ▶ to expand the
:list/id
and:item/id
"tables". Now we can see the same data as before, but all at once. -
Let’s see now how the raw data of the client DB looks like. Go to
ui.cljs
and evaluate the Exercise 5.4 form, then switch to Shadow - Inspect Latest to look at it and Pretty-Print it.-
Notice that you are looking at the very same data that you can see in Fulcro Inspect - DB
-
Pay attention to the values of
:list/items
- it is not the attribute maps but just idents of the included items
-
LESSON: We saw how data is stored in the client DB mostly in a normalized form (<entity name>/id → <id value> → <map of props, where some props contain idents, to link to other entities>
) and how idents are used to link entities together. We have experienced how Fulcro fulfills a query by "walking" the client DB. We have seen that the client DB is nothing else than a map (of maps of maps, mostly). Tip: my short screencast Fulcro explained: Turning Root query into props tree also demonstrates how the the query and client DB work in tandem to produce props for the Root component.
LESSON 2: Normalized data cache means it is easy to find any piece of data & a change will affect all places that use it.
Where does the current-list list data come from? From the server, as we will see. But first, let’s review what the frontend part of a Fulcro application looks like:
Remember that any "request for a change" passes through the "asynchronous" transaction subsystem (marked Tx) and that this includes both queries - i.e. data loads - and mutations. Notice also that EQL queries are used both for getting the props tree from the Client DB (not shown in the figure) and for loading data from the backend into the Client DB. Let’s see how the loading looks in practice.
-
Make sure Fulcro Inspect is opened and reload the page
-
In Fulcro Inspect, open the Transactions tab. You should see a single transaction1 there, a LOAD, and its EQL query. Explore the query.
1) Tip: If you do not see the transaction then try to hard-reload the browser tab. (This happens if you opened Fulcro Inspect after the data has already been loaded.)-
How does the LOAD query differ from Root query? Are they same? No? Why?
-
Transactions shows side-effects executed in the frontend, i.e. data loads and other Fulcro mutations
-
-
Click on the transaction to see its details. Notice that the transaction is simply data, of the form
(<fully qualified symbol> {options…})
. Let’s explore the details of its query.
Note: Sadly, the Diff added / Diff removed does not work in the current version of Fulcro Inspect, so ignore it. -
Open Fulcro Inspect’s Network tab (not the browser’s!). You should see a single Request with the load’s query. Click it to see its details and the Response and notice how the response data tree mirrors the EQL query.
-
Now click the [Send to query] button next to the Request in the details, which will send you to the EQL tab. Once there, press also the [(Re)load Pathom Index] button to the very right (for autocompletion) and then the [Run EQL] button next to it. Here we can play with EQL queries that the backend can answer.
-
Play with the query. Delete some attributes (keywords), add a made-up attribute and run it again. Replace the
{:list/items …}
join with just the plain:list/items
attribute and re-run. (Note: The data is returned primarily by thelist-resolver
.)-
As you have noticed, Pathom by default returns all attributes of a nested entity if you do not declare what you want using a join
-
-
Review the EQL Primer above
-
Replace the whole query with the ident query
[[:item/id 1]]
. What do you get back?-
Notice this case differs from 6. above because we do not query for an attribute but for an ident
-
-
Change the plain ident query to a join, to specify the details of what you want from the item, run. (See 1 below for help.)
-
[Optional] Open the Index Explorer tab and click its [Load index] button. Then collapse the ▼ Attributes section so that you will see the Resolvers and click e.g. the
…/list-resolver
. Here you can see all the resolvers defined on the server, what input they need, and what attributes they can output.
1) The query thus becomes [ {[:item/id 1] [<some item attributes…>]} ]
LESSON: EQL queries are used both to turn data from the Client DB into a props tree and to load data from the backend into the Client DB. All changes in the application, including loading data, pass through the transaction subsystem and thus show up in the Transactions view of Fulcro Inspect. You have further used the Network tab to see both the load request and response. Finally, you have used the EQL tab to experiment with crafting different queries that the server could resolve.
LESSON 2: The backend exposes all data frontend might care about through a single endpoint and the frontend asks for whatever subset of it that it wants. To expose a new piece of data just write a backend function that returns it and add it to the frontend query.
In the previous exercise we have observed the effect of the application loading its data. Now we will trigger the load ourselves from the code, in the same manner that the application does in its startup code.
-
Go to
ui.cljs
and evaluate thedf/load!
form marked Exercise 7.1
(Note: df/load! is just a helper function that eventually transacts a Fulcro mutation (the Fulcro and Pathom concept of a mutation diverge slightly and not all F. mutations are P. mutations but let’s ignore that for now))-
Notice that it only returns a uuid of the submitted transaction, which is executed asynchronously
-
Also notice that we pass it the ident of the thing we want to load and a component - internally, Fulcro will call
(comp/get-query <the component>)
to get the component’s query with its metadata. -
Finally, notice that item 1 in the UI has been updated (thanks to the two lists sharing the item and data normalization)
-
(The loaded list will not be displayed because we do not tell Fulcro to do so. Loading and displaying data are unrelated concerns.)
-
-
Check that you can see the transaction in the Fulcro Inspect Transactions tab and explore the request and response in its Network tab.
-
Notice that the ident and the query of the component that you supplied to
df/load!
have been combined into a join query - which the backend can resolve because it has thelist-resolver
that takes a:list/id
and returns its data -
Notice the shape of the response - it is a tree
-
-
Go to Fulcro Inspect - DB and expand
:list/id
to verify the list2
has been added there. Then expand:item/id
to verify that the item99
has also been added and item1
updated-
Notice the shape of the data in the client DB: is it exactly the same as the response (a single tree) or not?
-
not - Fulcro has placed each data entity into its own "table", i.e. the list into
:list/id
and the item into:item/id
, and the list links to the item using its ident instead of containing it. I.e. Fulcro has normalized the incoming data tree into the Client DB’s "entity tables":That is only possible because both
TodoList
andTodoItem
declare their:ident
and thanks to the metadata linking each (sub)query to the component where it originates:(defsc TodoItem [_ _] {:ident :item/id, :query [:item/id :item/label ...]} ...) (defsc TodoList [_ _] {:ident :list/id :query [:list/id :list/title {:list/items (get-query TodoItem)} ...]} ...)
-
LESSON: Load targets a backend resolver by asking for a data that it can provide and supplying the inputs that it requires. It includes a component’s query to request a particular subset of the available data and uses the component’s (and children’s) ident to normalize the data into the Client DB. Normalized data ensure that a change affects all relevant places.
To change state either in the frontend or in the backend, you "transact" (submit to the transaction subsystem) a mutation. A mutation is just data, namely a list of (<namespaced symbol> {<parameter key-value pairs>})
. You can register a "handler" for that symbol both on the frontend and backend using the Fulcro/Pathom defmutation
macros. Let’s trigger a mutation and explore it.
-
Mark "Item 3" as complete by clicking to the left of it. This essentially triggers this DOM event handler (simplified):
:onChange #(comp/transact! this [(fulcro-todomvc.api/todo-check {:id (:item/id props)})])
Note: The
todo-check
call simply returns the whole expression as-is (otherwise your would need to manually type (notice the syntax quote!)`(api/todo-check {:id 1})
) -
Look at the transaction in the Transactions tab and verify it indeed is a list
-
Explore it in Fulcro Inspect’s Network tab
-
Notice the mutation is now wrapped in
[ … ]
to become a complete EQL transaction.
-
-
Read the frontend Fulcro defmutation code in
fulcro-todomvc.api
.-
action
is performed to handle the mutation on the frontend -
(remote [_] true)
instructs Fulcro to also send the mutation to the backend (as we have seen in the Network tab)
-
-
Read the backend Pathom defmutation code in
fulcro-todomvc.server
-
The frontend and backend "handlers" must be registered under the same symbol, which is normally derived from the defmutation name and the current namespace; since here the backend mutation is in a different ns than the frontend one, we override that with the
::pc/sym
config option -
It updates the server-side "db" and returns an empty map (as we have seen in the Network tab)
-
LESSON: A mutation is data that can have a handler associated on the frontend and backend. The frontend handler action updates the Client DB and the mutation is then sent asynchronously to the backend as an EQL transaction iff it is marked as remote. The backend handler gets the same parameters (plus the general pathom environment) and typically updates a data store and possibly returns data.
LESSON 2: Navigability - you can click through to the mutation code for where you "transact" it. Locality - the frontend and backend mutation code can be next to each other. Productivity - adding a new local/remote side-effect is one defmutation away.
As always in Fulcro, we do not need to click around and can trigger the mutation from the REPL.
-
Go to
ui.cljs
and eval the form marked Exercise 9.1 -
Observe that the UI now shows the item as not completed
-
In
api.cljs
, find thetodo-check
mutation (you can Command-click / Control-click the "call" go to its definition) and inside theremote
section change thetrue
tofalse
and save the file (so that Shadow will hot-reload the code) and wait a few seconds. (Perhaps even check browser’s Console for the info logshadow-cljs: load JS fulcro_todomvc/api.cljs
.) -
In
ui.cljs
, eval the form marked Exercise 9.2-
Notice the item is marked as completed in the UI
-
Verify that you see the
todo-check
mutation in the Transactions tab -
Verify that you do not see it in Fulcro Inspect’s Network tab
-
-
(Bonus) At the top left of the DB tab, click on the
<
next to the slider a few times - notice the item’s attributes in the client DB revert to the previous state and you can use the circular arrow icon to reset the app and UI to that state (which create a new revision of the state). This way you can go back and forward in the history. (The UI used to get changed as you did this but that seems not to work for me anymore. This is reportedly a cool feature in theory but worse in practice due to side-effects such as I/O and lifecycle hooks.) -
Hard-reload the browser page. Notice that "Item 3" is no longer marked as completed (since it was only checked on the frontend and we replaced that with the backend data)
LESSON: Mutations can be local-only or both local and remote. You can use the DB view to go back in history and look at the data and UI at any prior point in time.
-
UI is a tree of components
-
A component declares its data needs using
:query
and identifies the ID attribute using:ident
-
Parent components include children’s queries using
get-query
⇒ the Root’s query has them all -
Fulcro turns Root’s query into a props tree by walking the Client DB from the top-level keys
-
An EQL query contains plain attributes (keywords) and joins (maps), which declare what attributes we want from a sub-entity(ies)
-
Client DB is primarily a map of maps of maps (table → <id> → props) and the data is stored mostly in a normalized form, with each entity in its own table and referenced via its ident, instead of being nested in its parent
-
All changes pass through the (asynchronous) transaction subsystem
-
Data can be loaded via
df/load!
, which leverages a component and its query -
Changes are performed by transacting a mutation, which is just a list of a symbol and parameters - and it may have associated frontend and possibly backend "handlers" via the
defmutation
macros -
Mutations can be frontend-only or both frontend and backend, with the frontend action performed first
-
There is superb tooling in Fulcro Inspect and Shadow Inspect
-
Fulcro is REPL-friendly - almost everything can be done from the REPL
Now is the time to ask any questions you might have, before we proceed to the final part.
You will now get the chance to test your understanding of Fulcro concepts by completing coding exercises (included from fulcro-exercises). Here you will modify and evolve tiny Fulcro applications, one concept at a time.
Important
|
The exercises rely on some knowledge that the workshop does not provide so read carefully the
It is advisable that you skim through the whole tutorial to get an idea what is there, and where. |
Tip
|
If you run into troubles then check the Help section below |
-
🛑 Stop the running todomvc REPL (either by clicking
REPL
in VS Code’s bottom toolbar or running Calva: Disconnect from the REPL Server). -
🏁 Start REPL for the exercises:
-
In
.vscode/settings.json
, removeautoSelectForJackIn
or set it tofalse
(so that you can select the "coding" alias instead of the F. Workshop one) -
In the menu View - Command Palette… - Calva: Start a Project REPL and Connect (aka Jack-In)
-
Select Coding Exercises
-
-
Navigate to http://localhost:9001
-
Open
src/holyjak/fulcro_exercises.cljs
, read the ns docstring and then work through it from top to bottom, following the;; TASK
instructions -
You might need to refer to the Minimalist Fulcro Tutorial to refresh your knowledge or fill in any gaps
Use repeatedly (hint <exercise number>)
(as long as there are any more hints for the exercise) to get useful tips when you get stuck.
Leverage the Fulcro Troubleshooting Decision Tree to help you troubleshoot your problems.
Leverage Fulcro Inspect (especially the DB and perhaps Element tabs), check the Chrome JS Console for warnings and errors.
It might help to close it, reload the page, then open it again.
Study the Minimalist Fulcro Tutorial and explore the resources it links to.