fulcro

Book: http://book.fulcrologic.com, Community Resources: https://fulcro-community.github.io/, RAD book at http://book.fulcrologic.com/RAD.html
yubrshen 2021-04-11T03:21:08.477500Z

To help me to understand the code below better:

(defsc TopChrome [this {:root/keys [router current-session login]}]
  {:query         [{:root/router (comp/get-query TopRouter)}
                   {:root/current-session (comp/get-query Session)}
                   [::uism/asm-id ::TopRouter]
                   {:root/login (comp/get-query Login)}]
   :ident         (fn [] [:component/id :top-chrome])
   :initial-state {:root/router          {}
                   :root/login           {}
                   :root/current-session {}}}
  (let [current-tab (some-> (dr/current-route this this) first keyword)]
    (div :.ui.container
      (div :.<http://ui.secondary.pointing.menu|ui.secondary.pointing.menu>
        (dom/a :.item {:classes [(when (= :main current-tab) "active")]
                       :onClick (fn [] (dr/change-route this ["main"]))} "Main")
        (dom/a :.item {:classes [(when (= :settings current-tab) "active")]
                       :onClick (fn [] (dr/change-route this ["settings"]))} "Settings")
        (div :.<http://right.menu|right.menu>
          (ui-login login)))
      (div :.ui.grid
        (div :.ui.row
          (ui-top-router router))))))

(def ui-top-chrome (comp/factory TopChrome))
can I rewrite it as follows:
(declare link-for-route)

(defsc TopChrome [this {:root/keys [router current-session login]}]
  {:query         [{:root/router (comp/get-query TopRouter)}
                   {:root/current-session (comp/get-query Session)}
                   [::uism/asm-id ::TopRouter]
                   {:root/login (comp/get-query Login)}]
   :ident         (fn [] [:component/id :top-chrome])
   :initial-state {:root/router          {}
                   :root/login           {}
                   :root/current-session {}}}
  (let [current-tab (some-&gt; (dr/current-route this this) first keyword)]
    (div :.ui.container
         (div :.<http://ui.secondary.pointing.menu|ui.secondary.pointing.menu>
              (link-for-route :main     current-tab)
              (link-for-route :settings current-tab)
              (div :.<http://right.menu|right.menu>
                   (ui-login login)))
         (div :.ui.grid
              (div :.ui.row
                   (ui-top-router router))))))

(def ui-top-chrome (comp/factory TopChrome))

(defn link-for-route [menu-item current-tab]
  (case current-tab
    :main     (dom/a :.item {:classes ["active"]
                             :onClick (fn [] (dr/change-route this ["main"]))} "Main")
    :settings (dom/a :.item {:classes ["active"]
                             :onClick (fn [] (dr/change-route this ["settings"]))} "Settings")
    (dom/a :.item {:classes [nil]       ; This default may not be executed at all
                   :onClick (fn [] nil)} "")
    ))
by introducing a function link-for-route to capture the logic that for different menu, there should be a corresponding menu name and onClick function. Thanks for your helping me to learn to write better Clojure! (Note, the original code of TopChrome is from Fulcro 3 template.)

zhuxun2 2021-04-11T03:58:19.479200Z

What's the recommended pattern for maintaining a dynamic document title? Is there way to have the title tracking a few idents in the database?

zhuxun2 2021-04-11T04:01:42.480800Z

Seems like I could set up a defsc component that has a side-effecting body (in this case setting the title) that returns nil, and have it query those idents.

yubrshen 2021-04-11T05:28:30.485700Z

In Fulcro 3 tempalte, in app.ui.root, what does {current-user :account/name} (get props [:component/id :session]) mean in let bindings, also does {:keys [floating-menu]} (css/get-classnames Login)? in the following code:

