Skip to content

support latest version of ember-changeset / ember-changeset-validations #27

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
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
70 changes: 51 additions & 19 deletions addon/components/bs-form/element.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,56 @@
import { notEmpty } from '@ember/object/computed';
import { defineProperty, computed } from '@ember/object';
import { A } from '@ember/array';
import BsFormElement from 'ember-bootstrap/components/bs-form/element';
import { action } from '@ember/object';
import { dependentKeyCompat } from '@ember/object/compat';

export default BsFormElement.extend({
'__ember-bootstrap_subclass' : true,
export default class BsFormElementWithChangesetValidationsSupport extends BsFormElement {
'__ember-bootstrap_subclass' = true;

hasValidator: notEmpty('model.validate'),
@dependentKeyCompat
get errors() {
let error = this.model?.error?.[this.property]?.validation;
return error ? [error] : [];
}

get hasValidator() {
return typeof this.model?.validate === 'function';
}

// Ember Changeset does not validate the initial state. Properties are not
// validated until they are set the first time. But Ember Bootstrap may show
// validation results before the property was changed. We need to make sure
// that changeset is validated at that time.
// Ember Bootstrap may show the validation in three cases:
// 1. User triggered one of the events that should cause validation errors to
// be shown (e.g. focus out) by interacting with the form element.
// Ember Bootstrap stores these state in `showOwnValidation` property of
// the form element.
// 2. User submits the form. Ember Bootstrap will show validation errors
// for all form elements in that case. That state is handled by
// `showAllValidations` arguments passed to the form element.
// 3. User passes in a validation error or warning explicilty using
// `customError` or `customWarning` arguments of the form element.
// Ember Bootstrap ensures that the model is valided as part of its submit
// handler. So we can assume that validations are run in second case. Ember
// Bootstrap does not show the validation errors of the model but only the
// custom error and warning if present. So it does not matter if initial
// state is validated or not. That means we only have to handle the first
// case.
// Ember Bootstrap does not provide any API for validation plugins to support
// these needs. We have to override a private method to run the validate
// logic for now.
@action
async showValidationOnHandler(event) {
let validationShowBefore = this.showOwnValidation;

// run original implementation provided by Ember Bootstrap
super.showValidationOnHandler(event);

setupValidations() {
// `Changeset.error` is a getter based on a tracked property. Since it's a
// derived state it's not working together with computed properties smoothly.
// As a work-a-round we observe the `Changeset._errors` computed property
// directly, which holds the state. This is not optimal cause it's private.
// Should refactor to native getter as soon as `<FormElement>` component
// of Ember Bootstrap supports native getters for `FormElement.errors`
// property.
let key = `model.error.${this.get('property')}.validation`;
defineProperty(this, 'errors', computed(`model._errors`, function() {
return A(this.get(key));
}));
// run initial validation if
// - visibility of validations changed
let canValidate = this.hasValidator && this.property;
let validationVisibilityChanged = !validationShowBefore && this.showOwnValidation;
if (canValidate && validationVisibilityChanged) {
await this.model.validate(this.property);
}
}
});
}
157 changes: 106 additions & 51 deletions tests/integration/components/bs-form-element-test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, triggerEvent, fillIn, blur } from '@ember/test-helpers';

import { render, triggerEvent, fillIn, focus, blur } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

import {
validatePresence,
validateLength,
validateConfirmation,
validateFormat,
} from 'ember-changeset-validations/validators';

