Skip to content

Who? Me?

Kenneth Tilton edited this page May 13, 2023 · 5 revisions

We had a great question, about this code from this web-mx-sampler TodoMVC code for the toggle-all widget:

(defn toggle-all []
  (div {} {:action (cF (if (every? td-completed (mx-todo-items me))
                         :uncomplete :complete))}
    (input {:id        "toggle-all"
            :class     "toggle-all"
            :type "checkbox"
            :checked   (cF (= (mget (mpar me) :action) :uncomplete))})
    (label {:for     "toggle-all"
            :onclick #(let [action (mget me :action)]
                        (event/preventDefault %)
                        (doseq [td (mx-todo-items)]
                          (mset! td :completed (when (= action :complete) (now)))))}
      "Mark all as complete")))

The question:

Why does "me" in `(mget me :action)` resolve to the DIV instead of the LABEL?

I was busted. That code was obscure, and I knew it when I wrote it. I do not comment much, but I always comment the obscure. It has since been commented, but here is the story.

First, a reminder:

  • me is an injected variable akin to Smalltalk self or Javascript this; and
  • me is injected by any of the cF* family of macros for cell formulas, an example above being
(input {...
        :checked   (cF (= (mget (mpar me) :action) :uncomplete))}
   ...)

Getting back to our :onclick handler, how can we know, just from looking at the code, which widget will be "me"? Easy! Just search outward to the nearest cF*! Except there is none! Now what?

More background: every Web/MX macro named for HTML tags, in this case div, silently wraps children in a cFkids macro. That saves us from having to write:

(defn toggle-all []
  (div {}
    {:action (cF (if (every? td-completed (mx-todo-items me))
                   :uncomplete :complete))}
    :kids (cFkids
            (input {:id      "toggle-all"
                    :class   "toggle-all"
                    :type    "checkbox"
                    :checked (cF (= (mget (mpar me) :action) :uncomplete))})
            (label {:for     "toggle-all"
                    :onclick #(let [action (mget me :action)]
                                (event/preventDefault %)
                                (doseq [td (mx-todo-items)]
                                  (mset! td :completed (when (= action :complete) (now)))))}
              "Mark all as complete"))))

Now when we search out from (mget me :action), we find the cFkids defining the :kids of the div!

Let's look at some other ways we could have written this code less obscurely.

For one, we could wrap the :on-click handler in a cF, just to get a "me". This "me" would be the label, so to get the desired :action property we would have to take the mpar parent of the label. Not as clean, but not obscure! But more brittle, because as we juggle our HTML the property :action might not be on our parent. So...

We need to give the :action host a name, :toggle-all-controller, and do an ascending search: (mget (fasc :toggle-all-controller) :action):

(defn toggle-all []
  (div {} 
    {:name :toggle-all-controller
     :action (cF (if (every? td-completed (mx-todo-items me))
                    :uncomplete :complete))}
    (input {:id      "toggle-all"
            :class   "toggle-all"
            :type    "checkbox"
            :checked (cF (= :uncomplete (mget (mpar) :action)))})
    (label {:for     "toggle-all"
            :onclick (cF #(let [action (mget (fasc :toggle-all-controller) :action)]
                            (event/preventDefault %)
                            (doseq [td (mx-todo-items)]
                              (mset! td :completed (when (= action :complete) (now))))))}
      "Mark all as complete")))

Does the anaphoric me have your head spinning? Worry not. As I said, this was an almost deliberately obscure case. And we are not stuck with me in event handlers! Web/MX internally connects handlers with the proxy models that manage every DOM node. In the above example, we could have obtained the proxy label from the dom label attached handler thus:

(label {:for     "toggle-all"
        :onclick (fn [evt]
                   (let [label (evt-md evt)
                         action (mget (fasc :toggle-all label) :action)]
                     (event/preventDefault evt)
                     (doseq [td (mx-todo-items)]
                       (mset! td :completed (when (= action :complete) (now))))))}
  "Mark all as complete")

Which approach to me resolution is best? After decades with Matrix, I am comfortable with the anaphoric me, so the lighter mechanism of just grabbing a lexical works best. For others, working from the event to the proxy of the .-target might be more comfortable.

Clone this wiki locally