-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support @validateOn="blur/change"
#24
Conversation
const { name } = target as HTMLInputElement; | ||
|
||
if (name) { | ||
const field = this.fields.get(name as FormKey<FormData<DATA>>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the name matching logic of variant 1 happening!
field.validationEnabled = true; | ||
} | ||
} else { | ||
// @todo how to handle custom controls that don't emit focusout/change events from native form controls? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And yeah, that's the downside of that approach 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imo,
custom controls can / should emit these events tho.
"To be a custom control, it must behave like a control, including having <these events>
"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imo,
custom controls can / should emit these events tho.
Yeah, I agree. We definitely will want validation to trigger for controls like <PowerSelect
, <PowerCalendar
, etc. used inside of forms. Getting these elements to act more like their native versions would be 💯 (e.g., <PowerSelect
-> <select>
, <PowerCalendar
-> <input type="date"
, etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, but the question is still whether to do 1. or 3. then.
For 1. to work, the event must have event.target.name === @name passed to field
. A <PowerSelect>
could have <input type="hidden" name={{@name}}>
(does it, idk?), and if that triggers focusout
then we are fine.
But what about the example with the 3-select date picker. It will receive @name="date"
, but will split this up internally to 3 selects of names date_day
, date_month
, date_year
. When one of those selects triggers a focusout
, our form will not know what field this belongs to, because event.target.name === 'date'
.
For 3., event.target.name
would not matter, and everything would "just work"(TM). But the downside (wrapper div) remains...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the caveat for 1 is fine.
For 3, can you write out the details of this approach by extending this demo -- i just want to be super extra sure I understand how this wrapping div approach would work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure tbh how the demo would relate to this addon, but I'll try it this way:
The current form.field template would have a wrapping div like so:
<div {{on "focusout (fn @triggerValidationFor @name)}} ...attributes>
{{! here is what is currently in the template }}
</div>
@triggerValidationFor
would be curried into the component when yielding it as a contextual component, so that's a private implementation detail.
From the caller's site:
<HeadlessForm as |form|>
<form.field @name="date" as |field|> <-- this would now be a div that captures the event, and *knows* what @name it belongs to!
<field.label>Date:</field.label>
<CustomDatePickerComponentConsistingOfThreeSelectsWithUnknownNames
@value={{field.value}}
@onUpdate={{field.setValue}}
/>
<field.errors/>
</form.field>
</HeadlessForm>
So here, as long as the custom component triggers a focusout
event, validation would work ootb, no matter if there is no native form element, or what name
it/they have or not have...
Btw, triggerValidationFor
would not call e.stopPropagation()
, so the event would not stop to bubble at the form.field
div level, if that was a concern. You could still attach event listeners to the <form>
or even parent elements, and see the event...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this div is too much to assume. I imagine this may be something that a custom-control author would want to opt in to.
as in:
<HeadlessForm as |form|>
<form.field @name="date" as |field|>
<field.captureEvents @events={{"focusout"}}> or @events={{array "focusout" "change"}} ? idk
^-- this would now be a div that captures the event, and *knows* what @name it belongs to!
<field.label>Date:</field.label>
<CustomDatePickerComponentConsistingOfThreeSelectsWithUnknownNames
@value={{field.value}}
@onUpdate={{field.setValue}}
/>
</field.captureEvents>
<field.errors/>
</form
</HeadlessForm>
But maybe that begins to defeat the purpose of the headless form?
would be curried into the component when yielding it as a contextual component, so that's a private implementation detail.
would there be a way to not add those event bindings if no validator is passed? (conditionally apply the on modifier?)
Btw, triggerValidationFor would not call e.stopPropagation(), so the event would not stop to bubble at the form.field div level, if that was a concern
it was a concern! this is good news.
You could still attach event listeners to the
or even parent elements, and see the event...
most excellent.
I think I've now changed my opinion from being all in on option 1, to at least being a bit 50/50 on options 1 and 3 now.
I do have a concern though, so, what would happen if a field has multiple focusables? (this may sway me back to option 1? idk!)
<HeadlessForm as |form|>
<form.field @name="date" as |field|>
<field.label>
Date:
<button {{on 'click' (fn this.helpAbout "date")}}>help</button>
^ - would we end up validating on events from here?
</field.label>
<CustomDatePickerComponentConsistingOfThreeSelectsWithUnknownNames
@value={{field.value}}
@onUpdate={{field.setValue}}
/>
<field.errors/>
</form
</HeadlessForm>
I think allowing bubbling is one of our requirements, because users may want to do this approach to get easy / no-fuss two-way binding™ |
For number 3 above - I could be misunderstanding this, but thought I'd ask. One concern I'd have here is if the markup is: <div ...attributes>
<input />
</div> rather than <div>
<input ...attributes />
</div> It's my understanding that you have no way to access the input component directly to add modifiers or attributes without exposing component arguments, right? So if I wanted to do <div ...attributes>
<input type={{@type}} />
</div> As a user, I'd much rather have access directly to the input element with the attributes spread so that I can provide attributes and modifiers that just work. <SimonsInput type="number" {{on "focus" this.someAction}} /> So I think for me, I'm leaning more towards option 1 knowing that, but also am curious what @NullVoxPopuli and @nicolechung think. |
I, and our current users, also want direct access to the control elements 🙃 |
No, that's not correct. Nothing in that regard would change with 3. here! See this example adding (imaginary) classes to all elements: <HeadlessForm as |form|>
<form.field @name="date" class="add-padding-to-wrapper" as |field|> <-- this would now be a div that captures the event, and *knows* what @name it belongs to!
<field.label class="align-right">Date:</field.label>
<field.input class="border-gray"/>
<field.errors class="text-red" />
</form.field>
</HeadlessForm> In other words, there would still be no implicit element, that doesn't have any What you see above is already the current API. The only thing that changes is that you can actually do |
Btw, I think in most cases you will want a wrapping div around the label and input anyway, like a flex container for positioning the children horizontally. So you could use that to do something like In our case, when we use <HeadlessForm as |form|>
<form.field @name="date" as |field|> <-- this would now be a div that captures the event, and *knows* what @name it belongs to!
<Field>
<:label>
<field.label class="align-right">Date:</field.label>
</:label>
<:input>
<field.input class="border-gray"/>
</:input>
<:hint>
<field.errors class="text-red" />
</:hint>
</Field>
</form.field>
</HeadlessForm> we would probably end up with one additional wrapper div here. But that's also not the end of the world!? 🤷♂️ |
having a built-in opportunity for controlling layout (form.field) is a good call out. I think you sold me 🎉 option 3 ✨ |
Thanks for the great write up! Yeah if that's not an issue at all then I think I'm good with option 3 as well! As you mention, we will want the label and input in a wrapping element anyway. (Doh, I see now you explicitly mention putting this in field, apologies. Friday brain) |
If we all have Friday brain on Friday, we should all take the day off, right? 🙃 |
Had to re-read all of this a few times, but option #3 sounds good to me now too 🎉
This is good to know! Because sometimes we might want the label on the left or right (not just above)
I can't quite visualize how this would work until I see it in a demo or test / maybe I'm not following that whole thread here (quite likely, Friday brain for me too) |
I can certainly provide a working example, like I could amend this PR to switch to 3., shouldn't be much work. But the outline of the implementation is what I wrote in #24 (comment). See the first snippet there: when you have a div, the field can put on
Oh, that's an interesting approach as well! 🤔 Option 4. I guess 😅 I think it could instead also be a yielded modifier? By currying <HeadlessForm as |form|>
<form.field @name="date" as |field|>
<div {{field.captureEvents}}>
^-- this would now be a div that captures the event, and *knows* what @name it belongs to!
<field.label>Date:</field.label>
<CustomDatePickerComponentConsistingOfThreeSelectsWithUnknownNames
@value={{field.value}}
@onUpdate={{field.setValue}}
/>
</div>
<field.errors/>
</form
</HeadlessForm>
Probably yes. Should be the case when you explicitly pass
Oh, another great point, I haven't thought about. So yes, without further measures, this would trigger validations as it is now. But there are ways to handle this. Off the top of my head:
Btw nitpick: we use |
I created a real PR (#25), but keeping this open here for further discussions. I am leaning towards option 4 here, which is basically option 1 plus additional primitives for custom controls in case they are needed, like a yielded modifier (see above comment) as a variation of what @NullVoxPopuli proposed with |
This is a draft PR to just start a discussion, will close it afterwards as it is not ready for review yet...
Let's say we have
@validateOn="blur"
. Now on an input (or other field control) the user causes theblur
/focusout
events to trigger. The action that must follow is that we validate, all fields or just this one, but most importantly we only show the field's errors that triggered the event, not any other.So the question is this: how do we know, which field caused the event? There are several ways to do this, all with their pro and cons:
<form>
(so our main<HeadlessForm>
component), inspect the event target'sname
attribute (which is what we passed asform.field @name="..."
) and use that to look-up the field. This. is what this PR currently does, and it works so far. However it requires the event to be triggered from a real native form control, and it needs to havename
properly set up. That's the case here, but this might not work everywhere, e.g. might not work for rich custom controls like a<PowerSelect>
, or combined controls like a date picker that (internally) is split up into 3<select>
withname="date_{day,month,year}"
, so thename
won't match what the field's name is (justdate
in this example). I think it would be possible to provide a yielded action liketriggerValidationFor(name)
, but this requires actively wiring this up for the user.triggerValidationFor(name)
and all controls need to wire this up explicitly, so our own here (field.input
,field.checkbox
etc.) same as any custom ones., and not rely on anyname
matching logic at allform.field
component create a wrapper div, and attach the event listeners to that. Currently it does not render any markup (truly headless), so when the event bubbles up from the<input>
it would immediately reach the<form>
, at which point we wouldn't know which field this belongs to (unless we do 1.). But with a wrapping div (which would have...attributes
, so users could use it for their own styling etc.) we would be able to catch the event within the boundaries of the field, so we know which field this event belongs to, and show validation only for this field. This has the downside of an additional wrapping div, that does not have any other good reason to exist like a11y or so, so not really headless in a pure way. But the upside is that this would work out of the box, both for our own controls, but also any custom ones, as long as they are able to triggerfocusout
/change
events properly.Any thoughts?