Skip to content

Commit ccbdfb3

Browse files
committed
Convert to EmberCLI addon
1 parent f263fc2 commit ccbdfb3

File tree

7 files changed

+341
-6
lines changed

7 files changed

+341
-6
lines changed

addon/components/validatable-form.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import Ember from 'ember';
2+
3+
export default Ember.Component.extend({
4+
/**
5+
* @type {string}
6+
*/
7+
tagName: 'form',
8+
9+
/**
10+
* @type {Array}
11+
*/
12+
attributeBindings: ['novalidate'],
13+
14+
/**
15+
* Prevent the built-in browser navigation error messages to pop up
16+
*
17+
* @type {boolean}
18+
*/
19+
novalidate: true,
20+
21+
/**
22+
* Optional Ember-Data model from where to fetch server-side errors
23+
*
24+
* @type {DS.Model|null}
25+
*/
26+
model: null,
27+
28+
/**
29+
* Send the action bound to the submit event if the form is valid
30+
*
31+
* @returns {boolean}
32+
*/
33+
submit: function() {
34+
var form = this.get('element'),
35+
model = this.get('model');
36+
37+
if (form.checkValidity() && model.get('isValid') !== false && model.get('isDirty') !== false) {
38+
this.sendAction('action', model);
39+
} else {
40+
this.scrollToFirstError();
41+
}
42+
43+
return false;
44+
},
45+
46+
/**
47+
* Alias the enter button to submit the form
48+
*
49+
* @returns {boolean}
50+
*/
51+
keyDown: function(event) {
52+
// Enter key
53+
if (event.keyCode === 13 && event.target.tagName !== 'textarea') {
54+
this.submit();
55+
56+
// Prevent other buttons to accidentally submit
57+
event.preventDefault();
58+
return false;
59+
}
60+
61+
return true;
62+
},
63+
64+
/**
65+
* Extract server-side errors from Ember-Data model
66+
*
67+
* @returns {void}
68+
*/
69+
extractServerErrors: function() {
70+
var errors = this.get('model.errors'),
71+
childViews = this.get('childViews');
72+
73+
// This thing can be pretty inefficient if you have lot of form elements... we need to find
74+
// a better way!
75+
errors.forEach(function(item) {
76+
var attribute = Ember.String.dasherize(item.attribute),
77+
childViewLength = childViews.get('length');
78+
79+
for (var i = 0 ; i !== childViewLength ; ++i) {
80+
if (attribute === childViews[i].get('elementId')) {
81+
childViews[i].setCustomErrorMessage(item.message);
82+
break;
83+
}
84+
}
85+
});
86+
87+
this.scrollToFirstError();
88+
}.observes('model.errors.length'),
89+
90+
/**
91+
* Scroll to the first input field that does not pass the validation
92+
*
93+
* @returns {void}
94+
*/
95+
scrollToFirstError: function() {
96+
var form = this.get('element');
97+
98+
// We get the first element that fails, and scroll to it
99+
for (var i = 0 ; i !== form.elements.length ; ++i) {
100+
if (!form.elements[i].validity.valid) {
101+
Ember.$('html, body').animate({
102+
scrollTop: Ember.$(form.elements[i]).offset().top - 40
103+
}, 200);
104+
105+
break;
106+
}
107+
}
108+
}
109+
});

addon/ext/checkbox.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Ember from 'ember';
2+
import ValidatableInput from '../mixins/validatable-input';
3+
4+
/**
5+
* @namespace Ember
6+
* @class Checkbox
7+
*/
8+
Ember.Checkbox.reopen(ValidatableInput);

addon/ext/text-area.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Ember from 'ember';
2+
import ValidatableInput from '../mixins/validatable-input';
3+
4+
/**
5+
* @namespace Ember
6+
* @class TextArea
7+
*/
8+
Ember.TextArea.reopen(ValidatableInput);

