Set choices directly via public function + callback on change

This commit is contained in:
Josh Johnson 2016-07-31 20:02:46 +01:00
parent e667b61bf2
commit ca39e30684
5 changed files with 155 additions and 44 deletions

View file

@ -70,6 +70,7 @@ A lightweight, configurable select box/text input plugin. Similar to Select2 and
callbackOnInit: () => {}, callbackOnInit: () => {},
callbackOnAddItem: (id, value, passedInput) => {}, callbackOnAddItem: (id, value, passedInput) => {},
callbackOnRemoveItem: (id, value, passedInput) => {}, callbackOnRemoveItem: (id, value, passedInput) => {},
callbackOnChange: (value, passedInput) => {},
callbackOnRender: () => {}, callbackOnRender: () => {},
}); });
</script> </script>
@ -292,14 +293,21 @@ classNames: {
<strong>Input types affected:</strong> `text`, `select-one`, `select-multiple` <strong>Input types affected:</strong> `text`, `select-one`, `select-multiple`
<strong>Usage:</strong> Function to run each time an item is added. <strong>Usage:</strong> Function to run each time an item is added (programmatically or by the user).
### callbackOnRemoveItem ### callbackOnRemoveItem
<strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}` <strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}`
<strong>Input types affected:</strong> `text`, `select-one`, `select-multiple` <strong>Input types affected:</strong> `text`, `select-one`, `select-multiple`
<strong>Usage:</strong> Function to run each time an item is removed. <strong>Usage:</strong> Function to run each time an item is removed (programmatically or by the user).
### callbackOnChange
<strong>Type:</strong> `Function` <strong>Default:</strong>`(value, passedInput) => {}`
<strong>Input types affected:</strong> `text`, `select-one`, `select-multiple`
<strong>Usage:</strong> Function to run each time an item is added/removed by a user.
## Methods ## Methods

File diff suppressed because one or more lines are too long

View file

