diff --git a/CHANGELOG.md b/CHANGELOG.md index 4256077..4e0b4fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project are documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## [4.0.3] - 2026-06-17 + +### Fixed +- **`setSource()` now invalidates rendered results.** Swapping the source while + the control was closed left the previous source's options in `this.results`; + the focus path's `!results.length` guard then skipped the re-search, so the + next open showed stale options (e.g. a dependent Country → Region dropdown + never refreshed). `setSource()` now clears `results`/`_total`/`activeIndex` + (matching `setScope()`), so the next open reflects the new source. The current + selection/value is left untouched. The `setScope({})` workaround is no longer + needed. + ## [4.0.2] - 2026-06-17 ### Docs diff --git a/dist/liveselect.js b/dist/liveselect.js index 4ee2fc4..32804f3 100644 --- a/dist/liveselect.js +++ b/dist/liveselect.js @@ -1043,7 +1043,14 @@ /** Swap the data source (e.g. after pushing a new item to an array). */ LiveSelect.prototype.setSource = function (source) { this.opts.source = source; - this._cache = {}; // results from the old source are no longer valid + // Invalidate everything rendered from the old source — otherwise a closed + // control keeps its stale results, and _onFocus's `!results.length` guard + // then skips the re-search on the next open. (The selection/value is left + // untouched; only the available options change.) + this.results = []; + this._total = null; + this.activeIndex = -1; + this._cache = {}; if (this.isOpen) this._runSearch(); }; diff --git a/package.json b/package.json index dfaa9c7..c4b0e24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@michaeljfalk/liveselect", - "version": "4.0.2", + "version": "4.0.3", "publishConfig": { "access": "public" }, diff --git a/test/client.test.js b/test/client.test.js index 18e4f5b..564d650 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -518,3 +518,41 @@ test('enhance('); dd.destroy(); }); + +test('setSource() while closed refreshes the menu on next open (no stale options)', { skip: !domAvailable }, () => { + const host = mount(); + const A = [{ value: 'a1', label: 'Apple' }, { value: 'a2', label: 'Apricot' }]; + const B = [{ value: 'b1', label: 'Banana' }, { value: 'b2', label: 'Blueberry' }]; + const dd = new LiveSelect(host, { source: A }); + + // Open once so results populate from A, then close. + dd.query = ''; dd.isOpen = true; dd._runSearch(); dd._renderMenu(); + assert.equal(dd.results.length, 2); + dd.close(); + + // Swap the source while closed. + dd.setSource(B); + assert.deepEqual(dd.results, [], 'rendered results invalidated immediately'); + + // Re-open via the focus path → must reflect B, not stale A. + dd._onFocus(); + dd._renderMenu(); + const labels = Array.from(dd.menu.querySelectorAll('.liveselect__opt-label')).map((s) => s.textContent); + assert.deepEqual(labels, ['Banana', 'Blueberry'], 'menu lists the new source'); + dd.destroy(); +}); + +test('setSource() leaves the current selection/value intact', { skip: !domAvailable }, () => { + const host = mount(); + const dd = new LiveSelect(host, { + name: 'fruit', + source: [{ value: 'a1', label: 'Apple' }], + }); + dd._select({ value: 'a1', label: 'Apple', sublabel: '' }); + assert.equal(dd.getValue(), 'a1'); + + dd.setSource([{ value: 'b1', label: 'Banana' }]); // swap options only + assert.equal(dd.getValue(), 'a1', 'selected value survives the source swap'); + assert.equal(host.querySelector('input[type=hidden][name=fruit]').value, 'a1'); + dd.destroy(); +});