Skip to content

Commit a35bfce

Browse files
authored
Merge pull request #27 from jelhan/support-latest-ember-changeset-versions
support latest version of ember-changeset / ember-changeset-validations
2 parents ef98dbc + 21636fd commit a35bfce

File tree

3 files changed

+1750
-1553
lines changed

3 files changed

+1750
-1553
lines changed
Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,56 @@
1-
import { notEmpty } from '@ember/object/computed';
2-
import { defineProperty, computed } from '@ember/object';
3-
import { A } from '@ember/array';
41
import BsFormElement from 'ember-bootstrap/components/bs-form/element';
2+
import { action } from '@ember/object';
3+
import { dependentKeyCompat } from '@ember/object/compat';
54

6-
export default BsFormElement.extend({
7-
'__ember-bootstrap_subclass' : true,
5+
export default class BsFormElementWithChangesetValidationsSupport extends BsFormElement {
6+
'__ember-bootstrap_subclass' = true;
87

9-
hasValidator: notEmpty('model.validate'),
8+
@dependentKeyCompat
9+
get errors() {
10+
let error = this.model?.error?.[this.property]?.validation;
11+
return error ? [error] : [];
12+
}
13+
14+
get hasValidator() {
15+
return typeof this.model?.validate === 'function';
16+
}
17+
18+
// Ember Changeset does not validate the initial state. Properties are not
19+
// validated until they are set the first time. But Ember Bootstrap may show
20+
// validation results before the property was changed. We need to make sure
21+
// that changeset is validated at that time.
22+
// Ember Bootstrap may show the validation in three cases:
23+
// 1. User triggered one of the events that should cause validation errors to
24+
// be shown (e.g. focus out) by interacting with the form element.
25+
// Ember Bootstrap stores these state in `showOwnValidation` property of
26+
// the form element.
27+
// 2. User submits the form. Ember Bootstrap will show validation errors
28+
// for all form elements in that case. That state is handled by
29+
// `showAllValidations` arguments passed to the form element.
30+
// 3. User passes in a validation error or warning explicilty using
31+
// `customError` or `customWarning` arguments of the form element.
32+
// Ember Bootstrap ensures that the model is valided as part of its submit
33+
// handler. So we can assume that validations are run in second case. Ember
34+
// Bootstrap does not show the validation errors of the model but only the
35+
// custom error and warning if present. So it does not matter if initial
36+
// state is validated or not. That means we only have to handle the first
37+
// case.
38+
// Ember Bootstrap does not provide any API for validation plugins to support
39+
// these needs. We have to override a private method to run the validate
40+
// logic for now.
41+
@action
42+
async showValidationOnHandler(event) {
43+
let validationShowBefore = this.showOwnValidation;
44+
45+
// run original implementation provided by Ember Bootstrap
46+
super.showValidationOnHandler(event);
1047

11-
setupValidations() {
12-
// `Changeset.error` is a getter based on a tracked property. Since it's a
13-
// derived state it's not working together with computed properties smoothly.
14-
// As a work-a-round we observe the `Changeset._errors` computed property
15-
// directly, which holds the state. This is not optimal cause it's private.
16-
// Should refactor to native getter as soon as `<FormElement>` component
17-
// of Ember Bootstrap supports native getters for `FormElement.errors`
18-
// property.
19-
let key = `model.error.${this.get('property')}.validation`;
20-
defineProperty(this, 'errors', computed(`model._errors`, function() {
21-
return A(this.get(key));
22-
}));
48+
// run initial validation if
49+
// - visibility of validations changed
50+
let canValidate = this.hasValidator && this.property;
51+
let validationVisibilityChanged = !validationShowBefore && this.showOwnValidation;
52+
if (canValidate && validationVisibilityChanged) {
53+
await this.model.validate(this.property);
54+
}
2355
}
24-
});
56+
}
Lines changed: 106 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { module, test } from 'qunit';
22
import { setupRenderingTest } from 'ember-qunit';
3-
import { render, triggerEvent, fillIn, blur } from '@ember/test-helpers';
4-
3+
import { render, triggerEvent, fillIn, focus, blur } from '@ember/test-helpers';
54
import hbs from 'htmlbars-inline-precompile';
6-
75
import {
86
validatePresence,
97
validateLength,
10-
validateConfirmation,
11-
validateFormat,
128
} from 'ember-changeset-validations/validators';
139

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

24-
const extendedValidation = {
25-
name: [
26-
validatePresence(true),
27-
validateLength({ min: 4 })
28-
],
29-
email: validateFormat({ type: 'email', allowBlank: true }),
30-
password: [
31-
validateLength({ min: 6 })
32-
],
33-
passwordConfirmation: validateConfirmation({ on: 'password' })
34-
}
35-
36-
test('valid validation is supported as expected', async function(assert) {
20+
test('form is submitted if valid and validation success shown', async function(assert) {
3721
let model = {
3822
name: '1234',
3923
};
@@ -57,7 +41,7 @@ module('Integration | Component | bs form element', function(hooks) {
5741
assert.verifySteps(['submit action has been called.']);
5842
});
5943

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

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

86-
test('more complicated validations', async function(assert) {
87-
let model = {
88-
name: '',
89-
password: null,
90-
passwordConfirmation: null,
91-
email: '',
92-
};
73+
await render(hbs`
74+
<BsForm @model={{changeset this.model this.validation}} as |form|>
75+
<form.element @label="Name" @property="name" />
76+
</BsForm>
77+
`);
78+
assert.dom('input').doesNotHaveClass('is-invalid');
9379

94-
this.set('model', model);
95-
this.set('validation', extendedValidation);
96-
this.submitAction = function() {
97-
assert.ok(false, 'submit action must not been called.');
98-
};
99-
this.invalidAction = function() {
100-
assert.step('Invalid action has been called.');
101-
};
80+
await focus('input');
81+
await blur('input');
82+
assert.dom('input').hasClass('is-invalid');
83+
});
84+
85+
test('validation success is shown after blur', async function(assert) {
86+
this.set('model', { name: 'Clara' });
87+
this.set('validation', validation);
10288

10389
await render(hbs`
104-
<BsForm @model={{changeset this.model this.validation}} @onSubmit={{this.submitAction}} @onInvalid={{this.invalidAction}} as |form|>
105-
<form.element id="name" @label="Name" @property="name" />
106-
<form.element id="email" @label="Email" @property="email" />
107-
<form.element id="password" @label="Password" @property="password" />
108-
<form.element id="password-confirmation" @label="Password confirmation" @property="passwordConfirmation" />
90+
<BsForm @model={{changeset this.model this.validation}} as |form|>
91+
<form.element @label="Name" @property="name" />
10992
</BsForm>
11093
`);
94+
assert.dom('input').doesNotHaveClass('is-valid');
11195

112-
await fillIn('#password input', 'bad');
113-
assert.dom('#password input').doesNotHaveClass('is-invalid', 'password does not have error while typing.');
114-
assert.dom('#password input').doesNotHaveClass('is-valid', 'password does not have success while typing.');
96+
await focus('input');
97+
await blur('input');
98+
assert.dom('input').hasClass('is-valid');
99+
});
115100

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

119-
await fillIn('#password-confirmation input', 'betterpass');
120-
assert.dom('#password-confirmation input').doesNotHaveClass('is-invalid', 'password confirmation does not have error while typing.');
105+
await render(hbs`
106+
<BsForm @model={{changeset this.model this.validation}} as |form|>
107+
<form.element @label="Name" @property="name" />
108+
</BsForm>
109+
`);
110+
assert.dom('input').doesNotHaveClass('is-invalid');
111+
112+
await fillIn('input', 'R');
113+
assert.dom('input').doesNotHaveClass('is-invalid', 'validation is not shown while user is typing');
114+
115+
await blur('input');
116+
assert.dom('input').hasClass('is-invalid', 'validation error is shown after focus out');
117+
});
121118

122-
await blur('#password-confirmation input');
123-
assert.dom('#password-confirmation input').hasClass('is-invalid', 'password confirmation does have error when focus out.');
119+
test('validation success is shown after user input', async function(assert) {
120+
this.set('model', { name: '' });
121+
this.set('validation', validation);
122+
123+
await render(hbs`
124+
<BsForm @model={{changeset this.model this.validation}} as |form|>
125+
<form.element @label="Name" @property="name" />
126+
</BsForm>
127+
`);
128+
assert.dom('input').doesNotHaveClass('is-valid');
129+
130+
await fillIn('input', 'Rosa');
131+
assert.dom('input').doesNotHaveClass('is-valid', 'validation is not shown while user is typing');
132+
133+
await blur('input');
134+
assert.dom('input').hasClass('is-valid', 'validation error is shown after focus out');
135+
});
136+
137+
test('does not break forms which are not using a changeset as model', async function(assert) {
138+
this.set('model', { name: '' });
139+
this.set('submitAction', () => {
140+
assert.step('submit action has been called');
141+
});
142+
143+
await render(hbs`
144+
<BsForm @model={{this.model}} @onSubmit={{this.submitAction}} as |form|>
145+
<form.element @label="Name" @property="name" />
146+
</BsForm>
147+
`);
148+
assert.dom('input').doesNotHaveClass('is-valid');
149+
assert.dom('input').doesNotHaveClass('is-invalid');
150+
151+
await fillIn('input', 'Rosa');
152+
await blur('input');
153+
assert.dom('input').doesNotHaveClass('is-valid');
154+
assert.dom('input').doesNotHaveClass('is-invalid');
124155

125156
await triggerEvent('form', 'submit');
126-
assert.dom('#password input').hasClass('is-invalid', 'password still has error after submit.');
127-
assert.dom('#password-confirmation input').hasClass('is-invalid', 'password confirmation still has error after submit.');
128-
assert.verifySteps(['Invalid action has been called.']);
157+
assert.dom('input').doesNotHaveClass('is-valid');
158+
assert.dom('input').doesNotHaveClass('is-invalid');
159+
assert.verifySteps(['submit action has been called']);
129160
});
130161

162+
test('does not break for forms which are not having a model at all', async function(assert) {
163+
this.set('submitAction', () => {
164+
assert.step('submit action has been called');
165+
});
166+
this.set('noop', () => {});
167+
168+
await render(hbs`
169+
<BsForm @onSubmit={{this.submitAction}} as |form|>
170+
<form.element @label="Name" @property="name" @onChange={{this.noop}} />
171+
</BsForm>
172+
`);
173+
assert.dom('input').doesNotHaveClass('is-valid');
174+
assert.dom('input').doesNotHaveClass('is-invalid');
175+
176+
await fillIn('input', 'Rosa');
177+
await blur('input');
178+
assert.dom('input').doesNotHaveClass('is-valid');
179+
assert.dom('input').doesNotHaveClass('is-invalid');
180+
181+
await triggerEvent('form', 'submit');
182+
assert.dom('input').doesNotHaveClass('is-valid');
183+
assert.dom('input').doesNotHaveClass('is-invalid');
184+
assert.verifySteps(['submit action has been called']);
185+
});
131186
});

0 commit comments

Comments
 (0)