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: () => {},
callbackOnAddItem: (id, value, passedInput) => {},
callbackOnRemoveItem: (id, value, passedInput) => {},
callbackOnChange: (value, passedInput) => {},
callbackOnRender: () => {},
});
</script>
@ -292,14 +293,21 @@ classNames: {
<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
<strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}`
<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

File diff suppressed because one or more lines are too long

View file

@ -70,7 +70,7 @@ export class Choices {
callbackOnInit: () => {},
callbackOnAddItem: (id, value, passedInput) => {},
callbackOnRemoveItem: (id, value, passedInput) => {},
callbackOnRender: (state) => {},
callbackOnChange: (value, passedInput) => {},
};
// Merge options with user options
@ -177,6 +177,8 @@ export class Choices {
* @public
*/
destroy() {
this._removeEventListeners();
this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
this.passedElement.tabIndex = '';
this.passedElement.removeAttribute('style', 'display:none;');
@ -188,8 +190,6 @@ export class Choices {
this.userConfig = null;
this.config = null;
this.store = null;
this._removeEventListeners();
}
/**
@ -368,28 +368,59 @@ export class Choices {
* @public
*/
setValue(args) {
// Convert args to an itterable array
const values = [...args];
if(this.initialised === true) {
// Convert args to an itterable array
const values = [...args];
values.forEach((item, index) => {
if(isType('Object', item)) {
if(!item.value) return;
// 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.
if(this.passedElement.type !== 'text') {
this._addChoice(true, false, item.value, item.label, -1);
} else {
this._addItem(item.value, item.label, item.id);
values.forEach((item, index) => {
if(isType('Object', item)) {
if(!item.value) return;
// 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.
if(this.passedElement.type !== 'text') {
this._addChoice(true, false, item.value, item.label, -1);
} else {
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 {
this._addItem(item);
});
}
return this;
}
/**
* 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;
}
@ -465,6 +496,25 @@ export class Choices {
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
* @param {Array} activeItems Items that are currently active
@ -502,6 +552,7 @@ export class Choices {
if(canAddItem) {
this.toggleDropdown();
this._addItem(value);
this._triggerChange(value);
this.clearInput(this.passedElement);
}
}
@ -523,6 +574,7 @@ export class Choices {
if(this.config.editItems && !hasHighlightedItems && lastItem) {
this.input.value = lastItem.value;
this._removeItem(lastItem);
this._triggerChange(lastItem.value);
} else {
if(!hasHighlightedItems) { this.highlightItem(lastItem); }
this.removeHighlightedItems();
@ -589,6 +641,7 @@ export class Choices {
const label = highlighted.innerHTML;
const id = highlighted.getAttribute('data-id');
this._addItem(value, label, id);
this._triggerChange(value);
this.clearInput(this.passedElement);
if(this.passedElement.type === 'select-one') {
@ -778,6 +831,7 @@ export class Choices {
const itemId = e.target.parentNode.getAttribute('data-id');
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
this._removeItem(itemToRemove);
this._triggerChange(itemToRemove.value);
}
} else if(e.target.hasAttribute('data-item')) {
// If we are clicking on an item
@ -803,6 +857,7 @@ export class Choices {
if(!choice.selected && !choice.disabled) {
this._addItem(choice.value, choice.label, choice.id);
this._triggerChange(choice.value);
if(this.passedElement.type === 'select-one') {
this.input.value = "";
this.isSearching = false;
@ -872,7 +927,13 @@ export class Choices {
if(e.target === this.input && !hasActiveDropdown) {
this.containerOuter.classList.add(this.config.classNames.focusState);
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) {
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);
} else {
}
// Close the dropdown if there is one
if(hasActiveDropdown) {
this.hideDropdown();
}
}
@ -1139,7 +1206,15 @@ export class Choices {
const classNames = this.config.classNames;
const templates = {
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: () => {
return strToEl(`<div class="${ classNames.containerInner }"></div>`);
@ -1430,8 +1505,8 @@ export class Choices {
} else if(activeChoices.length >= 1) {
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
// append it and highlight the first choice
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;
}
}
@ -1485,6 +1552,10 @@ export class Choices {
document.addEventListener('mousedown', this._onMouseDown);
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('paste', this._onPaste);
this.input.addEventListener('focus', this._onFocus);
@ -1501,6 +1572,10 @@ export class Choices {
document.removeEventListener('keydown', this._onKeyDown);
document.removeEventListener('mousedown', this._onMouseDown);
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('paste', this._onPaste);

View file

@ -189,7 +189,7 @@
var choices10 = new Choices('#choices-10', {
placeholder: true,
placeholderValue: 'Pick an Strokes record',
callbackOnRender: function(state) { console.log(state) }
callbackOnChange: function(value, passedInput) { console.log(value) }
}).ajax(function(callback) {
fetch('https://api.discogs.com/artists/55980/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW')
.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>
</body>

View file

@ -45,6 +45,7 @@ describe('Choices', function() {
it('should have config options', function() {
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.addItems).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.callbackOnAddItem).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() {
@ -156,7 +157,6 @@ describe('Choices', function() {
beforeEach(function() {
this.input = document.createElement('select');
this.input.className = 'js-choices';
this.input.multiple = false;
this.input.placeholder = 'Placeholder text';
for (let i = 1; i < 4; i++) {
@ -219,6 +219,28 @@ describe('Choices', function() {
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() {
this.choices.input.focus();
this.choices.input.value = 'Value 3';
@ -240,7 +262,7 @@ describe('Choices', function() {
beforeEach(function() {
this.input = document.createElement('select');
this.input.className = 'js-choices';
this.input.multiple = true;
this.input.setAttribute('multiple', '');
for (let i = 1; i < 4; i++) {
const option = document.createElement('option');
@ -264,7 +286,7 @@ describe('Choices', function() {
{value: 'Two', label: 'Label Two', disabled: true},
{value: 'Three', label: 'Label Three'},
],
});;
});
});
it('should add any pre-defined values', function() {