Fork me on GitHub
#fulcro
<
2021-04-11
>
yubrshen03:04:08

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 :.
        (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 :.
          (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-> (dr/current-route this this) first keyword)]
    (div :.ui.container
         (div :.
              (link-for-route :main     current-tab)
              (link-for-route :settings current-tab)
              (div :.
                   (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.)

zhuxun203:04:19

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?

zhuxun204:04:42

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.

yubrshen05:04:30

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 :.
          (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!

Henry11:04:17

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 "" :session/valid? true}, the symbol current-user would be bound to the value "". {: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

yubrshen20:04:53

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

yubrshen06:04:55

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 Ebbinghaus08:04:17

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

🙏 3
👍 3
Alex18:04:40

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?

Alex18:04:30

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

thosmos18:04:31

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

Alex18:04:00

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.

Alex18:04:27

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.

thosmos18:04:58

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]
          (-> st
            (merge/merge-component AccountItemForm form-data
              :replace [:ui/current-form]
              :replace [:component/id :some-path :form-id])))))

Alex18:04:15

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

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

thosmos18:04:59

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

thosmos18:04:07

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

thosmos18:04:40

fs/pristine->entity*
resets it back to its pristine state I believe?

Alex18:04:04

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

thosmos18:04:45

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

Alex18:04:03

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

thosmos18:04:16

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

thosmos18:04:49

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

Alex18:04:09

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

Alex18:04:12

thank you

thosmos18:04:25

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

👍 3
Alex18:04:05

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

thosmos18:04:15

yeah exactly

Alex18:04:41

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

thosmos18:04:31

yes there are many foot guns available

thosmos18:04:41

and therefore very powerful

thosmos18:04:49

or vice versa

thosmos18:04:34

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

💯 3
thosmos18:04:22

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

Alex18:04:14

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.

thosmos18:04:47

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

Alex19:04:20

have you used fulcro in any serious project?