@ -70,7 +70,7 @@ export class Choices {
callbackOnInit: () => {}, callbackOnInit: () => {},
callbackOnAddItem: (id, value, passedInput) => {}, callbackOnAddItem: (id, value, passedInput) => {},
callbackOnRemoveItem: (id, value, passedInput) => {}, callbackOnRemoveItem: (id, value, passedInput) => {},
callbackOnRender: (state) => {}, callbackOnChange: (value, passedInput) => {},
}; };
// Merge options with user options // Merge options with user options
@ -177,6 +177,8 @@ export class Choices {
* @public * @public
*/ */
destroy() { destroy() {
this._removeEventListeners();
this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState); this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
this.passedElement.tabIndex = ''; this.passedElement.tabIndex = '';
this.passedElement.removeAttribute('style', 'display:none;'); this.passedElement.removeAttribute('style', 'display:none;');
@ -188,8 +190,6 @@ export class Choices {
this.userConfig = null; this.userConfig = null;
this.config = null; this.config = null;
this.store = null; this.store = null;
this._removeEventListeners();
} }
/** /**
@ -368,28 +368,59 @@ export class Choices {
* @public * @public
*/ */
setValue(args) { setValue(args) {
// Convert args to an itterable array if(this.initialised === true) {
const values = [...args]; // Convert args to an itterable array
const values = [...args];
values.forEach((item, index) => { values.forEach((item, index) => {
if(isType('Object', item)) { if(isType('Object', item)) {
if(!item.value) return; if(!item.value) return;
// If we are dealing with a select input, we need to create an option first // If we are dealing with a select input, we need to create an option first
// that is then selected. For text inputs we can just add items normally. // that is then selected. For text inputs we can just add items normally.
if(this.passedElement.type !== 'text') { if(this.passedElement.type !== 'text') {
this._addChoice(true, false, item.value, item.label, -1); this._addChoice(true, false, item.value, item.label, -1);
} else { } else {
this._addItem(item.value, item.label, item.id); this._addItem(item.value, item.label, item.id);
}
} else if(isType('String', item)) {
if(this.passedElement.type !== 'text') {
this._addChoice(true, false, item, item, -1);
} else {
this._addItem(item);
}
} }
} else if(isType('String', item)) { });
if(this.passedElement.type !== 'text') { }
this._addChoice(true, false, item, item, -1);
} else { return this;
this._addItem(item); }
/**
* Direct populate choices
* @param {Array} choices - Choices to insert
* @param {string} value - Name of 'value' property
* @param {string} label - Name of 'label' property
* @return {Object} Class instance
* @public
*/
setChoices(choices, value, label){
if(this.initialised === true) {
if(this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
if(!isType('Array', choices) || !value) return;
if(choices && choices.length) {
this.containerOuter.classList.remove(this.config.classNames.loadingState);
choices.forEach((result, index) => {
// Select first choice in list if single select input
if(index === 0 && this.passedElement.type === 'select-one') {
this._addChoice(true, false, result[value], result[label]);
} else {
this._addChoice(false, false, result[value], result[label]);
}
});
} }
} }
}); }
return this; return this;
} }
@ -465,6 +496,25 @@ export class Choices {
return this; return this;
} }
/**
* Call change callback
* @param {string} value - last added/deleted/selected value
* @return
* @private
*/
_triggerChange(value) {
if(!value) return;
// Run callback if it is a function
if(this.config.callbackOnChange){
const callback = this.config.callbackOnChange;
if(isType('Function', callback)) {
callback(value, this.passedElement);
} else {
console.error('callbackOnChange: Callback is not a function');
}
}
}
/** /**
* Process enter key event * Process enter key event
* @param {Array} activeItems Items that are currently active * @param {Array} activeItems Items that are currently active
@ -502,6 +552,7 @@ export class Choices {
if(canAddItem) { if(canAddItem) {
this.toggleDropdown(); this.toggleDropdown();
this._addItem(value); this._addItem(value);
this._triggerChange(value);
this.clearInput(this.passedElement); this.clearInput(this.passedElement);
} }
} }
@ -523,6 +574,7 @@ export class Choices {
if(this.config.editItems && !hasHighlightedItems && lastItem) { if(this.config.editItems && !hasHighlightedItems && lastItem) {
this.input.value = lastItem.value; this.input.value = lastItem.value;
this._removeItem(lastItem); this._removeItem(lastItem);
this._triggerChange(lastItem.value);
} else { } else {
if(!hasHighlightedItems) { this.highlightItem(lastItem); } if(!hasHighlightedItems) { this.highlightItem(lastItem); }
this.removeHighlightedItems(); this.removeHighlightedItems();
@ -589,6 +641,7 @@ export class Choices {
const label = highlighted.innerHTML; const label = highlighted.innerHTML;
const id = highlighted.getAttribute('data-id'); const id = highlighted.getAttribute('data-id');
this._addItem(value, label, id); this._addItem(value, label, id);
this._triggerChange(value);
this.clearInput(this.passedElement); this.clearInput(this.passedElement);
if(this.passedElement.type === 'select-one') { if(this.passedElement.type === 'select-one') {
@ -778,6 +831,7 @@ export class Choices {
const itemId = e.target.parentNode.getAttribute('data-id'); const itemId = e.target.parentNode.getAttribute('data-id');
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId)); const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
this._removeItem(itemToRemove); this._removeItem(itemToRemove);
this._triggerChange(itemToRemove.value);
} }
} else if(e.target.hasAttribute('data-item')) { } else if(e.target.hasAttribute('data-item')) {
// If we are clicking on an item // If we are clicking on an item
@ -803,6 +857,7 @@ export class Choices {
if(!choice.selected && !choice.disabled) { if(!choice.selected && !choice.disabled) {
this._addItem(choice.value, choice.label, choice.id); this._addItem(choice.value, choice.label, choice.id);
this._triggerChange(choice.value);
if(this.passedElement.type === 'select-one') { if(this.passedElement.type === 'select-one') {
this.input.value = ""; this.input.value = "";
this.isSearching = false; this.isSearching = false;
@ -872,7 +927,13 @@ export class Choices {
if(e.target === this.input && !hasActiveDropdown) { if(e.target === this.input && !hasActiveDropdown) {
this.containerOuter.classList.add(this.config.classNames.focusState); this.containerOuter.classList.add(this.config.classNames.focusState);
if(this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple'){ if(this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple'){
this.showDropdown(); this.showDropdown();
}
} else if(this.passedElement.type === 'select-one' && e.target === this.containerOuter && !hasActiveDropdown) {
this.containerOuter.classList.add(this.config.classNames.focusState);
this.showDropdown();
if(this.config.search) {
this.input.focus();
} }
} }
} }
@ -885,9 +946,15 @@ export class Choices {
*/ */
_onBlur(e) { _onBlur(e) {
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
if(e.target === this.input && !hasActiveDropdown) {
// If the blurred element is this input
if(e.target === this.input) {
// Remove the focus state
this.containerOuter.classList.remove(this.config.classNames.focusState); this.containerOuter.classList.remove(this.config.classNames.focusState);
} else { }
// Close the dropdown if there is one
if(hasActiveDropdown) {
this.hideDropdown(); this.hideDropdown();
} }
} }
@ -1139,7 +1206,15 @@ export class Choices {
const classNames = this.config.classNames; const classNames = this.config.classNames;
const templates = { const templates = {
containerOuter: () => { containerOuter: () => {
return strToEl(`<div class="${ classNames.containerOuter }" data-type="${ this.passedElement.type }"></div>`); if(this.passedElement.type === 'select-one') {
return strToEl(`
<div class="${ classNames.containerOuter }" data-type="${ this.passedElement.type }" tabindex="0"></div>
`);
} else {
return strToEl(`
<div class="${ classNames.containerOuter }" data-type="${ this.passedElement.type }"></div>
`);
}
}, },
containerInner: () => { containerInner: () => {
return strToEl(`<div class="${ classNames.containerInner }"></div>`); return strToEl(`<div class="${ classNames.containerInner }"></div>`);
@ -1430,8 +1505,8 @@ export class Choices {
} else if(activeChoices.length >= 1) { } else if(activeChoices.length >= 1) {
choiceListFragment = this.renderChoices(activeChoices, choiceListFragment); choiceListFragment = this.renderChoices(activeChoices, choiceListFragment);
} }
if(choiceListFragment.childNodes) { if(choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {
// If we actually have anything to add to our dropdown // If we actually have anything to add to our dropdown
// append it and highlight the first choice // append it and highlight the first choice
this.choiceList.appendChild(choiceListFragment); this.choiceList.appendChild(choiceListFragment);
@ -1462,14 +1537,6 @@ export class Choices {
} }
} }
if(this.config.callbackOnRender){
if(isType('Function', this.config.callbackOnRender)) {
this.config.callbackOnRender(this.currentState);
} else {
console.error('callbackOnRender: Callback is not a function');
}
}
this.prevState = this.currentState; this.prevState = this.currentState;
} }
} }
@ -1485,6 +1552,10 @@ export class Choices {
document.addEventListener('mousedown', this._onMouseDown); document.addEventListener('mousedown', this._onMouseDown);
document.addEventListener('mouseover', this._onMouseOver); document.addEventListener('mouseover', this._onMouseOver);
if(this.passedElement.type && this.passedElement.type === 'select-one') {
this.containerOuter.addEventListener('focus', this._onFocus);
}
this.input.addEventListener('input', this._onInput); this.input.addEventListener('input', this._onInput);
this.input.addEventListener('paste', this._onPaste); this.input.addEventListener('paste', this._onPaste);
this.input.addEventListener('focus', this._onFocus); this.input.addEventListener('focus', this._onFocus);
@ -1501,6 +1572,10 @@ export class Choices {
document.removeEventListener('keydown', this._onKeyDown); document.removeEventListener('keydown', this._onKeyDown);
document.removeEventListener('mousedown', this._onMouseDown); document.removeEventListener('mousedown', this._onMouseDown);
document.removeEventListener('mouseover', this._onMouseOver); document.removeEventListener('mouseover', this._onMouseOver);
if(this.passedElement.type && this.passedElement.type === 'select-one') {
this.containerOuter.removeEventListener('focus', this._onFocus);
}
this.input.removeEventListener('input', this._onInput); this.input.removeEventListener('input', this._onInput);
this.input.removeEventListener('paste', this._onPaste); this.input.removeEventListener('paste', this._onPaste);

View file

@ -189,7 +189,7 @@
var choices10 = new Choices('#choices-10', { var choices10 = new Choices('#choices-10', {
placeholder: true, placeholder: true,
placeholderValue: 'Pick an Strokes record', placeholderValue: 'Pick an Strokes record',
callbackOnRender: function(state) { console.log(state) } callbackOnChange: function(value, passedInput) { console.log(value) }
}).ajax(function(callback) { }).ajax(function(callback) {
fetch('https://api.discogs.com/artists/55980/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW') fetch('https://api.discogs.com/artists/55980/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW')
.then(function(response) { .then(function(response) {
@ -242,6 +242,12 @@
], ],
}); });
choices15.setChoices([
{value: 'Four', label: 'Label Four'},
{value: 'Five', label: 'Label Five'},
{value: 'Six', label: 'Label Six'},
], 'value', 'label');
}); });
</script> </script>
</body> </body>

