From 1203c51af10328360e622d5b72d1101d182bfb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=CC=88nther=20Debrauwer?= Date: Mon, 8 Jan 2024 09:45:52 +0100 Subject: [PATCH 1/4] Save optionKey in x-data When optionKey is not saved in x-data, the value is lost after a Livewire update --- packages/ui/src/combobox.js | 14 ++++++++------ packages/ui/src/listbox.js | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/combobox.js b/packages/ui/src/combobox.js index 50739fcc8..aca04a074 100644 --- a/packages/ui/src/combobox.js +++ b/packages/ui/src/combobox.js @@ -43,19 +43,19 @@ export default function (Alpine) { Alpine.magic('comboboxOption', el => { let data = Alpine.$data(el) - let optionEl = Alpine.findClosest(el, i => i.__optionKey) + let optionEl = Alpine.findClosest(el, i => Alpine.$data(i).optionKey) if (! optionEl) throw 'No x-combobox:option directive found...' return { get isActive() { - return data.__context.isActiveKey(optionEl.__optionKey) + return data.__context.isActiveKey(Alpine.$data(optionEl).optionKey) }, get isSelected() { return data.__isSelected(optionEl) }, get isDisabled() { - return data.__context.isDisabled(optionEl.__optionKey) + return data.__context.isDisabled(Alpine.$data(optionEl).optionKey) }, } }) @@ -450,18 +450,20 @@ function handleOption(el, Alpine) { // Initialize... 'x-data'() { return { + 'optionKey': null, + init() { - let key = this.$el.__optionKey = (Math.random() + 1).toString(36).substring(7) + this.optionKey = (Math.random() + 1).toString(36).substring(7) let value = Alpine.extractProp(this.$el, 'value') let disabled = Alpine.extractProp(this.$el, 'disabled', false, false) // memoize the context as it's not going to change // and calling this.$data on mouse action is expensive - this.__context.registerItem(key, this.$el, value, disabled) + this.__context.registerItem(this.optionKey, this.$el, value, disabled) }, destroy() { - this.__context.unregisterItem(this.$el.__optionKey) + this.__context.unregisterItem(this.optionKey) } } }, diff --git a/packages/ui/src/listbox.js b/packages/ui/src/listbox.js index 08eebb972..e088d63f7 100644 --- a/packages/ui/src/listbox.js +++ b/packages/ui/src/listbox.js @@ -48,19 +48,19 @@ export default function (Alpine) { Alpine.magic('listboxOption', (el) => { let data = Alpine.$data(el) - let optionEl = Alpine.findClosest(el, i => i.__optionKey) + let optionEl = Alpine.findClosest(el, i => Alpine.$date(i).optionKey) if (! optionEl) throw 'No x-combobox:option directive found...' return { get isActive() { - return data.__context.isActiveKey(optionEl.__optionKey) + return data.__context.isActiveKey(Alpine.$date(optionEl).optionKey) }, get isSelected() { return data.__isSelected(optionEl) }, get isDisabled() { - return data.__context.isDisabled(optionEl.__optionKey) + return data.__context.isDisabled(Alpine.$date(optionEl).optionKey) }, } }) @@ -339,16 +339,18 @@ function handleOption(el, Alpine) { // Initialize... 'x-data'() { return { + 'optionKey': null, + init() { - let key = el.__optionKey = (Math.random() + 1).toString(36).substring(7) + this.optionKey = (Math.random() + 1).toString(36).substring(7) let value = Alpine.extractProp(el, 'value') let disabled = Alpine.extractProp(el, 'disabled', false, false) - this.$data.__context.registerItem(key, el, value, disabled) + this.$data.__context.registerItem(this.optionKey, el, value, disabled) }, destroy() { - this.$data.__context.unregisterItem(this.$el.__optionKey) + this.$data.__context.unregisterItem(this.optionKey) }, } }, From 24a4edd2dfe5c3c7382d30a1b8c90798642acba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gu=CC=88nther=20Debrauwer?= Date: Mon, 8 Jan 2024 09:50:10 +0100 Subject: [PATCH 2/4] Fix typos --- packages/ui/src/listbox.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/listbox.js b/packages/ui/src/listbox.js index e088d63f7..17864a3a4 100644 --- a/packages/ui/src/listbox.js +++ b/packages/ui/src/listbox.js @@ -48,19 +48,19 @@ export default function (Alpine) { Alpine.magic('listboxOption', (el) => { let data = Alpine.$data(el) - let optionEl = Alpine.findClosest(el, i => Alpine.$date(i).optionKey) + let optionEl = Alpine.findClosest(el, i => Alpine.$data(i).optionKey) if (! optionEl) throw 'No x-combobox:option directive found...' return { get isActive() { - return data.__context.isActiveKey(Alpine.$date(optionEl).optionKey) + return data.__context.isActiveKey(Alpine.$data(optionEl).optionKey) }, get isSelected() { return data.__isSelected(optionEl) }, get isDisabled() { - return data.__context.isDisabled(Alpine.$date(optionEl).optionKey) + return data.__context.isDisabled(Alpine.$data(optionEl).optionKey) }, } }) From d6e13bd1adfbd62dc899b0e1d81651f00fb237ce Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Mon, 22 Jan 2024 10:07:13 -0500 Subject: [PATCH 3/4] refactor --- packages/ui/src/combobox.js | 15 +++++++-------- packages/ui/src/listbox.js | 14 +++++++------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/combobox.js b/packages/ui/src/combobox.js index aca04a074..d0f6633af 100644 --- a/packages/ui/src/combobox.js +++ b/packages/ui/src/combobox.js @@ -43,19 +43,19 @@ export default function (Alpine) { Alpine.magic('comboboxOption', el => { let data = Alpine.$data(el) - let optionEl = Alpine.findClosest(el, i => Alpine.$data(i).optionKey) + let optionEl = Alpine.findClosest(el, i => data.__optionKey) if (! optionEl) throw 'No x-combobox:option directive found...' return { get isActive() { - return data.__context.isActiveKey(Alpine.$data(optionEl).optionKey) + return data.__context.isActiveKey(Alpine.$data(optionEl).__optionKey) }, get isSelected() { return data.__isSelected(optionEl) }, get isDisabled() { - return data.__context.isDisabled(Alpine.$data(optionEl).optionKey) + return data.__context.isDisabled(Alpine.$data(optionEl).__optionKey) }, } }) @@ -450,20 +450,20 @@ function handleOption(el, Alpine) { // Initialize... 'x-data'() { return { - 'optionKey': null, + '__optionKey': null, init() { - this.optionKey = (Math.random() + 1).toString(36).substring(7) + this.__optionKey = (Math.random() + 1).toString(36).substring(7) let value = Alpine.extractProp(this.$el, 'value') let disabled = Alpine.extractProp(this.$el, 'disabled', false, false) // memoize the context as it's not going to change // and calling this.$data on mouse action is expensive - this.__context.registerItem(this.optionKey, this.$el, value, disabled) + this.__context.registerItem(this.__optionKey, this.$el, value, disabled) }, destroy() { - this.__context.unregisterItem(this.optionKey) + this.__context.unregisterItem(this.__optionKey) } } }, @@ -497,7 +497,6 @@ function handleOption(el, Alpine) { }) } - // Little utility to defer a callback into the microtask queue... function microtask(callback) { return new Promise(resolve => queueMicrotask(() => resolve(callback()))) diff --git a/packages/ui/src/listbox.js b/packages/ui/src/listbox.js index 17864a3a4..44b2e08dd 100644 --- a/packages/ui/src/listbox.js +++ b/packages/ui/src/listbox.js @@ -48,19 +48,19 @@ export default function (Alpine) { Alpine.magic('listboxOption', (el) => { let data = Alpine.$data(el) - let optionEl = Alpine.findClosest(el, i => Alpine.$data(i).optionKey) + let optionEl = Alpine.findClosest(el, i => data.__optionKey) if (! optionEl) throw 'No x-combobox:option directive found...' return { get isActive() { - return data.__context.isActiveKey(Alpine.$data(optionEl).optionKey) + return data.__context.isActiveKey(Alpine.$data(optionEl).__optionKey) }, get isSelected() { return data.__isSelected(optionEl) }, get isDisabled() { - return data.__context.isDisabled(Alpine.$data(optionEl).optionKey) + return data.__context.isDisabled(Alpine.$data(optionEl).__optionKey) }, } }) @@ -339,18 +339,18 @@ function handleOption(el, Alpine) { // Initialize... 'x-data'() { return { - 'optionKey': null, + '__optionKey': null, init() { - this.optionKey = (Math.random() + 1).toString(36).substring(7) + this.__optionKey = (Math.random() + 1).toString(36).substring(7) let value = Alpine.extractProp(el, 'value') let disabled = Alpine.extractProp(el, 'disabled', false, false) - this.$data.__context.registerItem(this.optionKey, el, value, disabled) + this.$data.__context.registerItem(this.__optionKey, el, value, disabled) }, destroy() { - this.$data.__context.unregisterItem(this.optionKey) + this.$data.__context.unregisterItem(this.__optionKey) }, } }, From b17b447d0a53f803d97ffd8befe0581750f53cd1 Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Mon, 22 Jan 2024 10:27:32 -0500 Subject: [PATCH 4/4] more fixes --- packages/ui/src/combobox.js | 6 ++- packages/ui/src/listbox.js | 8 +++- .../integration/plugins/ui/combobox.spec.js | 38 ++++++++++++++++++- .../integration/plugins/ui/listbox.spec.js | 38 ++++++++++++++++++- tests/cypress/utils.js | 12 ++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/combobox.js b/packages/ui/src/combobox.js index d0f6633af..824429665 100644 --- a/packages/ui/src/combobox.js +++ b/packages/ui/src/combobox.js @@ -43,7 +43,11 @@ export default function (Alpine) { Alpine.magic('comboboxOption', el => { let data = Alpine.$data(el) - let optionEl = Alpine.findClosest(el, i => data.__optionKey) + // It's not great depending on the existance of the attribute in the DOM + // but it's probably the fastest and most reliable at this point... + let optionEl = Alpine.findClosest(el, i => { + return i.hasAttribute('x-combobox:option') + }) if (! optionEl) throw 'No x-combobox:option directive found...' diff --git a/packages/ui/src/listbox.js b/packages/ui/src/listbox.js index 44b2e08dd..336c2902d 100644 --- a/packages/ui/src/listbox.js +++ b/packages/ui/src/listbox.js @@ -48,9 +48,13 @@ export default function (Alpine) { Alpine.magic('listboxOption', (el) => { let data = Alpine.$data(el) - let optionEl = Alpine.findClosest(el, i => data.__optionKey) + // It's not great depending on the existance of the attribute in the DOM + // but it's probably the fastest and most reliable at this point... + let optionEl = Alpine.findClosest(el, i => { + return i.hasAttribute('x-listbox:option') + }) - if (! optionEl) throw 'No x-combobox:option directive found...' + if (! optionEl) throw 'No x-listbox:option directive found...' return { get isActive() { diff --git a/tests/cypress/integration/plugins/ui/combobox.spec.js b/tests/cypress/integration/plugins/ui/combobox.spec.js index b3225dfba..d3c9c9720 100644 --- a/tests/cypress/integration/plugins/ui/combobox.spec.js +++ b/tests/cypress/integration/plugins/ui/combobox.spec.js @@ -1,4 +1,4 @@ -import { beVisible, beHidden, haveAttribute, haveClasses, notHaveClasses, haveText, contain, notContain, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test, haveValue, haveLength} from '../../../utils' +import { beVisible, beHidden, haveAttribute, haveClasses, notHaveClasses, haveText, contain, notContain, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test, haveValue, haveLength, ensureNoConsoleWarns} from '../../../utils' test('it works with x-model', [html` @@ -1558,3 +1558,39 @@ test('can remove an option without other options getting removed', get('[check="3"]').should(notBeVisible()) }, ); + +test('works with morph', + [html` +
+
+ + +
    +
  • Laravel
  • +
+
+ + Selected: +
+ `], + ({ get }, reload, window, document) => { + let toHtml = html` +
+
+ + +
    +
  • Laravel
  • +
+
+ + Selected: +
+ ` + ensureNoConsoleWarns() + + get('div').then(([el]) => window.Alpine.morph(el, toHtml)) + + get('button').should(haveText('Select Framework (updated)')) + }, +) diff --git a/tests/cypress/integration/plugins/ui/listbox.spec.js b/tests/cypress/integration/plugins/ui/listbox.spec.js index f1da78bc7..faa6aebb4 100644 --- a/tests/cypress/integration/plugins/ui/listbox.spec.js +++ b/tests/cypress/integration/plugins/ui/listbox.spec.js @@ -1,4 +1,4 @@ -import { beVisible, beHidden, haveAttribute, haveClasses, notHaveClasses, haveText, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test} from '../../../utils' +import { beVisible, beHidden, haveAttribute, haveClasses, notHaveClasses, haveText, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test, ensureNoConsoleWarns} from '../../../utils' test('it works with x-model', [html` @@ -865,4 +865,40 @@ test('"static" prop', }, ) +test('works with morph', + [html` +
+
+ + +
    +
  • Laravel
  • +
+
+ + Selected: +
+ `], + ({ get }, reload, window, document) => { + let toHtml = html` +
+
+ + +
    +
  • Laravel
  • +
+
+ + Selected: +
+ ` + ensureNoConsoleWarns() + + get('div').then(([el]) => window.Alpine.morph(el, toHtml)) + + get('button').should(haveText('Select Framework (updated)')) + }, +) + // test "by" attribute diff --git a/tests/cypress/utils.js b/tests/cypress/utils.js index 898975613..11dab95b3 100644 --- a/tests/cypress/utils.js +++ b/tests/cypress/utils.js @@ -5,6 +5,18 @@ export function html(strings) { return strings.raw[0] } +export function ensureNoConsoleWarns() { + cy.window().then((win) => { + let cache = win.console.warn + + win.console.warn = () => { throw new Error('Console warn was triggered') } + + cy.on('window:before:unload', () => { + win.console.warn = cache + }); + }); +} + export let test = function (name, template, callback, handleExpectedErrors = false) { it(name, () => { injectHtmlAndBootAlpine(cy, template, callback, undefined, handleExpectedErrors)