diff --git a/README.md b/README.md index 3f92d8a..3df59d5 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ Or include Choices directly: ```html - + - + - + ``` ## Setup diff --git a/src/scripts/choices.js b/src/scripts/choices.js index 13f80be..05a4e4e 100644 --- a/src/scripts/choices.js +++ b/src/scripts/choices.js @@ -131,7 +131,8 @@ class Choices { this.passedElement.value.split(this.config.delimiter), ); } - this.render = this.render.bind(this); + + this._render = this._render.bind(this); this._onFocus = this._onFocus.bind(this); this._onBlur = this._onBlur.bind(this); this._onKeyUp = this._onKeyUp.bind(this); @@ -169,8 +170,8 @@ class Choices { // Set initial state (We need to clone the state because some reducers // modify the inner objects properties in the state) 🤢 this._initialState = cloneObject(this._store.state); - this._store.subscribe(this.render); - this.render(); + this._store.subscribe(this._render); + this._render(); this._addEventListeners(); this.initialised = true; @@ -232,32 +233,6 @@ class Choices { return this; } - render() { - this._currentState = this._store.state; - - const stateChanged = - this._currentState.choices !== this._prevState.choices || - this._currentState.groups !== this._prevState.groups || - this._currentState.items !== this._prevState.items; - const shouldRenderChoices = this._isSelectElement; - const shouldRenderItems = - this._currentState.items !== this._prevState.items; - - if (!stateChanged) { - return; - } - - if (shouldRenderChoices) { - this._renderChoices(); - } - - if (shouldRenderItems) { - this._renderItems(); - } - - this._prevState = this._currentState; - } - highlightItem(item, runEvent = true) { if (!item) { return this; @@ -485,6 +460,123 @@ class Choices { = Private functions = ============================================= */ + _render() { + this._currentState = this._store.state; + + const stateChanged = + this._currentState.choices !== this._prevState.choices || + this._currentState.groups !== this._prevState.groups || + this._currentState.items !== this._prevState.items; + const shouldRenderChoices = this._isSelectElement; + const shouldRenderItems = + this._currentState.items !== this._prevState.items; + + if (!stateChanged) { + return; + } + + if (shouldRenderChoices) { + this._renderChoices(); + } + + if (shouldRenderItems) { + this._renderItems(); + } + + this._prevState = this._currentState; + } + + _renderChoices() { + const { activeGroups, activeChoices } = this._store; + let choiceListFragment = document.createDocumentFragment(); + + this.choiceList.clear(); + + if (this.config.resetScrollPosition) { + requestAnimationFrame(() => this.choiceList.scrollToTop()); + } + + // If we have grouped options + if (activeGroups.length >= 1 && !this._isSearching) { + // If we have a placeholder choice along with groups + const activePlaceholders = activeChoices.filter( + activeChoice => + activeChoice.placeholder === true && activeChoice.groupId === -1, + ); + if (activePlaceholders.length >= 1) { + choiceListFragment = this._createChoicesFragment( + activePlaceholders, + choiceListFragment, + ); + } + choiceListFragment = this._createGroupsFragment( + activeGroups, + activeChoices, + choiceListFragment, + ); + } else if (activeChoices.length >= 1) { + choiceListFragment = this._createChoicesFragment( + activeChoices, + choiceListFragment, + ); + } + + // If we have choices to show + if ( + choiceListFragment.childNodes && + choiceListFragment.childNodes.length > 0 + ) { + const activeItems = this._store.activeItems; + const canAddItem = this._canAddItem(activeItems, this.input.value); + + // ...and we can select them + if (canAddItem.response) { + // ...append them and highlight the first choice + this.choiceList.append(choiceListFragment); + this._highlightChoice(); + } else { + // ...otherwise show a notice + this.choiceList.append(this._getTemplate('notice', canAddItem.notice)); + } + } else { + // Otherwise show a notice + let dropdownItem; + let notice; + + if (this._isSearching) { + notice = isType('Function', this.config.noResultsText) + ? this.config.noResultsText() + : this.config.noResultsText; + + dropdownItem = this._getTemplate('notice', notice, 'no-results'); + } else { + notice = isType('Function', this.config.noChoicesText) + ? this.config.noChoicesText() + : this.config.noChoicesText; + + dropdownItem = this._getTemplate('notice', notice, 'no-choices'); + } + + this.choiceList.append(dropdownItem); + } + } + + _renderItems() { + const activeItems = this._store.activeItems || []; + this.itemList.clear(); + + if (activeItems.length) { + // Create a fragment to store our list items + // (so we don't have to update the DOM for each item) + const itemListFragment = this._createItemsFragment(activeItems); + + // If we have items to add, append them + if (itemListFragment.childNodes) { + this.itemList.append(itemListFragment); + } + } + } + _createGroupsFragment(groups, choices, fragment) { const groupFragment = fragment || document.createDocumentFragment(); const getGroupChoices = group => @@ -793,6 +885,30 @@ class Choices { } } + _handleSearch(value) { + if (!value || !this.input.isFocussed) { + return; + } + + const choices = this._store.choices; + const { searchFloor, searchChoices } = this.config; + const hasUnactiveChoices = choices.some(option => !option.active); + + // Check that we have a value to search and the input was an alphanumeric character + if (value && value.length >= searchFloor) { + const resultCount = searchChoices ? this._searchChoices(value) : 0; + // Trigger search event + this.passedElement.triggerEvent(EVENTS.search, { + value, + resultCount, + }); + } else if (hasUnactiveChoices) { + // Otherwise reset choices to active + this._isSearching = false; + this._store.dispatch(activateChoices(true)); + } + } + _canAddItem(activeItems, value) { let canAddItem = true; let notice = isType('Function', this.config.addItemText) @@ -916,30 +1032,6 @@ class Choices { return results.length; } - _handleSearch(value) { - if (!value || !this.input.isFocussed) { - return; - } - - const choices = this._store.choices; - const { searchFloor, searchChoices } = this.config; - const hasUnactiveChoices = choices.some(option => !option.active); - - // Check that we have a value to search and the input was an alphanumeric character - if (value && value.length >= searchFloor) { - const resultCount = searchChoices ? this._searchChoices(value) : 0; - // Trigger search event - this.passedElement.triggerEvent(EVENTS.search, { - value, - resultCount, - }); - } else if (hasUnactiveChoices) { - // Otherwise reset choices to active - this._isSearching = false; - this._store.dispatch(activateChoices(true)); - } - } - _addEventListeners() { document.addEventListener('keyup', this._onKeyUp); document.addEventListener('keydown', this._onKeyDown); @@ -1960,97 +2052,6 @@ class Choices { : false; } - _renderChoices() { - const { activeGroups, activeChoices } = this._store; - let choiceListFragment = document.createDocumentFragment(); - - this.choiceList.clear(); - - if (this.config.resetScrollPosition) { - requestAnimationFrame(() => this.choiceList.scrollToTop()); - } - - // If we have grouped options - if (activeGroups.length >= 1 && !this._isSearching) { - // If we have a placeholder choice along with groups - const activePlaceholders = activeChoices.filter( - activeChoice => - activeChoice.placeholder === true && activeChoice.groupId === -1, - ); - if (activePlaceholders.length >= 1) { - choiceListFragment = this._createChoicesFragment( - activePlaceholders, - choiceListFragment, - ); - } - choiceListFragment = this._createGroupsFragment( - activeGroups, - activeChoices, - choiceListFragment, - ); - } else if (activeChoices.length >= 1) { - choiceListFragment = this._createChoicesFragment( - activeChoices, - choiceListFragment, - ); - } - - // If we have choices to show - if ( - choiceListFragment.childNodes && - choiceListFragment.childNodes.length > 0 - ) { - const activeItems = this._store.activeItems; - const canAddItem = this._canAddItem(activeItems, this.input.value); - - // ...and we can select them - if (canAddItem.response) { - // ...append them and highlight the first choice - this.choiceList.append(choiceListFragment); - this._highlightChoice(); - } else { - // ...otherwise show a notice - this.choiceList.append(this._getTemplate('notice', canAddItem.notice)); - } - } else { - // Otherwise show a notice - let dropdownItem; - let notice; - - if (this._isSearching) { - notice = isType('Function', this.config.noResultsText) - ? this.config.noResultsText() - : this.config.noResultsText; - - dropdownItem = this._getTemplate('notice', notice, 'no-results'); - } else { - notice = isType('Function', this.config.noChoicesText) - ? this.config.noChoicesText() - : this.config.noChoicesText; - - dropdownItem = this._getTemplate('notice', notice, 'no-choices'); - } - - this.choiceList.append(dropdownItem); - } - } - - _renderItems() { - const activeItems = this._store.activeItems || []; - this.itemList.clear(); - - if (activeItems.length) { - // Create a fragment to store our list items - // (so we don't have to update the DOM for each item) - const itemListFragment = this._createItemsFragment(activeItems); - - // If we have items to add, append them - if (itemListFragment.childNodes) { - this.itemList.append(itemListFragment); - } - } - } - /* ===== End of Private functions ====== */ } diff --git a/src/scripts/choices.test.js b/src/scripts/choices.test.js index 4999c2c..800e83d 100644 --- a/src/scripts/choices.test.js +++ b/src/scripts/choices.test.js @@ -61,7 +61,7 @@ describe('choices', () => { createTemplatesSpy = spy(instance, '_createTemplates'); createInputSpy = spy(instance, '_createStructure'); storeSubscribeSpy = spy(instance._store, 'subscribe'); - renderSpy = spy(instance, 'render'); + renderSpy = spy(instance, '_render'); addEventListenersSpy = spy(instance, '_addEventListeners'); instance.initialised = false; @@ -90,7 +90,7 @@ describe('choices', () => { it('subscribes to store with render method', () => { expect(storeSubscribeSpy.called).to.equal(true); - expect(storeSubscribeSpy.lastCall.args[0]).to.equal(instance.render); + expect(storeSubscribeSpy.lastCall.args[0]).to.equal(instance._render); }); it('fires initial render', () => { @@ -1748,32 +1748,5 @@ describe('choices', () => { }); }); }); - - // describe('render', () => { - // beforeEach(() => {}); - - // describe('no change to state', () => { - // it('returns early', () => {}); - // }); - - // describe('change to state', () => { - // it('updates previous state to current state', () => {}); - - // describe('select element', () => { - // it('clears choice list', () => {}); - - // describe('when resetScrollPosition config option is set to true', () => { - // it('scrolls to top of choice list', () => {}); - // }); - // }); - - // describe('text element', () => { - // describe('active items in store', () => { - // it('clears item list', () => {}); - // it('renders active items inside item list', () => {}); - // }); - // }); - // }); - // }); }); });