Assign element value based on store

This commit is contained in:
Josh Johnson 2016-04-04 21:44:32 +01:00
parent d8620f433f
commit 0676828ac3
6 changed files with 188 additions and 152 deletions

File diff suppressed because one or more lines are too long

View file

@ -6,12 +6,6 @@ export const addItemToStore = (value, id) => {
} }
}; };
export const unselectAllFromStore = () => {
return {
type: 'UNSELECT_ALL'
}
}
export const removeItemFromStore = (id) => { export const removeItemFromStore = (id) => {
return { return {
type: 'REMOVE_ITEM', type: 'REMOVE_ITEM',

View file

@ -2,19 +2,8 @@
import { createStore } from 'redux'; import { createStore } from 'redux';
import choices from './reducers/index.js'; import choices from './reducers/index.js';
import { addItemToStore, removeItemFromStore, selectItemFromStore, unselectAllFromStore } from './actions/index'; import { addItemToStore, removeItemFromStore, selectItemFromStore } from './actions/index';
import { hasClass, wrap, getSiblings, isType, strToEl } from './lib/utils.js'; import { hasClass, wrap, getSiblings, isType, strToEl, extend } from './lib/utils.js';
/**
TODO:
- State handling
- Dynamically set input width to contents
- Handle select input
- Handle multiple select input ?
*/
export class Choices { export class Choices {
constructor(options) { constructor(options) {
@ -41,95 +30,57 @@ export class Choices {
callbackOnAddItem: function() {} callbackOnAddItem: function() {}
}; };
// Initial instance state
this.initialised = false;
// Merge options with user options // Merge options with user options
this.options = this.extend(defaultOptions, userOptions || {}); this.options = extend(defaultOptions, userOptions || {});
// Create data store
this.store = createStore(choices); this.store = createStore(choices);
this.initialised = false; // Cutting the mustard
this.supports = 'querySelector' in document && 'addEventListener' in document && 'classList' in fakeEl; this.supports = 'querySelector' in document && 'addEventListener' in document && 'classList' in fakeEl;
// Retrieve triggering element (i.e. element with 'data-choice' trigger) // Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.element = this.options.element; this.element = this.options.element;
// If input already has values, parse the array, otherwise create a blank array
// Hmm, this should really map this.store
this.valueArray = this.element.value !== '' ? this.cleanInputValue(this.element.value) : [];
// How many values in array
this.valueCount = this.valueArray.length;
// Bind methods // Bind methods
this.onKeyDown = this.onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
this.onClick = this.onClick.bind(this); this.onClick = this.onClick.bind(this);
this.renderItems = this.renderItems.bind(this); this.render = this.render.bind(this);
// Let's have it large
this.init(); this.init();
} }
cleanInputValue(value) { /* State tests */
// Remove spaces and split with delimiter
return value.replace(/\s/g, '').split(this.options.delimiter);
}
/** /**
* Merges unspecified amount of objects into new object * Whether input is disabled
* @private * @return {Boolean}
* @return {Object} Merged object of arguments
*/ */
extend() {
let extended = {};
let length = arguments.length;
/**
* Merge one object into another
* @param {Object} obj Object to merge into extended object
*/
let merge = function(obj) {
for (let prop in obj) {
extended[prop] = obj[prop];
}
};
// Loop through each passed argument
for (let i = 0; i < length; i++) {
// store argument at position i
let obj = arguments[i];
// If we are in fact dealing with an object, merge it. Otherwise throw error
if (isType('Object', obj)) {
merge(obj);
} else {
console.error('Custom options must be an object');
}
}
return extended;
};
/* State */
isOpen() {
}
isDisabled() { isDisabled() {
return (this.input.disabled) ? true : false;
} }
/**
* Whether there are no values
* @return {Boolean}
*/
isEmpty() { isEmpty() {
return (this.valueCount.length === 0) ? true : false; return (this.store.getState().length === 0) ? true : false;
}
clearInput() {
if (this.input.value) this.input.value = '';
} }
/* Event handling */ /* Event handling */
onKeyUp(e) { /**
} * Handle keydown event
* @param {Object} e Event
* @return
*/
onKeyDown(e) { onKeyDown(e) {
const storeValues = this.store.getState();
const ctrlDownKey = e.ctrlKey || e.metaKey; const ctrlDownKey = e.ctrlKey || e.metaKey;
const deleteKey = 8 || 46; const deleteKey = 8 || 46;
const enterKey = 13; const enterKey = 13;
@ -137,6 +88,7 @@ export class Choices {
// If we are typing in the input // If we are typing in the input
if(e.target === this.input) { if(e.target === this.input) {
// If CTRL + A or CMD + A have been pressed and there are items to select // If CTRL + A or CMD + A have been pressed and there are items to select
if (ctrlDownKey && e.keyCode === aKey && this.list && this.list.children) { if (ctrlDownKey && e.keyCode === aKey && this.list && this.list.children) {
let handleSelectAll = () => { let handleSelectAll = () => {
@ -164,9 +116,9 @@ export class Choices {
// If no duplicates are allowed, and the value already exists // If no duplicates are allowed, and the value already exists
// in the array, don't update // in the array, don't update
if (this.options.allowDuplicates === false && this.element.value) { if (this.options.allowDuplicates === false && this.element.value) {
if (this.valueArray.indexOf(value) > -1) { canUpdate = !storeValues.some((item) => {
canUpdate = false; return item.value === value;
} });
} }
// All is good, update // All is good, update
@ -184,7 +136,6 @@ export class Choices {
// All is good, add // All is good, add
if(canAddItem) { if(canAddItem) {
this.addItem(value); this.addItem(value);
this.updateInputValue(value);
this.clearInput(this.element); this.clearInput(this.element);
} }
} }
@ -226,6 +177,12 @@ export class Choices {
} }
} }
/**
* Handle click event
* @param {Object} e Event
* @return
*/
onClick(e) { onClick(e) {
if(e.target.tagName === 'LI') { if(e.target.tagName === 'LI') {
let item = e.target; let item = e.target;
@ -235,7 +192,7 @@ export class Choices {
let items = this.list.children; let items = this.list.children;
// We only want to select one item with a click // We only want to select one item with a click
// so we unselect any items that aren't the target // so we deselect any items that aren't the target
for (var i = 0; i < items.length; i++) { for (var i = 0; i < items.length; i++) {
let singleItem = items[i]; let singleItem = items[i];
let id = singleItem.getAttribute('data-choice-id');; let id = singleItem.getAttribute('data-choice-id');;
@ -243,7 +200,7 @@ export class Choices {
if(id === passedId && !singleItem.classList.contains('is-selected')) { if(id === passedId && !singleItem.classList.contains('is-selected')) {
this.selectItem(singleItem); this.selectItem(singleItem);
} else { } else {
this.unselectItem(singleItem); this.deselectItem(singleItem);
} }
} }
} }
@ -253,12 +210,20 @@ export class Choices {
} }
/* Methods */ /* Methods */
setValue() {}
getValue() {} /**
* Set value of input to blank
getValues() {} * @return
*/
clearInput() {
if (this.input.value) this.input.value = '';
}
/**
* Tests value against a regular expression
* @param {string} value Value to test
* @return {Boolean} Whether test passed/failed
*/
regexFilter(value) { regexFilter(value) {
let expression = new RegExp(this.options.regexFilter, 'i'); let expression = new RegExp(this.options.regexFilter, 'i');
let passesTest = expression.test(value); let passesTest = expression.test(value);
@ -266,16 +231,32 @@ export class Choices {
return passesTest; return passesTest;
} }
/**
* Select item (a selected item can be deleted)
* @param {Element} item Element to select
* @return
*/
selectItem(item) { selectItem(item) {
let id = item.getAttribute('data-choice-id'); let id = item.getAttribute('data-choice-id');
this.store.dispatch(selectItemFromStore(id, true)); this.store.dispatch(selectItemFromStore(id, true));
} }
unselectItem(item) { /**
* Deselect item
* @param {Element} item Element to de-select
* @return
*/
deselectItem(item) {
let id = item.getAttribute('data-choice-id'); let id = item.getAttribute('data-choice-id');
this.store.dispatch(selectItemFromStore(id, false)); this.store.dispatch(selectItemFromStore(id, false));
} }
/**
* Select items within array
* @param {Array} items Array of items to select
* @return
*/
selectAll(items) { selectAll(items) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
let item = items[i]; let item = items[i];
@ -283,25 +264,10 @@ export class Choices {
}; };
} }
updateInputValue(value) { /**
if (this.options.debug) console.debug('Update input value'); * Add item to store with correct value
* @param {String} value Value to add to store
// Push new value to array */
this.valueArray.push(value);
// Caste array to string and set it as the hidden inputs value
this.element.value = this.valueArray.join(this.options.delimiter);
}
removeInputValue(value) {
if (this.options.debug) console.debug('Remove input value');
let index = this.valueArray.indexOf(value);
this.valueArray.splice(index, 1);
this.element.value = this.valueArray.join(this.options.delimiter);
}
addItem(value) { addItem(value) {
if (this.options.debug) console.debug('Add item'); if (this.options.debug) console.debug('Add item');
@ -330,9 +296,12 @@ export class Choices {
} }
this.store.dispatch(addItemToStore(passedValue, id)); this.store.dispatch(addItemToStore(passedValue, id));
this.store.dispatch(unselectAllFromStore(passedValue, id));
} }
/**
* Remove item from store
* @param
*/
removeItem(item) { removeItem(item) {
if(!item) { if(!item) {
console.error('removeItem: No item was passed to be removed'); console.error('removeItem: No item was passed to be removed');
@ -354,23 +323,27 @@ export class Choices {
this.store.dispatch(removeItemFromStore(id)); this.store.dispatch(removeItemFromStore(id));
} }
/**
* Remove all items from array
* @param {Array} items Items to remove from store
* @return
*/
removeAll(items) { removeAll(items) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
let item = items[i]; let item = items[i];
if (item.classList.contains('is-selected')) { if (item.classList.contains('is-selected')) {
this.removeItem(item); this.removeItem(item);
this.removeInputValue(item.textContent);
} }
}; };
} }
/* Rendering */
init() { /**
if (!this.supports) console.error('init: Your browser doesn\'nt support shit'); * Create DOM structure around passed text element
this.initialised = true; * @return
this.renderInput(this.element); */
}
renderTextInput() { renderTextInput() {
/* /*
Template: Template:
@ -419,44 +392,66 @@ export class Choices {
this.input = input; this.input = input;
this.list = list; this.list = list;
if (this.element.value !== '') { // Add any preset values seperated by delimiter
// Add any preset values let valueArray = this.element.value !== '' ? this.element.value.split(this.options.delimiter) : [];
this.valueArray.forEach((value) => { valueArray.forEach((value) => {
this.addItem(value); this.addItem(value);
}); });
}
// Trigger event listeners // Trigger event listeners
document.addEventListener('keydown', this.onKeyDown); document.addEventListener('keydown', this.onKeyDown);
this.list.addEventListener('click', this.onClick); this.list.addEventListener('click', this.onClick);
// Subscribe to store
this.store.subscribe(this.render);
// Render any items
this.render();
} }
renderItems(){ /**
let items = this.store.getState(); * Render DOM with values
* @return
*/
render() {
let state = this.store.getState();
// Simplify store data to just values
let valueArray = state.reduce((prev, current) => {
prev.push(current.value);
return prev;
}, []);
// Assign hidden input array of values
this.element.value = valueArray.join(this.options.delimiter);
// Clear list
this.list.innerHTML = ''; this.list.innerHTML = '';
items.forEach((item) => { // Add each list item to list
state.forEach((item) => {
if(item.active) { if(item.active) {
// Create new list element // Create new list element
let listItem = strToEl(`<li class="choices__item ${ item.selected ? 'is-selected' : '' }" data-choice-id="${item.id}" data-choice-selected="${item.selected}">${item.value}</li>`); let listItem = strToEl(`<li class="choices__item ${ item.selected ? 'is-selected' : '' }" data-choice-id="${ item.id }" data-choice-selected="${ item.selected }">${ item.value }</li>`);
// Append it to list // Append it to list
this.list.appendChild(listItem); this.list.appendChild(listItem);
} }
}); });
console.log(items); console.log(state);
} }
renderInput() { /**
* Determine how an input should be rendered
* @return {Element} Input to test
*/
renderInput(input) {
if (this.options.debug) console.debug('Render'); if (this.options.debug) console.debug('Render');
switch (this.element.type) { switch (input.type) {
case "text": case "text":
this.renderTextInput(); this.renderTextInput();
this.store.subscribe(this.renderItems);
this.renderItems();
break; break;
case "select-one": case "select-one":
// this.renderSelectInput(); // this.renderSelectInput();
@ -470,11 +465,24 @@ export class Choices {
} }
} }
/**
* Initialise Choices
* @return
*/
init() {
if (!this.supports) console.error('init: Your browser doesn\'nt support shit');
this.initialised = true;
this.renderInput(this.element);
}
/**
* Destroy Choices and nullify values
* @return
*/
destroy() { destroy() {
this.options = null; this.options = null;
this.element = null; this.element = null;
this.initialised = null; this.initialised = null;
this.removeEventListeners(this.input);
} }
}; };
@ -487,15 +495,15 @@ export class Choices {
let choices1 = new Choices({ let choices1 = new Choices({
element : input1, element : input1,
// delimiter: ' ', delimiter: ' ',
editItems: true, editItems: true,
maxItems: 5, maxItems: 5,
callbackOnRemoveItem: function(value) { // callbackOnRemoveItem: function(value) {
console.log(value); // console.log(value);
}, // },
callbackOnAddItem: function(item, value) { // callbackOnAddItem: function(item, value) {
console.log(item, value); // console.log(item, value);
} // }
}); });
let choices2 = new Choices({ let choices2 = new Choices({

View file

@ -24,6 +24,40 @@ export const isType = function(type, obj) {
return obj !== undefined && obj !== null && clas === type; return obj !== undefined && obj !== null && clas === type;
}; };
/**
* Merges unspecified amount of objects into new object
* @private
* @return {Object} Merged object of arguments
*/
export const extend = function() {
let extended = {};
let length = arguments.length;
/**
* Merge one object into another
* @param {Object} obj Object to merge into extended object
*/
let merge = function(obj) {
for (let prop in obj) {
extended[prop] = obj[prop];
}
};
// Loop through each passed argument
for (let i = 0; i < length; i++) {
// store argument at position i
let obj = arguments[i];
// If we are in fact dealing with an object, merge it. Otherwise throw error
if (isType('Object', obj)) {
merge(obj);
} else {
console.error('Custom options must be an object');
}
}
return extended;
};
/** /**
* CSS transition end event listener * CSS transition end event listener

View file

@ -1,21 +1,22 @@
const choices = (state = [], action) => { const initialState = [];
const choices = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'ADD_ITEM': case 'ADD_ITEM':
// Add object to items array // Add object to items array
return [...state, { let newState = [...state, {
id: parseInt(action.id), id: parseInt(action.id),
value: action.value, value: action.value,
active: true, active: true,
selected: false selected: false
}]; }];
case 'UNSELECT_ALL': return newState.map((item) => {
return state.map((item) => {
if(item.selected) { if(item.selected) {
item.selected = false; item.selected = false;
} }
return item; return item;
}); });;
case 'REMOVE_ITEM': case 'REMOVE_ITEM':
// Set item to inactive // Set item to inactive
@ -35,7 +36,6 @@ const choices = (state = [], action) => {
return item; return item;
}); });
default: default:
return state; return state;
} }

View file

@ -8,7 +8,7 @@
<body> <body>
<div class="container"> <div class="container">
<label for="1">Text input with no values and a limit of 5 items</label> <label for="1">Text input with no values and a limit of 5 items</label>
<input id="1" type="text" data-choice value="preset-1, preset-2"> <input id="1" type="text" data-choice value="preset-1 preset-2">
<label for="2">Text input with preset values, custom classes and a placeholder. No duplicate values allowed</label> <label for="2">Text input with preset values, custom classes and a placeholder. No duplicate values allowed</label>
<input id="2" type="text" data-choice value="preset-1, preset-2" placeholder="This is a placeholder" class="custom class"> <input id="2" type="text" data-choice value="preset-1, preset-2" placeholder="This is a placeholder" class="custom class">