Skip to content

Latest commit

 

History

History
378 lines (273 loc) · 29.1 KB

Workshop.adoc

File metadata and controls

378 lines (273 loc) · 29.1 KB

Workshop

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.

Battle plan

  1. DIY app exercises 1 - 9 (stop latest 10 min before the workshop’s end)

  2. Review Summary - lessons learned

  3. Q&A

  4. Coding exercises - if time permits or do them at home after the workshop

  5. Learn more about Fulcro

Part 1: App exercises

These interactive exercises introduce you to Fulcro concepts and the way of working with a Fulcro application

Setup

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
Workshop docs tab

Webapp tab + Shadow-Cljs tab

screen layout

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.)

Exercise instructions

Note: You have about 5 - 10 minutes per exercise.

Take good time to read and understand the data / code you are looking at.

1. Component tree

  1. Open the React dev tools' Components tab

  2. Explore the tree of the components displayed

  3. 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):

UI tree
Figure 1. UI components tree

The following code is a simplified version of the code defining the UI:

UI code (simplified):
(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))))
  1. 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.

2. Component props and query

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!

Fulcro Inspect - Element
  1. Open now the Fulcro Inspect tab and its Element sub-tab.

  2. 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

  3. 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

  4. 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

  5. Pick another list item element and compare its values with the original item

  6. Select the whole TodoList and look at its ident, props, and query. Does it relate in any way to the TodoItem'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.

3. Exploring the query & Shadow Inspect

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.

Shadow Inspect
  1. Open src/fulcro_todomvc/ui.cljs, scroll to the bottom and inside the (comment …​) find Exercise 3.1 and evaluate the get-query form (Alt - ENTER or Option - ENTER from the essential few Calva keybindings), read the result

    Note
    The evaluation result is displayed both inline and inside the output.calva-repl file (Go - Go to File…​ to reopen it)
  2. 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 and Alt - Enter).

    • Note: We used tap> so the data will not show up in the REPL but in a tap client. So:

      1. 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.)

      2. eval the form again; the value should appear in Shadow Inspect

      3. 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 to false)

  3. 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

  4. 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:

    Query composition
    Figure 2. Query composition

    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:

  5. 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.

4. Root query

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:

Query composition

Let’s have a look at the query and how it is turned into a props tree.

  1. Open src/fulcro_todomvc/ui.cljs, scroll to the bottom and inside the (comment …​) evaluate the get-query Root form marked Exercise 4.1

  2. 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

  3. 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.

5. Exploring the client DB

This is the Root query:

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:

Fulcro Inspect - DB Explorer
  1. Switch to Fulcro Inspect - DB Explorer

  2. 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.)

  3. 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.

  4. 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.

6. Loading data from the server

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:

Fulcro overview
Figure 3. Fulcro overview

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.

  1. Make sure Fulcro Inspect is opened and reload the page

  2. 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

  3. 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.

  4. 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.

  5. 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.

  6. 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 the list-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

  7. Review the EQL Primer above

  8. 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

  9. 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.)

  10. [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.

7. Loading data from the REPL

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.

  1. Go to ui.cljs and evaluate the df/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.)

  2. 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 the list-resolver that takes a :list/id and returns its data

    • Notice the shape of the response - it is a tree

  3. Go to Fulcro Inspect - DB and expand :list/id to verify the list 2 has been added there. Then expand :item/id to verify that the item 99 has also been added and item 1 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":

      Fulcro Inspect - DB and normalized data

      That is only possible because both TodoList and TodoItem 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.

8. Changing local and remote data with mutations

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.

  1. 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}))

  2. Look at the transaction in the Transactions tab and verify it indeed is a list

  3. Explore it in Fulcro Inspect’s Network tab

    • Notice the mutation is now wrapped in [ …​ ] to become a complete EQL transaction.

  4. 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)

  5. 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.

9. Triggering a mutation manually

As always in Fulcro, we do not need to click around and can trigger the mutation from the REPL.

  1. Go to ui.cljs and eval the form marked Exercise 9.1

  2. Observe that the UI now shows the item as not completed

  3. In api.cljs, find the todo-check mutation (you can Command-click / Control-click the "call" go to its definition) and inside the remote section change the true to false 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 log shadow-cljs: load JS fulcro_todomvc/api.cljs.)

  4. In ui.cljs, eval the form marked Exercise 9.2

    1. Notice the item is marked as completed in the UI

    2. Verify that you see the todo-check mutation in the Transactions tab

    3. Verify that you do not see it in Fulcro Inspect’s Network tab

  5. (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.)

  6. 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.

Summary - lessons learned

  1. UI is a tree of components

  2. A component declares its data needs using :query and identifies the ID attribute using :ident

  3. Parent components include children’s queries using get-query ⇒ the Root’s query has them all

  4. Fulcro turns Root’s query into a props tree by walking the Client DB from the top-level keys

  5. An EQL query contains plain attributes (keywords) and joins (maps), which declare what attributes we want from a sub-entity(ies)

  6. 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

  7. All changes pass through the (asynchronous) transaction subsystem

  8. Data can be loaded via df/load!, which leverages a component and its query

  9. 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

  10. Mutations can be frontend-only or both frontend and backend, with the frontend action performed first

  11. There is superb tooling in Fulcro Inspect and Shadow Inspect

  12. Fulcro is REPL-friendly - almost everything can be done from the REPL

Interlude: Questions and answers

Now is the time to ask any questions you might have, before we proceed to the final part.

Part 2: Coding exercises

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 RESOURCES referenced by each task and make sure you understand them. (You might occasionally also need to refer to a previous part of the tutorial, if something is not clear.) This applies especially to:

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

Setup

  1. 🛑 Stop the running todomvc REPL (either by clicking REPL in VS Code’s bottom toolbar or running Calva: Disconnect from the REPL Server).

  2. 🏁 Start REPL for the exercises:

    1. In .vscode/settings.json, remove autoSelectForJackIn or set it to false (so that you can select the "coding" alias instead of the F. Workshop one)

    2. In the menu View - Command Palette…​ - Calva: Start a Project REPL and Connect (aka Jack-In)

    3. Select Coding Exercises

  3. Navigate to http://localhost:9001

  4. Open src/holyjak/fulcro_exercises.cljs, read the ns docstring and then work through it from top to bottom, following the ;; TASK instructions

  5. You might need to refer to the Minimalist Fulcro Tutorial to refresh your knowledge or fill in any gaps

Help

Troubleshooting and getting help during the exercises

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.

Various problems and solutions

Fulcro Inspect shows an empty client DB

It might help to close it, reload the page, then open it again.

'shadow-cljs - (reload failed)' reported in the browser

If shadow-cljs fails because of a coding error then this error is reported in the browser. It may break live reloading, i.e. you might need to hard-reload the page after you fix it.

Learn more about Fulcro

Study the Minimalist Fulcro Tutorial and explore the resources it links to.