ampersand-array-input-view
Version:
A view module for intelligently rendering and validating inputs that should produce an array of values. Works well with ampersand-form-view.
206 lines (203 loc) • 6.49 kB
JavaScript
/*$AMPERSAND_VERSION*/
var View = require('ampersand-view');
var _ = require('underscore');
var FieldView = require('./lib/field-view');
var defaultTemplate = [
'<div>',
'<span data-hook="label"></span>',
'<div data-hook="field-container"></div>',
'<a data-hook="add-field" class="add-input">add</a>',
'<div data-hook="main-message-container" class="message message-below message-error">',
'<p data-hook="main-message-text"></p>',
'</div>',
'</div>'
].join('');
var defaultFieldTemplate = [
'<label>',
'<input>',
'<div data-hook="message-container" class="message message-below message-error">',
'<p data-hook="message-text"></p>',
'</div>',
'<a data-hook="remove-field">remove</a>',
'</label>'
].join('');
module.exports = View.extend({
initialize: function () {
if (!this.label) this.label = this.name;
this.fields = [];
// calculate default value if not provided
var defaultVal = [];
// make sure there's at least one
var num = this.minLength || 1;
while (num--) {
defaultVal.push('');
}
if (!this.value.length) this.value = defaultVal;
this.on('change:valid change:value', this.updateParent, this);
this.render();
},
render: function () {
if (this.rendered) return;
this.renderWithTemplate();
this.setValue(this.value);
this.rendered = true;
},
events: {
'click [data-hook~=add-field]': 'handleAddFieldClick'
},
bindings: {
'name': {
type: 'attribute',
selector: 'input',
name: 'name'
},
'label': {
hook: 'label'
},
'message': {
type: 'text',
hook: 'main-message-text'
},
'showMessage': {
type: 'toggle',
hook: 'main-message-container'
},
'canAdd': {
type: 'toggle',
hook: 'add-field'
}
},
props: {
name: ['string', true, ''],
value: ['array', true, function () { return []; }],
label: ['string', true, ''],
message: ['string', true, ''],
placeholder: ['string', true, ''],
requiredMessage: 'string',
validClass: ['string', true, 'input-valid'],
invalidClass: ['string', true, 'input-invalid'],
minLength: ['number', true, 0],
maxLength: ['number', true, 10],
tests: ['array', true, function () { return []; }],
template: ['string', true, defaultTemplate],
fieldTemplate: ['string', true, defaultFieldTemplate],
type: ['text', true, 'text']
},
session: {
shouldValidate: ['boolean', true, false],
fieldsValid: ['boolean', true, false],
fieldsRendered: ['number', true, 0],
rendered: ['boolean', true, false]
},
derived: {
fieldClass: {
deps: ['showMessage'],
fn: function () {
return this.showMessage ? this.invalidClass : this.validClass;
}
},
valid: {
deps: ['requiredMet', 'fieldsValid'],
fn: function () {
return this.requiredMet && this.fieldsValid;
}
},
showMessage: {
deps: ['valid', 'shouldValidate', 'message'],
fn: function () {
return !!(this.shouldValidate && this.message && !this.valid);
}
},
canAdd: {
deps: ['maxLength', 'fieldsRendered'],
fn: function () {
return this.fieldsRendered < this.maxLength;
}
},
requiredMet: {
deps: ['value', 'minLength'],
fn: function () {
return this.value.length > this.minLength;
}
}
},
setValue: function (arr) {
if (arr.length > this.maxLength) throw Error('Field value length greater than maxLength setting');
this.clearFields();
arr.forEach(this.addField, this);
this.update();
},
beforeSubmit: function () {
this.fields.forEach(function (field) {
field.beforeSubmit();
});
this.shouldValidate = true;
if (!this.valid && !this.requiredMet) {
this.message = this.requiredMessage || this.getRequiredMessage();
}
},
handleAddFieldClick: function (e) {
e.preventDefault();
var field = this.addField('');
field.input.focus();
},
addField: function (value) {
var self = this;
var firstField = this.fields.length === 0;
var removable = function () {
if (firstField) return false;
if (self.fields.length >= (self.minLength || 1)) {
return true;
}
return false;
}();
var initOptions = {
value: value,
parent: this,
required: false,
tests: this.tests,
placeholder: this.placeholder,
removable: removable,
type: this.type
};
var field = new FieldView(initOptions);
field.template = this.fieldTemplate;
field.render();
this.fieldsRendered += 1;
this.fields.push(field);
this.queryByHook('field-container').appendChild(field.el);
return field;
},
clearFields: function () {
this.fields.forEach(function (field) {
field.remove();
});
this.fields = [];
this.fieldsRendered = 0;
},
removeField: function (field) {
this.fields = _.without(this.fields, field);
field.remove();
this.fieldsRendered -= 1;
this.update();
},
update: function () {
var valid = true;
var value = this.fields.reduce(function (previous, field) {
if (field.value) previous.push(field.value);
if (!field.valid && valid) valid = false;
return previous;
}, []);
this.set({
value: value,
fieldsValid: valid
});
},
updateParent: function () {
if (this.parent) this.parent.update(this);
},
getRequiredMessage: function () {
var plural = this.minLength > 1;
return 'Enter at least ' + (this.minLength || 1) + ' item' + (plural ? 's.' : '.');
}
});