Skip to content

Commit 36d691d

Browse files
committed
do not show errors before user interaction
Errors aren't shown before a user interaction. If user interacts with a field errors for that field are shown. If user submits the form, errors for all field of the form are shown. What is treated as an user interaction differs by field. Only if an interaction could be seen as finished, it's causing errors to be shown. All focus out events are treated as finished user interactions. A change event on an input field isn't to prevent errors from popping up, while user inserts a valid string (e.g. if a minimum length is required). On the other hand an change on select and checkbox is. This topic was discussed intensively in Emerson#15. There were many different ideas how it could be implemented. This PR tries to reflect the last state of the debate. In opposite to branch feature/delayed-validations this is not optional. I don't think there are use cases where you want to show errors for untouched fields to the user. If there are use cases one could pass `true` to `showErrors` property of fields. There shouldn't be any changes for people using server-side validations, since there these ones aren't present before a form is submitted.
1 parent 7030429 commit 36d691d

19 files changed

+338
-45
lines changed

addon/components/fm-checkbox.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,28 @@ export default Ember.Component.extend({
66
classNameBindings: ['checkboxWrapperClass', 'errorClass'],
77
fmConfig: Ember.inject.service('fm-config'),
88
checkboxWrapperClass: Ember.computed.reads('fmConfig.checkboxWrapperClass'),
9-
errorClass: Ember.computed('errors', 'fmConfig.errorClass', function() {
10-
if(!Ember.isEmpty(this.get('errors'))) {
9+
errorClass: Ember.computed('showErrors', 'fmConfig.errorClass', function() {
10+
if(this.get('showErrors')) {
1111
return this.get('fmConfig.errorClass');
1212
}
13-
})
13+
}),
14+
15+
shouldShowErrors: false,
16+
showErrors: Ember.computed('shouldShowErrors', 'errors', function() {
17+
return this.get('shouldShowErrors') && !Ember.isEmpty(this.get('errors'));
18+
}),
19+
20+
change() {
21+
this.send('userInteraction');
22+
},
23+
24+
focusOut() {
25+
this.send('userInteraction');
26+
},
27+
28+
actions: {
29+
userInteraction() {
30+
this.set('shouldShowErrors', true);
31+
}
32+
}
1433
});

addon/components/fm-field.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export default Ember.Component.extend({
2929
placeholder: null,
3030
label: null,
3131
classNameBindings: ['wrapperClass', 'errorClass'],
32-
errorClass: Ember.computed('errors', 'fmConfig.errorClass', function() {
33-
if(!Ember.isEmpty(this.get('errors'))) {
32+
errorClass: Ember.computed('showErrors', 'fmConfig.errorClass', function() {
33+
if (this.get('showErrors')) {
3434
return this.get('fmConfig.errorClass');
3535
}
3636
}),
@@ -67,6 +67,15 @@ export default Ember.Component.extend({
6767
} else {
6868
this.set('value', value);
6969
}
70+
},
71+
72+
userInteraction() {
73+
this.set('shouldShowErrors', true);
7074
}
71-
}
75+
},
76+
77+
shouldShowErrors: false,
78+
showErrors: Ember.computed('shouldShowErrors', 'errors', function() {
79+
return this.get('shouldShowErrors') && !Ember.isEmpty(this.get('errors'));
80+
})
7281
});

addon/components/fm-form.js

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export default Ember.Component.extend({
1111
'for': null,
1212
submit: function(e) {
1313
e.preventDefault();
14+
this.get('childViews').forEach((chieldView) => {
15+
if (chieldView.get('shouldShowErrors') === false) {
16+
chieldView.set('shouldShowErrors', true);
17+
}
18+
});
1419
this.sendAction('action', this.get('for'));
1520
}
1621
});

addon/components/fm-input.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import DataAttributesSupport from '../mixins/data-attribute-support';
33

44
export default Ember.TextField.extend(DataAttributesSupport, {
55

6+
focusOut() {
7+
this.sendAction('onUserInteraction');
8+
},
9+
610
init: function() {
711
if(this.get('parentView.forAttribute')) {
812
this.set('elementId', this.get('parentView.forAttribute'));
@@ -11,4 +15,4 @@ export default Ember.TextField.extend(DataAttributesSupport, {
1115
this.setDataAttributes();
1216
}
1317

14-
});
18+
});

addon/components/fm-radio-group.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,22 @@ export default Ember.Component.extend({
55
layout: layout,
66
classNameBindings: ['radioGroupWrapperClass', 'errorClass'],
77
fmConfig: Ember.inject.service('fm-config'),
8-
errorClass: Ember.computed('errors', 'fmConfig.errorClass', function() {
9-
if(!Ember.isEmpty(this.get('errors'))) {
8+
errorClass: Ember.computed('showErrors', 'fmConfig.errorClass', function() {
9+
if(this.get('showErrors')) {
1010
return this.get('fmConfig.errorClass');
1111
}
1212
}),
1313
radioGroupWrapperClass: Ember.computed.reads('fmConfig.radioGroupWrapperClass'),
14-
labelClass: Ember.computed.reads('fmConfig.labelClass')
14+
labelClass: Ember.computed.reads('fmConfig.labelClass'),
15+
16+
shouldShowErrors: false,
17+
showErrors: Ember.computed('errors', 'shouldShowErrors', function() {
18+
return this.get('shouldShowErrors') && !Ember.isEmpty(this.get('errors'));
19+
}),
20+
21+
actions: {
22+
userInteraction() {
23+
this.set('shouldShowErrors', true);
24+
}
25+
}
1526
});

addon/components/fm-radio.js

+4
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,9 @@ export default Ember.Component.extend({
2626
}),
2727
change: function() {
2828
this.set('parentView.value', this.get('value'));
29+
this.sendAction('onUserInteraction');
30+
},
31+
focusOut() {
32+
this.sendAction('onUserInteraction');
2933
}
3034
});

addon/components/fm-select.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default Ember.Component.extend({
3535

3636
change: function() {
3737
this.send('change');
38+
this.sendAction('onUserInteraction');
3839
// console.log('changing');
3940
},
4041

@@ -63,6 +64,9 @@ export default Ember.Component.extend({
6364
const value = (path.length > 0)? Ember.get(selection, path) : selection;
6465
this.attrs.action(value);
6566
}
66-
}
67+
},
6768

69+
focusOut() {
70+
this.sendAction('onUserInteraction');
71+
}
6872
});

addon/components/fm-textarea.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import Ember from 'ember';
22
import DataAttributesSupport from 'ember-form-master-2000/mixins/data-attribute-support';
33

44
export default Ember.TextArea.extend(DataAttributesSupport, {
5+
focusOut() {
6+
this.sendAction('onUserInteraction');
7+
},
58

69
init: function() {
710
if(this.get('parentView.forAttribute')) {
@@ -11,4 +14,4 @@ export default Ember.TextArea.extend(DataAttributesSupport, {
1114
this.setDataAttributes();
1215
}
1316

14-
});
17+
});

addon/templates/components/ember-form-master-2000/fm-checkbox.hbs

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
{{label}}
1111
</label>
1212

13-
{{fm-errortext errors=errors}}
13+
{{#if showErrors}}
14+
{{fm-errortext errors=errors}}
15+
{{/if}}
1416

15-
</div>
17+
</div>

addon/templates/components/ember-form-master-2000/fm-field.hbs

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
value=value
99
classNameBindings='errorClass inputClass'
1010
maxlength=maxlength
11-
placeholder=placeholder}}
11+
placeholder=placeholder
12+
onUserInteraction='userInteraction'
13+
}}
1214
{{/if}}
1315

1416
{{#if isSelect}}
@@ -19,6 +21,7 @@
1921
prompt=prompt
2022
value=value
2123
action=(action 'selectAction')
24+
onUserInteraction='userInteraction'
2225
}}
2326
{{/if}}
2427

@@ -32,10 +35,11 @@
3235
maxlength=maxlength
3336
spellcheck=spellcheck
3437
disabled=disabled
38+
onUserInteraction='userInteraction'
3539
}}
3640
{{/if}}
3741