module('Integration | Component | bs form element', function(hooks) {
Expand All @@ -21,19 +17,7 @@ module('Integration | Component | bs form element', function(hooks) {
]
};

const extendedValidation = {
name: [
validatePresence(true),
validateLength({ min: 4 })
],
email: validateFormat({ type: 'email', allowBlank: true }),
password: [
validateLength({ min: 6 })
],
passwordConfirmation: validateConfirmation({ on: 'password' })
}

test('valid validation is supported as expected', async function(assert) {
test('form is submitted if valid and validation success shown', async function(assert) {
let model = {
name: '1234',
};
Expand All @@ -57,7 +41,7 @@ module('Integration | Component | bs form element', function(hooks) {
assert.verifySteps(['submit action has been called.']);
});

test('invalid validation is supported as expected', async function(assert) {
test('validation errors are shown on submit', async function(assert) {
let model = {
name: '',
};
Expand All @@ -82,50 +66,121 @@ module('Integration | Component | bs form element', function(hooks) {
assert.verifySteps(['Invalid action has been called.']);
});

test('validation errors are shown after blur', async function(assert) {
this.set('model', { name: '' });
this.set('validation', validation);

test('more complicated validations', async function(assert) {
let model = {
name: '',
password: null,
passwordConfirmation: null,
email: '',
};
await render(hbs`
<BsForm @model={{changeset this.model this.validation}} as |form|>
<form.element @label="Name" @property="name" />
</BsForm>
`);
assert.dom('input').doesNotHaveClass('is-invalid');

this.set('model', model);
this.set('validation', extendedValidation);
this.submitAction = function() {
assert.ok(false, 'submit action must not been called.');
};
this.invalidAction = function() {
assert.step('Invalid action has been called.');
};
await focus('input');
await blur('input');
assert.dom('input').hasClass('is-invalid');
});

test('validation success is shown after blur', async function(assert) {
this.set('model', { name: 'Clara' });
this.set('validation', validation);

await render(hbs`
<BsForm @model={{changeset this.model this.validation}} @onSubmit={{this.submitAction}} @onInvalid={{this.invalidAction}} as |form|>
<form.element id="name" @label="Name" @property="name" />
<form.element id="email" @label="Email" @property="email" />
<form.element id="password" @label="Password" @property="password" />
<form.element id="password-confirmation" @label="Password confirmation" @property="passwordConfirmation" />
<BsForm @model={{changeset this.model this.validation}} as |form|>
<form.element @label="Name" @property="name" />
</BsForm>
`);
assert.dom('input').doesNotHaveClass('is-valid');

await fillIn('#password input', 'bad');
assert.dom('#password input').doesNotHaveClass('is-invalid', 'password does not have error while typing.');
assert.dom('#password input').doesNotHaveClass('is-valid', 'password does not have success while typing.');
await focus('input');
await blur('input');
assert.dom('input').hasClass('is-valid');
});

await blur('#password input');
assert.dom('#password input').hasClass('is-invalid', 'password does have error when focus out.');
test('validation errors are shown after user input', async function(assert) {
this.set('model', { name: '' });
this.set('validation', validation);

await fillIn('#password-confirmation input', 'betterpass');
assert.dom('#password-confirmation input').doesNotHaveClass('is-invalid', 'password confirmation does not have error while typing.');
await render(hbs`
<BsForm @model={{changeset this.model this.validation}} as |form|>
<form.element @label="Name" @property="name" />
</BsForm>
`);
assert.dom('input').doesNotHaveClass('is-invalid');

await fillIn('input', 'R');
assert.dom('input').doesNotHaveClass('is-invalid', 'validation is not shown while user is typing');

await blur('input');
assert.dom('input').hasClass('is-invalid', 'validation error is shown after focus out');
});

await blur('#password-confirmation input');
assert.dom('#password-confirmation input').hasClass('is-invalid', 'password confirmation does have error when focus out.');
test('validation success is shown after user input', async function(assert) {
this.set('model', { name: '' });
this.set('validation', validation);

await render(hbs`
<BsForm @model={{changeset this.model this.validation}} as |form|>
<form.element @label="Name" @property="name" />
</BsForm>
`);
assert.dom('input').doesNotHaveClass('is-valid');

await fillIn('input', 'Rosa');
assert.dom('input').doesNotHaveClass('is-valid', 'validation is not shown while user is typing');

await blur('input');
assert.dom('input').hasClass('is-valid', 'validation error is shown after focus out');
});

test('does not break forms which are not using a changeset as model', async function(assert) {
this.set('model', { name: '' });
this.set('submitAction', () => {
assert.step('submit action has been called');
});

await render(hbs`
<BsForm @model={{this.model}} @onSubmit={{this.submitAction}} as |form|>
<form.element @label="Name" @property="name" />
</BsForm>
`);
assert.dom('input').doesNotHaveClass('is-valid');
assert.dom('input').doesNotHaveClass('is-invalid');

await fillIn('input', 'Rosa');
await blur('input');
assert.dom('input').doesNotHaveClass('is-valid');
assert.dom('input').doesNotHaveClass('is-invalid');

await triggerEvent('form', 'submit');
assert.dom('#password input').hasClass('is-invalid', 'password still has error after submit.');
assert.dom('#password-confirmation input').hasClass('is-invalid', 'password confirmation still has error after submit.');
assert.verifySteps(['Invalid action has been called.']);
assert.dom('input').doesNotHaveClass('is-valid');
assert.dom('input').doesNotHaveClass('is-invalid');
assert.verifySteps(['submit action has been called']);
});

test('does not break for forms which are not having a model at all', async function(assert) {
this.set('submitAction', () => {
assert.step('submit action has been called');
});
this.set('noop', () => {});

await render(hbs`
<BsForm @onSubmit={{this.submitAction}} as |form|>
<form.element @label="Name" @property="name" @onChange={{this.noop}} />
</BsForm>
`);
assert.dom('input').doesNotHaveClass('is-valid');
assert.dom('input').doesNotHaveClass('is-invalid');

await fillIn('input', 'Rosa');
await blur('input');
assert.dom('input').doesNotHaveClass('is-valid');
assert.dom('input').doesNotHaveClass('is-invalid');

await triggerEvent('form', 'submit');
assert.dom('input').doesNotHaveClass('is-valid');
assert.dom('input').doesNotHaveClass('is-invalid');
assert.verifySteps(['submit action has been called']);
});
});
Loading