View file

@ -45,6 +45,7 @@ describe('Choices', function() {
it('should have config options', function() { it('should have config options', function() {
expect(this.choices.config.items).toEqual(jasmine.any(Array)); expect(this.choices.config.items).toEqual(jasmine.any(Array));
expect(this.choices.config.choices).toEqual(jasmine.any(Array));
expect(this.choices.config.maxItemCount).toEqual(jasmine.any(Number)); expect(this.choices.config.maxItemCount).toEqual(jasmine.any(Number));
expect(this.choices.config.addItems).toEqual(jasmine.any(Boolean)); expect(this.choices.config.addItems).toEqual(jasmine.any(Boolean));
expect(this.choices.config.removeItems).toEqual(jasmine.any(Boolean)); expect(this.choices.config.removeItems).toEqual(jasmine.any(Boolean));
@ -64,7 +65,7 @@ describe('Choices', function() {
expect(this.choices.config.callbackOnInit).toEqual(jasmine.any(Function)); expect(this.choices.config.callbackOnInit).toEqual(jasmine.any(Function));
expect(this.choices.config.callbackOnAddItem).toEqual(jasmine.any(Function)); expect(this.choices.config.callbackOnAddItem).toEqual(jasmine.any(Function));
expect(this.choices.config.callbackOnRemoveItem).toEqual(jasmine.any(Function)); expect(this.choices.config.callbackOnRemoveItem).toEqual(jasmine.any(Function));
expect(this.choices.config.callbackOnRender).toEqual(jasmine.any(Function)); expect(this.choices.config.callbackOnChange).toEqual(jasmine.any(Function));
}); });
it('should expose public methods', function() { it('should expose public methods', function() {
@ -156,7 +157,6 @@ describe('Choices', function() {
beforeEach(function() { beforeEach(function() {
this.input = document.createElement('select'); this.input = document.createElement('select');
this.input.className = 'js-choices'; this.input.className = 'js-choices';
this.input.multiple = false;
this.input.placeholder = 'Placeholder text'; this.input.placeholder = 'Placeholder text';
for (let i = 1; i < 4; i++) { for (let i = 1; i < 4; i++) {
@ -219,6 +219,28 @@ describe('Choices', function() {
expect(this.choices.currentState.items.length).toBe(2); expect(this.choices.currentState.items.length).toBe(2);
}); });
it('should trigger a change callback on selection', function() {
spyOn(this.choices.config, 'callbackOnChange');
this.choices.input.focus();
// Key down to second choice
this.choices._onKeyDown({
target: this.choices.input,
keyCode: 40,
ctrlKey: false,
preventDefault: () => {}
});
// Key down to select choice
this.choices._onKeyDown({
target: this.choices.input,
keyCode: 13,
ctrlKey: false
});
expect(this.choices.config.callbackOnChange).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(HTMLElement));
});
it('should filter choices when searching', function() { it('should filter choices when searching', function() {
this.choices.input.focus(); this.choices.input.focus();
this.choices.input.value = 'Value 3'; this.choices.input.value = 'Value 3';
@ -240,7 +262,7 @@ describe('Choices', function() {
beforeEach(function() { beforeEach(function() {
this.input = document.createElement('select'); this.input = document.createElement('select');
this.input.className = 'js-choices'; this.input.className = 'js-choices';
this.input.multiple = true; this.input.setAttribute('multiple', '');
for (let i = 1; i < 4; i++) { for (let i = 1; i < 4; i++) {
const option = document.createElement('option'); const option = document.createElement('option');
@ -264,7 +286,7 @@ describe('Choices', function() {
{value: 'Two', label: 'Label Two', disabled: true}, {value: 'Two', label: 'Label Two', disabled: true},
{value: 'Three', label: 'Label Three'}, {value: 'Three', label: 'Label Three'},
], ],
});; });
}); });
it('should add any pre-defined values', function() { it('should add any pre-defined values', function() {