38-
{{#if errors}}
42+
{{#if showErrors}}
3943
{{fm-errortext errors=errors}}
4044
{{/if}}
4145

addon/templates/components/ember-form-master-2000/fm-radio-group.hbs

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
{{#each content as |option|}}
66

7-
{{fm-radio content=option}}
7+
{{fm-radio content=option onUserInteraction='userInteraction'}}
88

99
{{/each}}
1010

11-
{{fm-errortext errors=errors}}
11+
{{#if showErrors}}
12+
{{fm-errortext errors=errors}}
13+
{{/if}}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
// import { moduleForComponent, test } from 'ember-qunit';
2-
// import hbs from 'htmlbars-inline-precompile';
1+
import { moduleForComponent, test } from 'ember-qunit';
2+
import hbs from 'htmlbars-inline-precompile';
33
// import {initialize} from 'ember-form-master-2000/initializers/fm-initialize';
4-
//
5-
// moduleForComponent('fm-checkbox', 'Integration | Component | fm-checkbox', {
6-
// integration: true,
7-
// setup: function() {
8-
// this.container.inject = this.container.injection;
9-
// initialize(null, this.container);
10-
// }
11-
// });
12-
//
4+
5+
moduleForComponent('fm-checkbox', 'Integration | Component | fm-checkbox', {
6+
integration: true,
7+
setup: function() {
8+
this.container.inject = this.container.injection;
9+
// initialize(null, this.container);
10+
}
11+
});
12+
1313
// test('fm-checkbox renders properly', function(assert) {
1414
// assert.expect(2);
1515
// this.render(hbs `{{fm-checkbox}}`);
@@ -24,3 +24,40 @@
2424
//
2525
// assert.equal(this.$('label').text().trim(), 'This is the label', 'the fm-checkbox label matches');
2626
// });
27+
28+
test('errors are shown after user interaction but not before', function(assert) {
29+
this.set('errors', ['error message']);
30+
this.render(hbs `{{fm-checkbox errors=errors}}`);
31+
assert.ok(
32+
this.$('.help-block').length === 0,
33+
'error message is not shown before user interaction'
34+
);
35+
assert.notOk(
36+
this.$('div').hasClass('has-error'),
37+
'there is no errorClass before user interaction'
38+
);
39+
this.$('input').trigger('focusout');
40+
assert.equal(
41+
this.$('.help-block').text().trim(), 'error message',
42+
'error message is shown after user interaction'
43+
);
44+
assert.ok(
45+
this.$('div').hasClass('has-error'),
46+
'errorClass is added after user interaction'
47+
);
48+
this.set('errors', []);
49+
assert.notOk(
50+
this.$('div').hasClass('has-error'),
51+
'errorClass is removed when errors array got empty'
52+
);
53+
});
54+
55+
test('change event is treated as userInteraction', function(assert) {
56+
this.set('errors', ['error message']);
57+
this.render(hbs `{{fm-checkbox errors=errors}}`);
58+
this.$('input').change();
59+
assert.ok(
60+
this.$('div').hasClass('has-error'),
61+
'errorClass is added after change event'
62+
);
63+
});

tests/integration/components/fm-field-test.js

+82
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,85 @@ test('action is passed down to select component', function(assert) {
8888
// assert.equal(this.$('label').attr('for'), 'example-id');
8989
// assert.equal(this.$('input').attr('id'), 'example-id');
9090
// });
91+
92+
test('errors are not shown after user interaction but not before', function(assert) {
93+
this.set('errors', ['error message']);
94+
this.render(hbs `{{fm-field errors=errors}}`);
95+
assert.ok(
96+
this.$('.help-block').length === 0,
97+
'error message is not shown before user interaction'
98+
);
99+
assert.notOk(
100+
this.$('div').hasClass('has-error'),
101+
'errorClass is not there before user interaction'
102+
);
103+
this.$('input').trigger('focusout');
104+
assert.ok(
105+
this.$('.help-block').text().trim(), 'error message',
106+
'error message is shown after user interaction'
107+
);
108+
assert.ok(
109+
this.$('div').hasClass('has-error'),
110+
'errorClass is added after user interaction'
111+
);
112+
this.set('errors', []);
113+
assert.notOk(
114+
this.$('div').hasClass('has-error'),
115+
'errorClass is removed when errors empty got empty'
116+
);
117+
});
118+
119+
test('errors are not shown after user interaction but not before (textarea)', function(assert) {
120+
this.set('errors', ['error message']);
121+
this.render(hbs `{{fm-field type='textarea' errors=errors}}`);
122+
assert.ok(
123+
this.$('.help-block').length === 0,
124+
'error message is not shown before user interaction'
125+
);
126+
assert.notOk(
127+
this.$('div').hasClass('has-error'),
128+
'errorClass is not there before user interaction'
129+
);
130+
this.$('textarea').trigger('focusout');
131+
assert.ok(
132+
this.$('.help-block').text().trim(), 'error message',
133+
'error message is shown after user interaction'
134+
);
135+
assert.ok(
136+
this.$('div').hasClass('has-error'),
137+
'errorClass is added after user interaction'
138+
);
139+
this.set('errors', []);
140+
assert.notOk(
141+
this.$('div').hasClass('has-error'),
142+
'errorClass is removed when errors empty got empty'
143+
);
144+
});
145+
146+
test('errors are not shown after user interaction but not before (select)', function(assert) {
147+
this.set('errors', ['error message']);
148+
this.render(hbs `{{fm-field type='select' errors=errors}}`);
149+
assert.ok(
150+
this.$('.help-block').length === 0,
151+
'error message is not shown before user interaction'
152+
);
153+
assert.notOk(
154+
this.$('div').hasClass('has-error'),
155+
'errorClass is not there before user interaction'
156+
);
157+
this.$('select').trigger('focusout');
158+
assert.ok(
159+
this.$('.help-block').text().trim(), 'error message',
160+
'error message is shown after user interaction'
161+
);
162+
assert.ok(
163+
this.$('div').hasClass('has-error'),
164+
'errorClass is added after user interaction'
165+
);
166+
this.set('errors', []);
167+
assert.notOk(
168+
this.$('div').hasClass('has-error'),
169+
'errorClass is removed when errors empty got empty'
170+
);
171+
});
172+

0 commit comments

Comments
 (0)