Browse Source

Fix menu morphing

Caleb Porzio 1 year ago
parent
commit
be4b4199e6

+ 13 - 0
packages/morph/src/morph.js

@@ -132,6 +132,10 @@ export function morph(from, toHtml, options) {
         let currentFrom = getFirstNode(from)
 
         while (currentTo) {
+            // If the "from" element has a dynamically bound "id" (x-bind:id="..."),
+            // Let's transfer it to the "to" element so that there isn't a key mismatch...
+            seedingMatchingId(currentTo, currentFrom)
+
             let toKey = getKey(currentTo)
             let fromKey = getKey(currentFrom)
 
@@ -486,3 +490,12 @@ function monkeyPatchDomSetAttributeToAllowAtSymbols() {
         this.setAttributeNode(attr)
     }
 }
+
+function seedingMatchingId(to, from) {
+    let fromId = from && from._x_bindings && from._x_bindings.id
+
+    if (! fromId) return
+
+    to.setAttribute('id', fromId)
+    to.id = fromId
+}

+ 10 - 4
packages/ui/src/menu.js

@@ -14,7 +14,7 @@ export default function (Alpine) {
                 return $data.__activeEl == $data.__itemEl
             },
             get isDisabled() {
-                return el.__isDisabled.value
+                return $data.__itemEl.__isDisabled.value
             },
         }
     })
@@ -78,6 +78,12 @@ function handleButton(el, Alpine) {
     })
 }
 
+// When patching children:
+// The child isn't initialized until it is reached. This is normally fine
+// except when something like this happens where an "id" is added during the initializing phase
+// because the "to" element hasn't initialized yet, it doesn't have the ID, so there is a "key" mismatch
+
+
 function handleItems(el, Alpine) {
     Alpine.bind(el, {
         'x-ref': '__items',
@@ -158,10 +164,10 @@ function handleItem(el, Alpine) {
             },
             'x-id'() { return ['alpine-menu-item'] },
             ':id'() { return this.$id('alpine-menu-item') },
-            ':tabindex'() { return this.$el.__isDisabled.value ? false : '-1' },
+            ':tabindex'() { return this.__itemEl.__isDisabled.value ? false : '-1' },
             'role': 'menuitem',
-            '@mousemove'() { this.$el.__isDisabled.value || this.$menuItem.isActive || this.$el.__activate() },
-            '@mouseleave'() { this.$el.__isDisabled.value || ! this.$menuItem.isActive || this.$el.__deactivate() },
+            '@mousemove'() { this.__itemEl.__isDisabled.value || this.$menuItem.isActive || this.__itemEl.__activate() },
+            '@mouseleave'() { this.__itemEl.__isDisabled.value || ! this.$menuItem.isActive || this.__itemEl.__deactivate() },
         }
     })
 }

+ 67 - 0
tests/cypress/integration/plugins/morph.spec.js

@@ -337,6 +337,29 @@ test('can morph using different keys',
     },
 )
 
+test('can morph elements with dynamic ids',
+    [html`
+        <ul>
+            <li x-data x-bind:id="'1'" >foo<input></li>
+        </ul>
+    `],
+    ({ get }, reload, window, document) => {
+        let toHtml = html`
+            <ul>
+                <li x-data x-bind:id="'1'" >foo<input></li>
+            </ul>
+        `
+
+        get('input').type('foo')
+
+        get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {
+            key(el) { return el.id }
+        }))
+
+        get('li:nth-of-type(1) input').should(haveValue('foo'))
+    },
+)
+
 test('can morph different inline nodes',
     [html`
     <div id="from">
@@ -470,3 +493,47 @@ test('can morph @event handlers', [
         get('button').should(haveText('buzz'));
     }
 );
+
+test.only('can morph menu',
+    [html`
+        <main x-data>
+            <article x-menu>
+                <button data-trigger x-menu:button x-text="'ready'"></button>
+
+                <div x-menu:items>
+                    <button x-menu:item href="#edit">
+                        Edit
+                        <input>
+                    </button>
+                </div>
+            </article>
+        </main>
+    `],
+    ({ get }, reload, window, document) => {
+        let toHtml = html`
+            <main x-data>
+                <article x-menu>
+                    <button data-trigger x-menu:button x-text="'ready'"></button>
+
+                    <div x-menu:items>
+                        <button x-menu:item href="#edit">
+                            Edit
+                            <input>
+                        </button>
+                    </div>
+                </article>
+            </main>
+        `
+
+        get('[data-trigger]').should(haveText('ready'));
+        get('button[data-trigger').click()
+
+        get('input').type('foo')
+
+        get('main').then(([el]) => window.Alpine.morph(el, toHtml, {
+            key(el) { return el.id }
+        }))
+
+        get('input').should(haveValue('foo'))
+    },
+)