@tony.kay you were asking about reasons, why one would need to use set-query!
in a form component - at the moment, I am playing around with untangled-ui form components, trying to generate forms from clojure/specs dynamically at runtime. My idea was to start with a form component that initially queries for a spec, e.g. (s/keys :req [::phone-type ...])
, then parses the spec after the component was mounted and then modifies the query to add the spec'ed elements (e.g. ::phone-type
). Unfortunately, it is not possible (I think), to modify the form-spec at runtime to include inputs for the new elements. I managed to work around this only by modifying the build-form
function to accept an extra optional parameter form-spec
to override the form-spec defined at class-level, so I can provide a new form-spec at runtime. Is there a better way to do this, so I can maybe avoid using om/set-query!
and modifying build-form
altogether? Any idea would be appreciated!
@sfrank Nice. So, are the specs also dynamic, or are you just wanting a way to translate a spec into a form?
because if it is the latter, I'd probably write a macro that outputs the defui from the spec instead
And then supply (as I discussed earlier) generators for the UI elements separately. That way custom rendering is still possible, but automated form generation would be as short as something like:
(defform Person ::person
Ident
(ident [this props] [:person/by-id (:db/id props)])
Object
(render [this] (render-form-bootstrap this)))
I'm assuming here that defform would keep the defui syntax (for React lifecycle, etc), but would supply IQuery and IForm itself.
Fully dynamic (sending an entity with spec from the server) would require set-query!
. I can see a limited class of applications where this might be necessary: e.g. an application that let's users define their own entities with schema (ala SalesForce) where you want to autogenerate forms for those entities.
The current forms support was not designed for that use-case, but I'm sure we could figure out how to expand it. I don't personally need such support anytime in the forseeable future, but would be glad to take a well-designed contribution.
whereas the spec-based (static) form generation is something I'm very much interested in. Particularly since we could generate validation as well.
@sfrank I was thinking about this use case too, I find an interesting idea to generate the form from the specs, but if I was to do it I would try to make it more granular. Making it an entire form can be complicated to layout, because usually each form has a specific most efficient layout. Considering that would be interesting to have fields generated by spec, for example, if you have an int?
spec you might want to use a numeric spinner, a calendar for an inst?
or something, that way you could write something simpler as (form-input {::field :person/name})
, this way you can reuse the specs + field implementation for that type, and it still open to send a different spec if you want some disparity in some cases
One of the more difficult problems, though, is the nested entities. As long as it doesn't have relations that go in the form. Because the spec might say a person has many phone numbers, but you may not want that to be part of your form at all, and generation of the UI for this kind of nesting may or may not be worthwhile to auto-generate. So then to make it fully general you have to have options around what parts of the spec are meant to be part of your form. Actually, you probably have to solve that problem anyhow, because you're headed towards trying to use the same spec everywhere, which means you're always going to want exclusions, because it is rare you let all of the fields of an entity be edited, and there is no easy way for the form generation logic to know the difference between a field that might need to be protected from editing/visibility. Spec doesn't talk about that aspect of data.
This is part of the reason I have not gone down the road very far. The forms support, as written, is easy to use and understand, and is very light and general purpose. Auto-generation gets you into something considerably larger.
@tony.kay maybe one way to get around that would just have an extra spec for the form instead of the entity, like when doing partial validation
of course, but then you have two things to maintain that get out of sync...so why not just declare the form with validation in IForm?
that was kind of my point when suggesting a more granular approach, have fields you can easily place from the specs
So, show me a spec for a number between 1 and 100
(s/def ::my-int (s/int-in 1 101))
?
and this is probably showing a bit of ignorance on my part of spec...but how do Iget a good validation message for that field with i18n support?
I think I have to write a spec parser (or spec result parser)
you probably have to write an extra layer from it
I had worked on a "sub-layer" of spec for doing coercions
right, but if I declare it as a form field element, that layer is all right there. Easily visible. Easy to write. No extra code.
where I try to use spec as most as I can, and add missing stuff on top of it
yeah...I'm just playing devil's advocate 😉
I guess for forms/validation you gotta do something on that direction, try to re-use the information you have, and add extra stuff on top, trying to minimize the total number of names of you have
I'm just seeing it as a wider problem of: specs can be completely arbitrary. You can use lambdas to "validate". Parsing a spec to generate the right form field with validation is a hard problem in general
I personally would rather see specs used for their intended purpose: verification that things are sane. Using them in code generation seems a slippery slope to complexity
so you try to from the spec to reduce it into the most basic names you can (maybe have one for number, other for ranged numbers, other for strings, this grow as much as your domain needs), then when you have those basic forms you can derive from it, and for arbitrary things you might need arbitraty error message definitions, the challenge is try to avoid those, because they will decrease your reuse
but I agree with one, not a simple problem and there is a high risk of end up just being a lot of more complexity
so I would treat this as a very experimental thing to do 🙂
what's tempting about it is the fact that the key is giving you information, and its nice when we are able to re-use something that's already there
and would keep this in mind: https://clojure.org/about/spec#_informational_vs_implementational
I think that is ultimately the problem here: it's easy to slip into using a spec to inform an implementation detail.
I guess as long as you don't mix up your layers (don't add something directly on your spec to change something about the form), it's all good
Thanks for the answers, I have already learnt a lot! I am trying to build forms for specs that are provided at runtime, so the macro solution does not work for me.
Regarding the question whether or not specs should be used for validation only or also for generation: there has been a dicussion on stackoverflow: http://stackoverflow.com/questions/41657101/metaprogramming-with-clojure-spec-values
If my database supported schema, I'd be more tempted to declare legal values on fields (possibly as specs) but derive the forms from those simple field validations combined with the schema itself.
I don't know....it's a bigger problem than a few minutes of noodling can solve 🙂
Sure
well, in any case: it sounds like you have a good use-case for set-query!
OK, but then I have the problem to modify my form spec to match the new query. Is there any future plan to allow re-defining the form spec at runtime?
that sounds good, yes. We could make the first param of form-spec be this
instead of the class. which you'd have to assume would be the class in the static use-case
Or have build-form just allow you to pass the form spec when you call it.
I tried the latter solution and that worked for me so far
yeah, the declarative nature really is just for static forms...the dynamic case is better with what you're doing
You would also need to allow an optional form-spec argument for get-form-spec (which should be named get-form, I think)
I think making build-form just look to see if it is a class that implements IForm or a simple map would work. That's all a form spec is: a map
(build-form Person)
vs. (build-form some-spec)
oh...hm. Still need the component for the ident, I guess