addon/ext/text-field.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Ember from 'ember';
2+
import ValidatableInput from '../mixins/validatable-input';
3+
4+
/**
5+
* @namespace Ember
6+
* @class TextField
7+
*/
8+
Ember.TextField.reopen(ValidatableInput);

addon/mixins/validatable-input.js

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import Ember from 'ember';
2+
3+
/**
4+
* Simple mixin that wrap the logic of getting and rendering an input error message
5+
*/
6+
export default Ember.Mixin.create({
7+
/**
8+
* Title attribute is needed for providing a custom message
9+
*
10+
* @type {Array}
11+
*/
12+
attributeBindings: ['title'],
13+
14+
/**
15+
* Decide if we show the native browser error messages
16+
*
17+
* @type {boolean}
18+
*/
19+
useBrowserMessages: false,
20+
21+
/**
22+
* Current error message for the field
23+
*
24+
* @type {string}
25+
*/
26+
errorMessage: null,
27+
28+
/**
29+
* Allow to override error messages
30+
*
31+
* @type {Object}
32+
*/
33+
errorTemplates: {
34+
// Errors when an input with "required" attribute has no value
35+
valueMissing: {
36+
defaultMessage: 'Value is required',
37+
checkbox: 'You must check this box',
38+
select: 'You must select at least an option',
39+
radio: 'You must select an option'
40+
},
41+
42+
// Errors when a value does not match a given type like "url" or "email"
43+
typeMismatch: {
44+
defaultMessage: 'Value is invalid',
45+
email: 'Email address is invalid',
46+
url: 'URL is invalid'
47+
},
48+
49+
// Errors when a value does not follow the "pattern" regex
50+
patternMismatch: {
51+
defaultMessage: 'Value does not follow expected pattern'
52+
},
53+
54+
// Errors when an input is too long
55+
tooLong: {
56+
defaultMessage: 'Enter at most %@ characters'
57+
},
58+
59+
// Errors when an input is less than "min" value
60+
rangeUnderflow: {
61+
defaultMessage: 'Number must be more than %@'
62+
},
63+
64+
// Errors when an input is more than "max" value
65+
rangeOverflow: {
66+
defaultMessage: 'Number must be less than %@'
67+
},
68+
69+
// Errors when a value does not follow step (for instance for "range" type)
70+
stepMismatch: {
71+
defaultMessage: 'Value is invalid'
72+
},
73+
74+
// Default message that is used when none is matched
75+
defaultMessage: 'Value is invalid'
76+
},
77+
78+
/**
79+
* @returns {void}
80+
*/
81+
attachValidationListener: function() {
82+
Ember.$(this.get('element')).on('invalid', Ember.run.bind(this, this.validate));
83+
}.on('didInsertElement'),
84+
85+
/**
86+
* @returns {void}
87+
*/
88+
detachValidationListener: function() {
89+
Ember.$(this.get('element')).off('invalid');
90+
}.on('willDestroyElement'),
91+
92+
/**
93+
* Validate the input whenever it looses focus
94+
*
95+
* @returns {void}
96+
*/
97+
validate: function() {
98+
var input = this.get('element');
99+
100+
// According to spec, inputs that have "formnovalidate" should bypass any validation
101+
if (input.hasAttribute('formnovalidate')) {
102+
return;
103+
}
104+
105+
if (!input.validity.valid && !input.validity.customError) {
106+
this.set('errorMessage', this.getErrorMessage());
107+
} else {
108+
this.set('errorMessage', null);
109+
input.setCustomValidity('');
110+
}
111+
}.on('focusOut'),
112+
113+
/**
114+
* Set a custom error message for the input. Note that we set the error message directly, as well as we
115+
* set the error using setCustomValidity, so that a call to checkValidate evaluate to false
116+
*
117+
* @type {string} error
118+
* @returns {void}
119+
*/
120+
setCustomErrorMessage: function(error) {
121+
this.set('errorMessage', error);
122+
this.get('element').setCustomValidity(error);
123+
},
124+
125+
/**
126+
* Render the error message bound to the field (or remove if it is null)
127+
*
128+
* @TODO: this should be done in a more flexible way to allow custom template
129+
*/
130+
renderErrorMessage: function() {
131+
var element = this.$(),
132+
errorMessage = this.get('errorMessage');
133+
134+
if (null === errorMessage) {
135+
element.removeClass('invalid');
136+
element.siblings('.input-error').remove();
137+
} else {
138+
element.siblings('.input-error').remove();
139+
element.addClass('invalid');
140+
element.after('<p class="input-error">' + errorMessage + '</p>');
141+
}
142+
}.observes('errorMessage'),
143+
144+
/**
145+
* Get the message error
146+
*
147+
* @returns {String}
148+
*/
149+
getErrorMessage: function() {
150+
var target = this.get('element');
151+
152+
// If user want to use native browser error messages, we directly return
153+
if (this.get('useBrowserMessages')) {
154+
return target.validationMessage;
155+
}
156+
157+
var errorTemplates = this.get('errorTemplates'),
158+
type = target.getAttribute('type');
159+
160+
// We first check for the "required" case
161+
if (target.validity.valueMissing) {
162+
// For checkbox, we allow to have a title attribute that is shown instead of the
163+
// required message. Very useful for things like "You must accept our terms"
164+
if (type === 'checkbox' && target.hasAttribute('title')) {
165+
return target.getAttribute('title');
166+
}
167+
168+
return errorTemplates.valueMissing[type] || errorTemplates.valueMissing['defaultMessage'];
169+
}
170+
171+
// If a "title" attribute has been set, according to the spec, we can use it as the message
172+
if (target.hasAttribute('title')) {
173+
return target.getAttribute('title');
174+
}
175+
176+
var errorKeys = ['stepMismatch', 'rangeOverflow', 'rangeUnderflow', 'tooLong', 'patternMismatch', 'typeMismatch'];
177+
178+
for (var i = 0 ; i !== errorKeys.length ; ++i) {
179+
var errorKey = errorKeys[i];
180+
181+
if (!target.validity[errorKey]) {
182+
continue;
183+
}
184+
185+
var message = errorTemplates[errorKey][type] || errorTemplates[errorKey]['defaultMessage'];
186+
187+
switch (errorKey) {
188+
case 'tooLong':
189+
return message.fmt(target.getAttribute('maxlength'));
190+
case 'rangeUnderflow':
191+
return message.fmt(target.getAttribute('min'));
192+
case 'rangeOverflow':
193+
return message.fmt(target.getAttribute('max'));
194+
default:
195+
return message;
196+
}
197+
}
198+
199+
return errorTemplates.defaultMessage;
200+
}
201+
});

bower.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
"name": "ember-html5-validation",
33
"dependencies": {
44
"handlebars": "~1.3.0",
5-
"jquery": "^1.11.1",
5+
"jquery": "~2.0",
66
"ember": "1.7.0",
7-
"ember-data": "1.0.0-beta.10",
87
"ember-resolver": "~0.1.7",
98
"loader.js": "stefanpenner/loader.js#1.0.1",
109
"ember-cli-shims": "stefanpenner/ember-cli-shims#0.0.3",
@@ -14,4 +13,4 @@
1413
"ember-qunit-notifications": "0.0.4",
1514
"qunit": "~1.15.0"
1615
}
17-
}
16+
}

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ember-html5-validation",
3-
"version": "0.0.0",
3+
"version": "0.1.0",
44
"directories": {
55
"doc": "doc",
66
"test": "tests"
@@ -31,9 +31,11 @@
3131
"glob": "^4.0.5"
3232
},
3333
"keywords": [
34-
"ember-addon"
34+
"ember-addon",
35+
"validation",
36+
"form"
3537
],
3638
"ember-addon": {
3739
"configPath": "tests/dummy/config"
3840
}
39-
}
41+
}

0 commit comments

Comments
 (0)