(defsc Login [this {:account/keys [email]
                    :ui/keys      [error open?] :as props}]
  {:query         [:ui/open? :ui/error :account/email
                   {[:component/id :session] (comp/get-query Session)}
                   [::uism/asm-id ::session/session]]
   :css           [[:.floating-menu {:position "absolute !important"
                                     :z-index  1000
                                     :width    "300px"
                                     :right    "0px"
                                     :top      "50px"}]]
   :initial-state {:account/email "" :ui/error ""}
   :ident         (fn [] [:component/id :login])}
  (let [current-state (uism/get-active-state this ::session/session)
        {current-user :account/name} (get props [:component/id :session])
        initial?      (= :initial current-state)
        loading?      (= :state/checking-session current-state)
        logged-in?    (= :state/logged-in current-state)
        {:keys [floating-menu]} (css/get-classnames Login)
        password      (or (comp/get-state this :password) "")] ; c.l. state for security
    (dom/div
      (when-not initial?
        (dom/div :.<http://right.menu|right.menu>
          (if logged-in?
            (dom/button :.item
              {:onClick #(uism/trigger! this ::session/session :event/logout)}
              (dom/span current-user) ent/nbsp "Log out")
            (dom/div :.item {:style   {:position "relative"}
                             :onClick #(uism/trigger! this ::session/session :event/toggle-modal)}
              "Login"
              (when open?
                (dom/div :.four.wide.ui.raised.teal.segment {:onClick (fn [e]
                                                                        ;; Stop bubbling (would trigger the menu toggle)
                                                                        (evt/stop-propagation! e))
                                                             :classes [floating-menu]}
                  (dom/h3 :.ui.header "Login")
                  (div :.ui.form {:classes [(when (seq error) "error")]}
                    (field {:label    "Email"
                            :value    email
                            :onChange #(m/set-string! this :account/email :event %)})
                    (field {:label    "Password"
                            :type     "password"
                            :value    password
                            :onChange #(comp/set-state! this {:password (evt/target-value %)})})
                    (div :.ui.error.message error)
                    (div :.ui.field
                      (dom/button :.ui.button
                        {:onClick (fn [] (uism/trigger! this ::session/session :event/login {:username email
                                                                                             :password password}))
                         :classes [(when loading? "loading")]} "Login"))
                    (div :.ui.message
                      (dom/p "Don't have an account?")
                      (dom/a {:onClick (fn []
                                         (uism/trigger! this ::session/session :event/toggle-modal {})
                                         (dr/change-route this ["signup"]))}
                        "Please sign up!"))))))))))))
I may guess their meanings, but I never saw them before. Thanks!

Henry 2021-04-11T11:50:17.488100Z

Once you get familiar with destructuring, particularly associative destructuring in this case, you will find such patterns intuitive to understand and pleasing to use. {current-user :account/name} (get props [:component/id :session]) is saying for whatever `(get props [:component/id :session])` returns (which is a map), if the key `:account/name` exists, bind its value to the symbol `current-user`. For instance, if the returned map is {:account/name "<mailto:abc@example.com|abc@example.com>" :session/valid? true}, the symbol current-user would be bound to the value "<mailto:abc@example.com|abc@example.com>". {:keys [floating-menu]} (css/get-classnames Login) is saying for whatever (css/get-classnames Login) returns (which is a map), if the key :floating-menu exists, bind its value to the symbol floating-menu. For instance, if the returned map is {:floating-menu "app_ui_root_Login__floating-menu"}, the symbol floating-menu would be bound to the value "app_ui_root_Login__floating-menu". The above two are simply two different flavours of associative destructuring. The second one uses keyword-arg parsing which is very common across Fulcro codes, especially in defsc to destructure the parameter that is props. The first one is a bit special because 1) the key is namespaced and 2) the symbol is not named the same as the local name of the key (i.e. the symbol is not named name), so the "basic" form of associative destructuring is used instead of keyword-arg parsing. Hope this gives you some pointers to help you get the gist of destructuring. Strongly recommend you to read through the entire official docs on destructuring: https://clojure.org/guides/destructuring#_associative_destructuring

yubrshen 2021-04-11T20:45:53.497600Z

@hk9861 Thanks for very concise explanation and examples, and pointing to the general concept of "associative destructuring"!

yubrshen 2021-04-11T06:26:55.486Z

What’s the meaning of [’*] in :query in the following code,

(defsc SignupSuccess [this props]
  {:query         ['*]
   :initial-state {}
   :ident         (fn [] [:component/id :signup-success])
   :route-segment ["signup-success"]}
  (div
    (dom/h3 "Signup Complete!")
    (dom/p "You can now log in!"))
Thanks!

Björn Ebbinghaus 2021-04-11T08:54:17.487Z

' is a quote and * is an „everything“ query.

👍 1
🙏 1
Alex 2021-04-11T18:03:40.491400Z

Hey guys I had a question, I have the following form definition

(defsc AccountItemForm
  "User Account Edit Form"
  [this {:account/keys [id name email active? editing?] :as props}]
  {:query [:account/editing? :account/id :account/name :account/email :account/active? fs/form-config-join]
   :ident :account/id
   :form-fields #{:account/name :account/email :account/active?}}
  ...)
When I do (fs/dirty? props) I'm always getting false, I looked closer into the implementation of fs/dirty? and it extracts ::config and ::pristine-state. However, my props do not contain that information. So my question was, am I missing something here?

Alex 2021-04-11T18:32:30.491600Z

The form is being called like this:

(defsc AccountListItem
  "An account list item"
  [this {:account/keys [id name email active? editing?] :as props}]
  {:query [:account/id :account/name :account/email :account/active?
           :account/editing?]
   :ident :account/id}
  (if editing?
    (account-form props)
    ...))

thosmos 2021-04-11T18:40:31.491800Z

look into com.fulcrologic.fulcro.algorithms.form-state/add-form-config

Alex 2021-04-11T18:42:00.492Z

Okay so I fixed the issue and it makes sense but I'm not sure if it's correct. I added ..form-state/add-form-config to the AccountListItem . However I am also adding it to the AccountItemForm.

Alex 2021-04-11T18:42:27.492200Z

Most examples I find add it to the Form component and not the parent that renders it.. so I feel like I'm doing something wrong.

thosmos 2021-04-11T18:43:58.492400Z

so when preparing to show the form, you might do something like the following in a mutation:

(let [form-data (comp/get-initial-state AccountItemForm {:somevar :someval})
      form-data (fs/add-form-config AccountItemForm form-data)]
(swap! state
        (fn [st]
          (-&gt; st
            (merge/merge-component AccountItemForm form-data
              :replace [:ui/current-form]
              :replace [:component/id :some-path :form-id])))))

Alex 2021-04-11T18:46:15.492700Z

Would I have to do that even if I do the following on a mutation:

(swap! state
                 (fn [s]
                   (-&gt; s
                       (assoc-in [:account/id account-id :account/editing?] true)
                       (fs/add-form-config*
                        AccountItemForm
                        [:account/id account-id])
                       (fs/pristine-&gt;entity* [:account/id account-id])
                       (fs/mark-complete* [:account/id account-id]))))

thosmos 2021-04-11T18:46:59.492900Z

without looking up those fns that looks like it’s on the right track

thosmos 2021-04-11T18:48:07.493100Z

add-form-config sets ::pristine-state i believe

thosmos 2021-04-11T18:48:40.493300Z

fs/pristine-&gt;entity*
resets it back to its pristine state I believe?

Alex 2021-04-11T18:49:04.493500Z

Yeah that's right, just to start w/ a clean form state

thosmos 2021-04-11T18:49:45.493700Z

but it’s already pristine when you’re starting so unless you’re trying to reset it? But if that’s the case, then this is the wrong place for add-form-config

Alex 2021-04-11T18:50:03.493900Z

I was basing my solution off of the PhoneBook exampe in the fulcro book. https://book.fulcrologic.com/#_form_state_demos

thosmos 2021-04-11T18:50:16.494100Z

it’s either one or the other. also mark-complete is only used when making a change to a field to change it to dirty

thosmos 2021-04-11T18:50:49.494300Z

so in other words those 3 lines are basically mutually exclusive. I would use each one independently in different situations.

Alex 2021-04-11T18:51:09.494500Z

Yeah that makes sense, I'll get rid of pristine line

Alex 2021-04-11T18:51:12.494700Z

thank you

thosmos 2021-04-11T18:51:25.494900Z

add-form-config when showing the form initially, mark-complete after each change, and pristine->entity to cancel or undo changes

👍 1
Alex 2021-04-11T18:53:05.495200Z

I'll add some on-blur w/ the mark-complete on them

thosmos 2021-04-11T18:53:15.495500Z

yeah exactly

Alex 2021-04-11T18:53:41.495700Z

It's pretty fun figuring out how to work in fulcro, it's a frustrating fun experience so far. Thanks for the help.

thosmos 2021-04-11T18:54:31.495900Z

yes there are many foot guns available

thosmos 2021-04-11T18:54:41.496100Z

and therefore very powerful

thosmos 2021-04-11T18:54:49.496300Z

or vice versa

thosmos 2021-04-11T18:55:34.496500Z

most important thing I’ve found is to always think in terms of the data structure in the app DB.

💯 1
thosmos 2021-04-11T18:56:22.496700Z

pretty much all of fulcro’s tools are helpers for either modifying it or deriving from it

Alex 2021-04-11T18:57:14.497Z

Yeah that makes sense with the query definition, I'm still getting use to that. I mainly come from react/vue where it's just all manual so it's taking it's time to get use to it but I see the benefits.

thosmos 2021-04-11T18:58:47.497200Z

it’s actually pretty much all manual here too, just with a very different landscape (data structure)

Alex 2021-04-11T19:06:20.497400Z

have you used fulcro in any serious project?