Skip to content

Add `@validateOn="input", rename "blur" to "focusout" #31

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

Merged
merged 3 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ember-headless-form/src/components/-private/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export interface HeadlessFormFieldComponentSignature<
registerField: RegisterFieldCallback<FormData<DATA>, KEY>;
unregisterField: UnregisterFieldCallback<FormData<DATA>, KEY>;
triggerValidationFor(name: KEY): Promise<void>;
fieldValidationEvent: 'focusout' | 'change' | undefined;
fieldRevalidationEvent: 'focusout' | 'change' | undefined;
fieldValidationEvent: 'focusout' | 'change' | 'input' | undefined;
fieldRevalidationEvent: 'focusout' | 'change' | 'input' | undefined;
};
Blocks: {
default: [
Expand Down
17 changes: 6 additions & 11 deletions ember-headless-form/src/components/headless-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type {
WithBoundArgs,
} from '@glint/template';

type ValidateOn = 'change' | 'blur' | 'submit';
type ValidateOn = 'change' | 'focusout' | 'submit' | 'input';

export interface HeadlessFormComponentSignature<DATA extends UserData> {
Element: HTMLFormElement;
Expand Down Expand Up @@ -127,34 +127,29 @@ export default class HeadlessFormComponent<
/**
* Return the event type that will be listened on for dynamic validation (i.e. *before* submitting)
*/
get fieldValidationEvent(): 'focusout' | 'change' | undefined {
get fieldValidationEvent(): 'focusout' | 'change' | 'input' | undefined {
const { validateOn } = this;

return validateOn === 'submit'
? // no need for dynamic validation, as validation always happens on submit
undefined
: // our component API expects "blur", but the actual blur event does not bubble up, so we use focusout internally instead
validateOn === 'blur'
? 'focusout'
: validateOn;
}

/**
* Return the event type that will be listened on for dynamic *re*validation, i.e. updating the validation status of a field that has been previously marked as invalid
*/
get fieldRevalidationEvent(): 'focusout' | 'change' | undefined {
get fieldRevalidationEvent(): 'focusout' | 'change' | 'input' | undefined {
const { validateOn, revalidateOn } = this;

return revalidateOn === 'submit'
? // no need for dynamic validation, as validation always happens on submit
undefined
: // when validation happens more frequently than revalidation, then we can ignore revalidation, because the validation handler will already cover us
validateOn === 'change' ||
(validateOn === 'blur' && revalidateOn === 'blur')
validateOn === 'input' ||
(validateOn === 'change' && revalidateOn === 'focusout') ||
validateOn === revalidateOn
? undefined
: // our component API expects "blur", but the actual blur event does not bubble up, so we use focusout internally instead
revalidateOn === 'blur'
? 'focusout'
: revalidateOn;
}

Expand Down
3 changes: 2 additions & 1 deletion test-app/app/templates/index.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<HeadlessForm
@data={{this.data}}
@validateOn='blur'
@validateOn='focusout'
@revalidateOn='input'
@onSubmit={{this.doSomething}}
as |form|
>
Expand Down
24 changes: 24 additions & 0 deletions test-app/tests/helpers/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { find, triggerEvent } from '@ember/test-helpers';

/**
* Fill the provided text into the `value` property of the selected form element, similar to `fillIn`, but *without* implicitly triggering a `change` event.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10/10 would read again

* This mimics the behavior of a user typing data into an input without yet focusing out of it. Browsers will only trigger a `change` event when focusing
* out of the element, not while typing!
*
* `fillIn` will basically simulate entering the data *and* kinda focusing out (as it triggers `change`, however not `focusout`, which is impossible to achieve as a real user),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😮 I did NOT know this!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, had to confirm this for myself by looking up their source.

I should probably create an issue. I guess we cannot change the existing behaviour of fillIn, as that would break every app. But maybe we can upstream some additional helpers that cover the different real-world use cases better...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* while this helper does only the former.
*/
export async function input(selector: string, value: string): Promise<void> {
const el = find(selector);

if (!el) {
throw new Error(`No element found for selector ${selector}`);
}

if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
throw new Error(`Invalid element for \`input\` helper.`);
}

el.value = value;
await triggerEvent(el, 'input');
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ module(
});

module('captureEvents', function () {
test('captures blur events triggering validation without controls having name matching field name when @validateOn="blur"', async function (assert) {
test('captures blur events triggering validation without controls having name matching field name when @validateOn="focusout"', async function (assert) {
const data: TestFormData = { custom: 'foo' };
const validateCallback = sinon.fake.returns([
{
Expand All @@ -234,7 +234,7 @@ module(
]);

await render(<template>
<HeadlessForm @data={{data}} @validateOn="blur" as |form|>
<HeadlessForm @data={{data}} @validateOn="focusout" as |form|>
<form.field
@name="custom"
@validate={{validateCallback}}
Expand Down Expand Up @@ -315,7 +315,7 @@ module(
});
});

test('captures blur/change events triggering re-/validation without controls having name matching field name when @validateOn="blur" and @revalidateOn="change"', async function (assert) {
test('captures blur/change events triggering re-/validation without controls having name matching field name when @validateOn="focusout" and @revalidateOn="change"', async function (assert) {
const data: TestFormData = { custom: 'foo' };
const validateCallback = sinon.fake.returns([
{ type: 'invalid-date', value: undefined, message: 'Invalid Date!' },
Expand All @@ -324,7 +324,7 @@ module(
await render(<template>
<HeadlessForm
@data={{data}}
@validateOn="blur"
@validateOn="focusout"
@revalidateOn="change"
as |form|
>
Expand Down
Loading