Permalink
Fetching contributors…
Cannot retrieve contributors at this time. Cannot retrieve contributors at this time
256 lines (251 sloc) 8.42 KB
/*$AMPERSAND_VERSION*/
var View = require('ampersand-view');
var dom = require('ampersand-dom');
var matchesSelector = require('matches-selector');
var slice = Array.prototype.slice;
function getMatches(el, selector) {
if (selector === '') return [el];
var matches = [];
if (matchesSelector(el, selector)) matches.push(el);
return matches.concat(slice.call(el.querySelectorAll(selector)));
}
module.exports = View.extend({
template: [
'<label>',
'<span data-hook="label"></span>',
'<input class="form-input">',
'<div data-hook="message-container" class="message message-below message-error">',
'<p data-hook="message-text"></p>',
'</div>',
'</label>'
].join(''),
bindings: {
'name': {
type: 'attribute',
selector: 'input, textarea',
name: 'name'
},
'tabindex': {
type: 'attribute',
selector: 'input, textarea',
name: 'tabindex'
},
'label': [
{
hook: 'label'
},
{
type: 'toggle',
hook: 'label'
}
],
'message': {
type: 'text',
hook: 'message-text'
},
'showMessage': {
type: 'toggle',
hook: 'message-container'
},
'placeholder': {
type: 'attribute',
selector: 'input, textarea',
name: 'placeholder'
},
'readonly': {
type: 'booleanAttribute',
name: 'readonly',
selector: 'input, textarea'
},
'autofocus': {
type: 'booleanAttribute',
name: 'autofocus',
selector: 'input, textarea'
}
},
initialize: function (spec) {
spec || (spec = {});
this.tests = this.tests || spec.tests || [];
this.on('change:type', this.handleTypeChange, this);
this.handleChange = this.handleChange.bind(this);
this.handleInputChanged = this.handleInputChanged.bind(this);
var value = !spec.value && spec.value !== 0 ? '' : spec.value;
this.startingValue = value;
this.inputValue = value;
this.on('change:valid change:value', this.reportToParent, this);
this.on('change:validityClass', this.validityClassChanged, this);
if (spec.autoRender) this.autoRender = spec.autoRender;
if (spec.template) this.template = spec.template;
if (spec.beforeSubmit) this.beforeSubmit = spec.beforeSubmit;
},
render: function () {
this.renderWithTemplate();
this.input = this.query('input') || this.query('textarea');
// switches out input for textarea if that's what we want
this.handleTypeChange();
this.initInputBindings();
// Skip validation on initial setValue
// if the field is not required
this.setValue(this.inputValue, !this.required);
return this;
},
props: {
inputValue: 'any',
startingValue: 'any',
name: 'string',
type: ['string', true, 'text'],
placeholder: ['string', true, ''],
label: ['string', true, ''],
required: ['boolean', true, true],
directlyEdited: ['boolean', true, false],
readonly: ['boolean', true, false],
autofocus: ['boolean', true, false],
shouldValidate: ['boolean', true, false],
message: ['string', true, ''],
requiredMessage: ['string', true, 'This field is required.'],
validClass: ['string', true, 'input-valid'],
invalidClass: ['string', true, 'input-invalid'],
validityClassSelector: ['string', true, 'input, textarea'],
tabindex: ['number', true, 0]
},
derived: {
value: {
deps: ['inputValue'],
fn: function () {
return this.inputValue;
}
},
valid: {
cache: false,
deps: ['inputValue'],
fn: function () {
return !this.runTests();
}
},
showMessage: {
deps: ['message', 'shouldValidate'],
fn: function () {
return this.shouldValidate && this.message;
}
},
changed: {
deps: ['inputValue', 'startingValue'],
fn: function () {
return this.inputValue !== this.startingValue;
}
},
validityClass: {
deps: ['valid', 'validClass', 'invalidClass', 'shouldValidate'],
fn: function () {
if (!this.shouldValidate) {
return '';
} else {
return this.valid ? this.validClass : this.invalidClass;
}
}
}
},
setValue: function (value, skipValidation) {
if (!this.input) {
this.inputValue = value;
return;
}
if (!value && value !== 0) {
this.input.value = '';
} else {
this.input.value = value.toString();
}
this.inputValue = this.clean(this.input.value);
if (!skipValidation && !this.getErrorMessage()) {
this.shouldValidate = true;
} else if (skipValidation) {
this.shouldValidate = false;
}
},
getErrorMessage: function () {
var message = '';
if (this.required && this.value === '') {
return this.requiredMessage;
} else {
(this.tests || []).some(function (test) {
message = test.call(this, this.value) || '';
return message;
}, this);
return message;
}
},
handleTypeChange: function () {
if (this.type === 'textarea' && this.input.tagName.toLowerCase() !== 'textarea') {
var parent = this.input.parentNode;
var textarea = document.createElement('textarea');
parent.replaceChild(textarea, this.input);
this.input = textarea;
this._applyBindingsForKey('');
} else {
this.input.type = this.type;
}
},
clean: function (val) {
return (this.type === 'number') ? Number(val) : val.trim();
},
//`input` event handler
handleInputChanged: function () {
if (document.activeElement === this.input) {
this.directlyEdited = true;
}
this.inputValue = this.clean(this.input.value);
},
//`change` event handler
handleChange: function () {
if (this.inputValue && this.changed) {
this.shouldValidate = true;
}
this.runTests();
},
beforeSubmit: function () {
// catch undetected input changes that were not caught due to lack of
// browser event firing see:
// https://github.com/AmpersandJS/ampersand-input-view/issues/2
this.inputValue = this.clean(this.input.value);
// at the point where we've tried
// to submit, we want to validate
// everything from now on.
this.shouldValidate = true;
this.runTests();
},
runTests: function () {
var message = this.getErrorMessage();
if (!message && this.inputValue && this.changed) {
// if it's ever been valid,
// we want to validate from now
// on.
this.shouldValidate = true;
}
this.message = message;
return message;
},
initInputBindings: function () {
this.input.addEventListener('input', this.handleInputChanged, false);
this.input.addEventListener('change', this.handleChange,false);
},
remove: function () {
this.input.removeEventListener('input', this.handleInputChanged, false);
this.input.removeEventListener('change', this.handleChange, false);
View.prototype.remove.apply(this, arguments);
},
reset: function () {
this.setValue(this.startingValue, true); //Skip validation just like on initial render
},
clear: function () {
this.setValue('', true);
},
validityClassChanged: function (view, newClass) {
var oldClass = view.previousAttributes().validityClass;
getMatches(this.el, this.validityClassSelector).forEach(function (match) {
dom.switchClass(match, oldClass, newClass);
});
},
reportToParent: function () {
if (this.parent) this.parent.update(this);
}
});