Skip to content

Instantly share code, notes, and snippets.

@dbollinger
Last active March 3, 2020 19:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dbollinger/1fa62ee975ba852633326355d409f7ca to your computer and use it in GitHub Desktop.
Save dbollinger/1fa62ee975ba852633326355d409f7ca to your computer and use it in GitHub Desktop.
ember form component with yup validation
<div role="form" class="ember-form">
{{yield (hash
errors=this.formErrors
formset=formset
updateField=(action "updateField")
validateField=(action "validateField")
validateForm=(action "validateForm")
clearField=(action "clearField")
submit=(action "submitForm")
cancel=(action "revertForm")
)}}
</div>
import Component from '@ember/component';
import EmberObject from '@ember/object';
import layout from '../templates/components/ember-form';
/**
* @class EmberForm
* @namespace Components
*
* ```hbs
* <EmberForm @onSubmit={{action "submitMyForm"}} @formSchema={{myFormSchema}} as |form|>
* ...
* </EmberForm>
* ```
* @class EmberForm
* @yield {Hash} form
* @yield {EmberObject} form.errors
* @yield {Action} form.updateField
* @yield {Action} form.validateField
* @yield {Action} form.validateForm
* @yield {Component} form.submit
*/
export default Component.extend({
layout,
tagName: null,
/**
* A yup schema validation object.
*
* @argument formSchema
* @type ValidationSchema
*/
formSchema: null,
/**
* The initial data to populate the form.
*
* @property formSchema
* @type EmberObject
*/
data: null,
/**
* @property formset
* @type EmberObject
*/
formset: null,
/**
* @property formErrors
* @type EmberObject
*/
formErrors: null,
init() {
this._super(...arguments);
this.initForm();
},
/**
* @method initForm
*/
initForm() {
let formset = this.data ? EmberObject.create({ ...this.data }) : EmberObject.create();
this.set('formset', formset);
this.set('formErrors', EmberObject.create());
},
/**
* A method to validate a single field in the formset against the formSchema
*
* @method validateField
* @param {string} field
*/
async validateField(field) {
try {
let result = await this.formSchema.validateAt(field, this.formset);
this.formset.set(field, result);
this.formErrors.set(field, null);
} catch(e) {
this.formErrors.set(field, e);
}
},
/**
* @method validateForm
*/
async validateForm() {
try {
let result = await this.formSchema.validate(this.formset, { abortEarly: false });
this.set('formErrors', EmberObject.create());
return result;
} catch(e) {
this.set('formErrors', e.inner.reduce((errors, error) => {
errors.set(error.path, error);
return errors;
}, EmberObject.create()));
return e;
}
},
actions: {
updateField(field, value) {
if (value === null) {
// This is messy, but is handling an edge case for clearing the date input. Alternate options?
this.send('clearField', field);
} else {
this.formset.set(field, value);
// We can probably come up with a better rule to determine when to validate
if (this.formErrors[field]) {
this.validateField(field);
}
}
},
validateField(field) {
this.validateField(field);
},
validateForm() {
// I don't think this action would be necessary very often,
// But gives some extra flexibility to the consuming application.
return this.validateForm();
},
clearField(field) {
delete this.formset[field];
this.validateField(field);
},
submitForm() {
// Raise the submit action with the formset
this.validateForm().then(() => {
let result = this.onSubmit(this.formset);
}, (errors) => {
console.log('Validation Errors', errors);
});
},
revertForm() {
this.initForm();
}
}
});
<EmberForm
@formSchema={{this.formSchema}}
@data={{hash projectName="Super Fun Project"}}
@onSubmit={{action (mut this.formResult)}} as |form|>
<label for="project-name">Project Name</label>
<input
id="project-name"
name="project-name"
value={{readonly form.formset.projectName}}
oninput={{action form.updateField "projectName"}}
onblur={{action form.validateField "projectName"}}>
{{#if form.errors.projectName}}
<div class="error">
{{form.errors.projectName.message}}
</div>
{{/if}}
<label for="has-deadline">Has Deadline?</label>
<input
id="has-deadline"
type="checkbox"
name="has-deadline"
checked={{readonly form.formset.hasDeadline}}
onchange={{action form.updateField "hasDeadline"}}>
{{#if form.formset.hasDeadline}}
<label for="deadline-date">Deadline</label>
<input
type="date"
name="deadline"
value={{readonly form.formset.deadline}}
onchange={{action form.updateField "deadline"}}
onblur={{action form.validateField "deadline"}}
{{will-destroy (action form.clearField "deadline")}}>
{{#if form.errors.deadline}}
<div class="error">
{{form.errors.deadline.message}}
</div>
{{/if}}
{{/if}}
<button onclick={{action form.cancel}}>Cancel</form.cancel>
<button onclick={{action form.submit}}>Submit</form.submit>
</EmberForm>
{{log this.formResult}}
import Controller from '@ember/controller';
import * as yup from 'yup';
export const projectSchema = yup.object().shape({
projectName: yup.string().required().trim().min(10).label('Project Name'),
hasDeadline: yup.boolean().label('Has Deadline'),
deadline: yup.date().label('Deadline')
.when('hasDeadline', {
is: true,
then: yup.date().required()
}),
});
export default Controller.extend({
init() {
this._super(...arguments);
this.set('formSchema', projectSchema);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment