Skip to content

Instantly share code, notes, and snippets.

@hailwood
Last active August 29, 2015 14:22
Show Gist options
  • Save hailwood/e8cccc05ae417062bcea to your computer and use it in GitHub Desktop.
Save hailwood/e8cccc05ae417062bcea to your computer and use it in GitHub Desktop.
/* $AMPERSAND_VERSION */
var domify = require('domify');
var dom = require('ampersand-dom');
var matches = require('matches-selector');
var View = require('ampersand-view');
//Replaceable with anything with label, message-container, message-text data-hooks and a <select>
var defaultTemplate = [
'<label class="select">',
'<span data-hook="label"></span>',
'<select></select>',
'<span data-hook="message-container" class="message message-below message-error">',
'<p data-hook="message-text"></p>',
'</span>',
'</label>'
].join('\n');
var createOption = function (value, text, disabled) {
var node = document.createElement('option');
//Set to empty-string if undefined or null, but not if 0, false, etc
if (value === null || value === undefined) value = '';
if (disabled) node.disabled = true;
node.textContent = text;
node.value = value;
return node;
};
module.exports = View.extend({
initialize: function (opts) {
opts = opts || {};
if (typeof opts.name !== 'string') throw new Error('SelectView requires a name property.');
this.name = opts.name;
if (!Array.isArray(opts.options) && !opts.options.isCollection) {
throw new Error('SelectView requires select options.');
}
this.options = opts.options;
if (this.options.isCollection) {
this.idAttribute = opts.idAttribute || this.options.mainIndex || 'id';
this.textAttribute = opts.textAttribute || 'text';
this.disabledAttribute = opts.disabledAttribute;
}
this.el = opts.el;
if (opts.label === undefined) {
this.label = this.name;
} else {
this.label = opts.label;
}
this.parent = opts.parent || this.parent;
this.template = opts.template || defaultTemplate;
this.unselectedText = opts.unselectedText;
this.startingValue = opts.value;
this.yieldModel = (opts.yieldModel !== false);
this.multiple = opts.multiple || false;
this.eagerValidate = opts.eagerValidate;
this.required = opts.required || false;
this.validClass = opts.validClass || 'input-valid';
this.invalidClass = opts.invalidClass || 'input-invalid';
if (opts.requiredMessage === undefined) {
this.requiredMessage = 'Selection required';
} else {
this.requiredMessage = opts.requiredMessage;
}
this.onChange = this.onChange.bind(this);
this.startingValue = this.setValue(opts.value, this.eagerValidate ? false : true, true);
if (opts.beforeSubmit) this.beforeSubmit = opts.beforeSubmit;
if (opts.autoRender) this.autoRender = opts.autoRender;
},
render: function () {
var elDom,
labelEl;
if (this.rendered) return;
elDom = domify(this.template);
if (!this.el) this.el = elDom;
else this.el.appendChild(elDom);
labelEl = this.el.querySelector('[data-hook~=label]');
if (labelEl) {
labelEl.textContent = this.label;
this.label = labelEl;
} else {
delete this.label;
}
if (this.el.tagName === 'SELECT') {
this.select = this.el;
} else {
this.select = this.el.querySelector('select');
}
if (!this.select) throw new Error('no select found in template');
if (matches(this.el, 'select')) this.select = this.el;
if (this.select) this.select.setAttribute('name', this.name);
if (this.multiple) this.select.setAttribute('multiple', true);
this.bindDOMEvents();
this.renderOptions();
this.updateSelectedOption();
if (this.options.isCollection) {
this.options.on('add remove reset', function () {
var setValueFirstModel = function () {
if (!this.options.length || this.multiple) return;
if (this.yieldModel) this.setValue(this.options.models[0]);
else this.setValue(this.options.models[0][this.idAttribute]);
}.bind(this);
this.renderOptions();
if (this.hasOptionByValue(this.value)) this.updateSelectedOption();
else setValueFirstModel();
}.bind(this));
}
this.validate(this.eagerValidate ? false : true, true);
return this;
},
onChange: function () {
var value;
if (!this.multiple) {
value = this.select.options[this.select.selectedIndex].value;
} else {
value = Array.prototype.filter.apply(this.select.options, [function (o) {
return o.selected;
}]);
value = value.map(function (o) {
return o.value;
});
}
if (this.options.isCollection && this.yieldModel) {
if (!this.multiple) {
value = this.getModelForId(value);
} else {
value = this.getModelsForIds(value);
}
}
this.setValue(value);
},
/**
* Finds a model in the options collection provided an ID
* @param {any} id
* @return {State}
* @throws {RangeError} If model not found
*/
getModelForId: function (id) {
return this.options.filter(function (model) {
// intentionally coerce for '1' == 1
return model[this.idAttribute] == id;
}.bind(this))[0];
},
/**
* Finds all models in the options collection provided their ID
* @param {array} ids
* @return {Collection}
*/
getModelsForIds: function (ids) {
return this.options.filter(function (model) {
// for loop to all intentionally coercing for '1' == 1
for (var i = 0; i < ids.length; i++) {
if (ids[i].isState && model[this.idAttribute] == ids[i][this.idAttribute]) {
return true;
} else if(model[this.idAttribute] == ids[i]) {
return true;
}
}
return false;
}.bind(this));
},
bindDOMEvents: function () {
this.el.addEventListener('change', this.onChange, false);
},
renderOptions: function () {
if (!this.select) return;
this.select.innerHTML = '';
if (this.unselectedText && !this.multiple) {
this.select.appendChild(
createOption(null, this.unselectedText)
);
}
this.options.forEach(function (option) {
this.select.appendChild(
createOption(
this.getOptionValue(option),
this.getOptionText(option),
this.getOptionDisabled(option)
)
);
}.bind(this));
},
/**
* Updates the <select> control to set the select option when the option
* has changed programatically (i.e. not through direct user selection)
* @return {SelectView} this
* @throws {Error} If no option exists for this.value
*/
updateSelectedOption: function () {
var lookupValue = this.value;
if (lookupValue === null || lookupValue === undefined || lookupValue === '') {
if (this.unselectedText || (!this.startingValue && !this.rendered)) {
this.select.selectedIndex = 0;
return this;
} else if (!this.options.length && this.value === null) {
return this;
}
}
// Pull out the id if it's a model
if (this.options.isCollection && this.yieldModel && !this.multiple) {
lookupValue = lookupValue && lookupValue[this.idAttribute];
} else if (this.options.isCollection && this.yieldModel && this.multiple) {
var self = this;
lookupValue = [];
if (this.value) {
this.value.forEach(function (option) {
lookupValue.push(option[self.idAttribute]+'');
});
}
}
if (Array.isArray(lookupValue)) {
for (var i = this.select.options.length; i--; i) {
if (lookupValue.indexOf(this.select.options[i].value+'') !== -1) {
this.select.options[i].selected = true;
}
}
return this;
}
if (lookupValue || lookupValue === 0 || lookupValue === null) {
if (lookupValue === null) lookupValue = ''; // DOM sees only '' empty value
for (var i = this.select.options.length; i--; i) {
if (this.select.options[i].value == lookupValue) {
this.select.selectedIndex = i;
return this;
}
}
}
// failed to match any
throw new Error('no option exists for value: ' + lookupValue);
},
remove: function () {
if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
this.el.removeEventListener('change', this.onChange, false);
},
/**
* Sets control to unselectedText option, or user specified option with `null`
* value
* @return {SelectView} this
*/
clear: function () {
this.setValue(null, true);
return this;
},
/**
* Sets the selected option and view value to the original option value provided
* during construction
* @return {SelectView} this
*/
reset: function () {
return this.setValue(this.startingValue, true);
},
setValue: function (value, skipValidationMessage, init) {
var option, options, model, nullValid;
if (value === null || value === undefined || value === '') {
this.value = null;
// test if null is a valid option
if (this.unselectedText || this.multiple) {
nullValid = true;
} else {
nullValid = this.hasOptionByValue(null);
}
// empty value requested to be set. This may be because the field is just
// initializing. If initializing and `null` isn't in the honored option set
// set the select to the 0-th index
if (init && this.options.length && !nullValid) {
// no initial value passed, set initial value to first item in set
if (this.options.isCollection) {
model = this.options.models[0];
this.value = this.yieldModel ? model : model[this.idAttribute];
} else {
if (Array.isArray(this.options[0])) {
this.value = this.options[0][0];
} else {
this.value = this.options[0];
}
}
}
} else {
// Ensure corresponding option exists before assigning value
if (!this.multiple) {
option = this.getOptionByValue(value);
this.value = Array.isArray(option) ? option[0] : option;
} else {
options = this.getOptionByValue(value);
this.value = options;
}
}
this.validate(skipValidationMessage);
if (this.select) this.updateSelectedOption();
if (this.parent) this.parent.update(this);
return this.value;
},
validate: function (skipValidationMessage) {
if (!this.required) {
// selected option always known to be in option set,
// thus field is always valid if not required
this.valid = true;
if (this.select) this.toggleMessage(skipValidationMessage);
return this.valid;
}
if (this.required && !this.value && this.value !== 0) {
this.valid = false;
if (this.select) this.toggleMessage(skipValidationMessage, this.requiredMessage);
} else {
this.valid = true;
if (this.select) this.toggleMessage(skipValidationMessage);
}
return this.valid;
},
/**
* Called by FormView on submit
* @return {SelectView} this
*/
beforeSubmit: function () {
if (this.select) {
this.onChange();
}
},
/**
* Gets the option corresponding to provided value.
* @return {*} string, array, state, or model
* @param value
*/
getOptionByValue: function (value) {
var model;
if (this.options.isCollection && !this.multiple) {
// find value in collection, error if no model found
if (this.options.indexOf(value) === -1) model = this.getModelForId(value);
else model = value;
if (!model) throw new Error('model or model idAttribute not found in options collection');
return this.yieldModel ? model : model[this.idAttribute];
} else if (this.options.isCollection && this.multiple) {
model = this.getModelsForIds(value);
// find value in collection, error if no model found
return this.yieldModel ? model : model; //todo: non yieldModel multiple
} else if (Array.isArray(this.options) && !this.multiple) {
// find value value in options array
// find option, formatted [['val', 'text'], ...]
if (this.options.length && Array.isArray(this.options[0])) {
for (var i = this.options.length - 1; i >= 0; i--) {
if (this.options[i][0] == value) return this.options[i];
}
}
// find option, formatted ['valAndText', ...] format
if (this.options.length && this.options.indexOf(value) !== -1) return value;
throw new Error('value not in set of provided options');
} else if (Array.isArray(this.options) && this.multiple) {
var options = [];
// find value value in options array
// find option, formatted [['val', 'text'], ...]
if (this.options.length && Array.isArray(this.options[0])) {
for (var i = this.options.length - 1; i >= 0; i--) {
if (this.options[i][0] == value) options.push(this.options[i]);
}
return options;
}
// find option, formatted ['valAndText', ...] format
for (var i = this.options.length - 1; i >= 0; i--) {
if (this.options[i] == value) options.push(this.options[i]);
}
if (options.length) return options;
throw new Error('value not in set of provided options');
}
throw new Error('select option set invalid');
},
/**
* Tests if option set has an option corresponding to the provided value
* @param {*} value
* @return {Boolean}
*/
hasOptionByValue: function (value) {
try {
this.getOptionByValue(value);
return true;
} catch (err) {
return false;
}
},
getOptionValue: function (option) {
if (Array.isArray(option)) return option[0];
if (this.options.isCollection) return option[this.idAttribute];
return option;
},
getOptionText: function (option) {
if (Array.isArray(option)) return option[1];
if (this.options.isCollection) {
if (this.textAttribute && option[this.textAttribute] !== undefined) {
return option[this.textAttribute];
}
}
return option;
},
getOptionDisabled: function (option) {
if (Array.isArray(option)) return option[2];
if (this.options.isCollection && this.disabledAttribute) return option[this.disabledAttribute];
return false;
},
toggleMessage: function (hide, message) {
var mContainer = this.el.querySelector('[data-hook~=message-container]'),
mText = this.el.querySelector('[data-hook~=message-text]');
if (!mContainer || !mText) return;
if (hide) {
dom.hide(mContainer);
mText.textContent = '';
dom.removeClass(this.el, this.validClass);
dom.removeClass(this.el, this.invalidClass);
return;
}
if (message) {
dom.show(mContainer);
mText.textContent = message;
dom.addClass(this.el, this.invalidClass);
dom.removeClass(this.el, this.validClass);
} else {
dom.hide(mContainer);
mText.textContent = '';
dom.addClass(this.el, this.validClass);
dom.removeClass(this.el, this.invalidClass);
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment