Skip to content

Instantly share code, notes, and snippets.

@sukima
Last active January 5, 2021 05:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sukima/c4a253e136c548d20f56d5df045d26eb to your computer and use it in GitHub Desktop.
Save sukima/c4a253e136c548d20f56d5df045d26eb to your computer and use it in GitHub Desktop.
Validatable form
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class extends Component {
@action
validatedSubmit(event) {
event.preventDefault();
let { target: form } = event;
let validateEvent = new CustomEvent('validate');
for (let field of form.elements) {
field.dispatchEvent(validateEvent);
}
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 = '';
get inputElement() {
return document.getElementById(this.fieldId);
}
@action
validate() {
let { validate = () => [] } = this.args;
let [error = ''] = validate(this.inputElement);
this.inputElement.checkValidity();
this.inputElement.setCustomValidity(error);
this.validationMessage = this.inputElement.validationMessage;
this.hasValidated = true;
}
@action
validityCheck({ type, target: { value } }) {
let checkableTypes = this.args.eager
? ['change', 'blur', 'input']
: ['change'];
if (checkableTypes.includes(type)) {
this.validate();
}
this.args.onUpdate?.(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 composeValidators(validators) {
return (element) => validators
.map(i => i(element))
.reduce((a, b) => [...a, ...b], []);
});
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.'];
});
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;
}
:invalid + .validation-message::before {
content: attr(data-message);
color: red;
}
<MyForm @onSubmit={{this.fakeSubmit}}>
<MyInput
name="janeName"
value={{this.form.janeName}}
@label="Name (Jane/eager: true)"
@eager={{true}}
@validate={{validate-name "Jane"}}
@onUpdate={{this.form.update.janeName}}
/>
<MyInput
name="danName"
value={{this.form.danName}}
@label="Name (Dan/eager: true)"
@eager={{true}}
@validate={{compose-validators
(validate-name "Dan")
(validate-capitalized)
}}
@onUpdate={{this.form.update.danName}}
/>
<MyInput
name="phoneNative"
value={{this.form.phoneNative}}
required
pattern="\d\d\d-\d\d\d-\d\d\d\d"
@label="Phone (native/eager: false)"
@eager={{false}}
@onUpdate={{this.form.update.phoneNative}}
/>
<MyInput
name="phoneCustom"
value={{this.form.phoneCustom}}
@label="Phone (custom message/eager: false)"
@eager={{false}}
@validate={{validate-phone}}
@onUpdate={{this.form.update.phoneCustom}}
/>
<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 "validate" this.validate}}
{{on "change" this.validityCheck}}
{{on "input" this.validityCheck}}
{{on "blur" this.validityCheck}}
>
<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"
}
}
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
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment