Skip to content

Instantly share code, notes, and snippets.

@sukima
Last active January 8, 2021 08:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sukima/50fd0f528fe2a28f57236c89aa3bc858 to your computer and use it in GitHub Desktop.
Save sukima/50fd0f528fe2a28f57236c89aa3bc858 to your computer and use it in GitHub Desktop.
Form validation (modifiers)
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { validate } from '../utils/validate';
export default class extends Component {
@action
validatedSubmit(event) {
event.preventDefault();
let { target: form } = event;
validate(...form.elements);
if (form.checkValidity()) {
this.args.onSubmit?.(new FormData(form));
}
}
}
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';
import { tracked } from '@glimmer/tracking';
export default class extends Component {
fieldId = `${guidFor(this)}-field`;
@tracked hasValidated = false;
@tracked validationMessage = '';
@action
validated({ target }) {
this.validationMessage = target.validationMessage;
this.hasValidated = true;
}
@action
update({ target }) {
this.args.onUpdate?.(target.value);
}
}
import Controller from '@ember/controller';
import { action } from '@ember/object';
import FormData from '../utils/form-data';
export default class ApplicationController extends Controller {
form = new FormData();
@action
fakeSubmit(formData) {
alert([...formData.values()].join(', '));
}
}
import { helper } from '@ember/component/helper';
export default helper(function validateCapitalized() {
return ({ value }) => /^[A-Z][a-z]*$/.test(value)
? []
: ['Name must be titleized.'];
});
import { helper } from '@ember/component/helper';
export default helper(function validateName([name]) {
return ({ value }) => value.toLowerCase() !== name.toLowerCase()
? [`Only '${name}' will work.`]
: [];
});
import { helper } from '@ember/component/helper';
export default helper(function validatePhone() {
return ({ value }) => /\d\d\d-\d\d\d-\d\d\d\d/.test(value)
? []
: ['Phone number must be of format xxx-xxx-xxxx.'];
});
import { modifier } from 'ember-modifier';
import { validate } from '../utils/validate';
const commaSeperate = s => s.split(',').map(i => i.trim()).filter(Boolean);
const reduceValidators = async (validators, ...args) => {
let errors = await Promise.all(validators.map(validator => validator(...args)));
return errors.reduce((a, b) => [...a, ...b], []);
}
export default modifier(function validity(
element,
validators,
{ on: eventNames = 'change,input,blur' }
) {
let autoValidationEvents = commaSeperate(eventNames);
let autoValidationHandler = () => validate(element);
let validateHandler = async () => {
let [error = ''] = await reduceValidators(validators, element);
element.checkValidity();
element.setCustomValidity(error);
element.dispatchEvent(new CustomEvent('validated'));
};
element.addEventListener('validate', validateHandler);
autoValidationEvents.forEach(eventName => {
element.addEventListener(eventName, autoValidationHandler);
});
return () => {
element.removeEventListener('validate', validateHandler);
autoValidationEvents.forEach(eventName => {
element.removeEventListener(eventName, autoValidationHandler);
});
};
});
data:empty::before {
content: '—';
}
form > * + * {
margin-top: 0.5rem;
}
fieldset {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1rem;
}
[data-validated] :valid {
box-shadow: green 0px 0px 1.5px 1px;
}
[data-validated] :invalid {
box-shadow: red 0px 0px 1.5px 1px;
}
:invalid + .validation-message::before {
content: attr(data-message);
color: red;
}
<MyForm @onSubmit={{this.fakeSubmit}}>
<MyInput
name="janeName"
value={{this.form.janeName}}
@label="Name (Jane)"
@onUpdate={{this.form.update.janeName}}
{{validity (validate-name "Jane")}}
/>
<MyInput
name="danName"
value={{this.form.danName}}
@label="Name (Dan)"
@onUpdate={{this.form.update.danName}}
{{validity (validate-name "Dan") (validate-capitalized)}}
/>
<MyInput
name="phoneNative"
value={{this.form.phoneNative}}
required
pattern="\d\d\d-\d\d\d-\d\d\d\d"
@label="Phone (native)"
@onUpdate={{this.form.update.phoneNative}}
{{validity on="change"}}
/>
<MyInput
name="phoneCustom"
value={{this.form.phoneCustom}}
@label="Phone (custom message)"
@onUpdate={{this.form.update.phoneCustom}}
{{validity (validate-phone) on="change"}}
/>
<button type="submit">Go</button>
<dl class="tracked-data">
<dt>Jane name</dt>
<dd><data>{{this.form.janeName}}</data></dd>
<dt>Dan name</dt>
<dd><data>{{this.form.danName}}</data></dd>
<dt>Phone native</dt>
<dd><data>{{this.form.phoneNative}}</data></dd>
<dt>Phone custom</dt>
<dd><data>{{this.form.phoneCustom}}</data></dd>
</dl>
</MyForm>
<form
...attributes
novalidate
{{on "submit" this.validatedSubmit}}
>
{{yield}}
</form>
<fieldset data-validated={{this.hasValidated}}>
{{#if @legend}}
<legend>{{@legend}}</legend>
{{/if}}
{{#if @label}}
<label for={{this.fieldId}}>{{@label}}</label>
{{/if}}
<input
type="text"
...attributes
id={{this.fieldId}}
{{on "validated" this.validated}}
{{on "input" this.update}}
>
<span
class="validation-message"
data-message={{this.validationMessage}}
></span>
</fieldset>
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js",
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1"
},
"addons": {
"@glimmer/component": "1.0.0",
"ember-modifier": "2.1.1"
}
}
import { tracked } from '@glimmer/tracking';
export default class FormData {
@tracked janeName;
@tracked danName;
@tracked phoneNative;
@tracked phoneCustom;
update = new Proxy(this, {
get: (t, p) => Reflect.has(t, p) ? v => t[p] = v : undefined
});
}
export function validate(...elements) {
let validateEvent = new CustomEvent('validate');
elements.forEach(el => el.dispatchEvent(validateEvent));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment