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 <[email protected]
> <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 [email protected]
>> <javascript:>
>> Note that posts from new members are moderated - please be patient with
>> your first post.
>> To unsubscribe from this group, send email to
>> [email protected] <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 [email protected] <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 [email protected]
Note that posts from new members are moderated - please be patient with your
first post.
To unsubscribe from this group, send email to
[email protected]
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 [email protected].
For more options, visit https://groups.google.com/d/optout.