Thanks Michael for you quick response, that was definitely the pointer I 
needed!

After a bit of googling which didn't turn up much I ended up with:

(defmacro form [state & elements]
  (let [m-state (gensym)]
    `(let [~m-state ~state]
       [:div.form.horizontal
        ~@(map (fn [[f m & rest]]
                 `[~f (assoc ~m
                             :value (get @(:values ~m-state) (:id ~m))
                             :on-change #(swap! (:values ~m-state) assoc 
(:id ~m) "UPDATED"))
                   ~@rest])
               elements)])))

(clojure.pprint/pprint
 (macroexpand '(form {:values test-atom}
                     [:div {:id 1} "Hi there"])))
results in

(let*
 [G__79805 {:values test-atom}]
 [:div.form.horizontal
  [:div
   (clojure.core/assoc
    {:id 1}
    :value
    (clojure.core/get @(:values G__79805) (:id {:id 1}))
    :on-change
    (fn*
     []
     (clojure.core/swap!
      (:values G__79805)
      clojure.core/assoc
      (:id {:id 1})
      "UPDATED")))
   "Hi there"]])

My understanding is:

1(defmacro form [state & elements]
2  (let [m-state (gensym)]
3    `(let [~m-state ~state]
4       [:div.form.horizontal
5        ~@(map (fn [[f m & rest]]
6                 `[~f (assoc ~m
7                             :value (get @(:values ~m-state) (:id ~m))
8                             :on-change #(swap! (:values ~m-state) assoc 
(:id ~m) "UPDATED"))
9                   ~@rest])
10               elements)])))

0 - ` means "emit, don't evaluate", ~@ means "splice, e.g. remove the outer 
sequence so [a ~@[1 2]] becomes [a 1 2] and ' means 'the symbol of rather 
than the value of'.
2 - declare m-state, which is lexically scoped to the macro and is bound to 
a random identifier created by gensym
3 - the back-tick (syntax-quote) returns the form rather than evaluating 
the form, so the macro will return (let* [m-8_324230_ ....]) The [~m-state 
~state] is just bewildering though.
3 - in addition, the 'state' argument appears to be destructured, but only 
to one level so if the state contains an atom it is the var of the atom
4 - literal text emitted in-line
5 - splice the results of the map (i.e. rather than [:div.form.horizontal 
[child1 child2]] return [:div.form.horizontal child1 child2])
5 - also destructure each element assuming [f m(ap) and 0 or more other 
args]
6 - emit [<f> where <f> is the first symbol, 'f' l in each element. Also 
prevent this being evaluated in the macro with the syntax-quote as (5) has 
introduced some new scope because of the ~@ - not sure.
6 - also associate onto the symbol m (which is assumed to be associative, 
e.g. a map)...
7/8 - extract data out of the 'run-time' (e.g. not macro-time) value of the 
provided state (magically captured under ~m-state)
9 - splice in the rest of the arguments, if any, that were part of the 
element
10 - and do that magic for each element

This was all arrived at using a process only one step up from the good old 
"I don't _quite_ know what I'm doing so let's try random characters" so any 
improvements are most welcome! It does work but I am sure it is making 
peoples eyes bleed.

Thanks again, and any and all thoughts are welcome!

On Wednesday, 30 September 2015 21:41:31 UTC+1, Michael Blume wrote:
>
>  #foo gensyms don't survive across a 'break' in quoting
>
> you can do
>
> `(let [foo# bar]
>   (other stuff
>     (foo# ...)))
>
> but you can not do
>
> `(let [foo# bar]
>    ~(for [i s]
>        `(foo# ...)))
>
> or anything like that. The workaround is to create a gensym explicitly 
> using (gensym), let that, and splice it in wherever you need the gensym.
>
> On Wed, Sep 30, 2015 at 1:29 PM Colin Yates <colin...@gmail.com 
> <javascript:>> wrote:
>
>> Hi all,
>>
>> I am banging my head against the wall - I think it is obvious but I have 
>> started too long:
>>
>> The use-case is that I want a form which takes a set of children. The 
>> form also takes in some form-wide state, like the form-wide validation, the 
>> values for each item etc. I want the macro, for each child, to decorate 
>> that child by extracting the validation errors and value from the form-wide 
>> state.
>>
>> So, assuming:
>>  - validation looks like {:name "Duplicate name" :age "You must be at 
>> least 0"}
>>  - form-values looks like {:name "a-duplicate-user" :age -1}
>>
>> then my form might look like:
>>
>> (form {:editing? true :values form-values :validation validation-report 
>> :on-change handle-form-change}
>>   [form/text {:id :name}]
>>   [form/number {:id :age}])
>>
>> After the macro I want the following code:
>>
>> [:div.form.horizontal
>>   {:class "editing"}
>>   [form/text {:id :name :value "a-duplicate-user" :errors "Duplicate 
>> name" :on-click (fn [e] (handle-form-change :name (-> e .target .value])]
>>   [form/number {:id :age :value "-1" :errors "You must be at least 0" 
>> :on-click (fn [e] (handle-form-change :age (-> e .target .value))]]
>>
>> However, ideally the macro would _not_ emit the contents of the input as 
>> literals but would emit code that inspects the provided parameters at 
>> run-time (i.e. rather than :value "a-duplicate-user" I would much prefer 
>> :value (-> state values :name) as that will allow me to pass in an atom for 
>> example.
>>
>> I have tried so many variations and evaluating the state (e.g. (:editing? 
>> state)) works fine as the emitted code has the destructured values, but 
>> that doesn't work for an atom.
>>
>> Here is my attempt at trying to emit code that interrogates the provided 
>> parameter.
>>
>> (defmacro form [state & elements]
>>   (let [state# state]
>>     `[:div.form.horizontal
>>       {:class (if (:editing? state#) "editing" "editable")}
>>       ~@(map (fn [[_ {:keys [id]} :as child]]
>>                (update child 1 assoc
>>                        :editing? (:editing? state#)
>>                        :value `(-> (:values state#) 'deref (get ~id))
>>                        :on-change `(fn [e#]
>>                                      (js/console.log "E: " 
>> (cljs.core/clj->js e#))
>>                                      ((:on-change state#) ~id (-> e# 
>> .-target .-value)))))
>>              elements)]))
>>
>> The error I am getting is that there is such var as the gen-sym's state# 
>> in the namespace.
>>
>> The generic thing I am trying to do is remove the boilerplate from each 
>> of the items in the form.
>>
>> Any and all suggestions are welcome. 
>>
>> Thanks!
>>
>> -- 
>> You received this message because you are subscribed to the Google
>> Groups "Clojure" group.
>> To post to this group, send email to clo...@googlegroups.com 
>> <javascript:>
>> Note that posts from new members are moderated - please be patient with 
>> your first post.
>> To unsubscribe from this group, send email to
>> clojure+u...@googlegroups.com <javascript:>
>> For more options, visit this group at
>> http://groups.google.com/group/clojure?hl=en
>> --- 
>> You received this message because you are subscribed to the Google Groups 
>> "Clojure" group.
>> To unsubscribe from this group and stop receiving emails from it, send an 
>> email to clojure+u...@googlegroups.com <javascript:>.
>> For more options, visit https://groups.google.com/d/optout.
>>
>

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
--- 
You received this message because you are subscribed to the Google Groups 
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to clojure+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to