Browse Source

Fix components instantiated twice because of complex mutations (#2376)

* Add failing test

* Fix component instantiated twice because of complex mutations

* Update comment
Simone Todaro 3 years ago
parent
commit
282aacfc9e
2 changed files with 49 additions and 1 deletions
  1. 21 1
      packages/alpinejs/src/mutation.js
  2. 28 0
      tests/cypress/integration/mutation.spec.js

+ 21 - 1
packages/alpinejs/src/mutation.js

@@ -160,13 +160,33 @@ function onMutate(mutations) {
         onAttributeAddeds.forEach(i => i(el, attrs))
     })
 
+    // Mutations are undled together by the browser but sometimes
+    // for complex cases, there may be javascript code adding a wrapper
+    // and then an alpine component as a child of that wrapper in the same
+    // function and the mutation observer will receive 2 different mutations.
+    // when it comes to run them, the dom contains both changes so the child
+    // element would be processed twice as Alpine calls initTree on
+    // both mutations. We mark all node as _x_ignored and only remove the flag
+    // when processing the node to avoid those duplicates.
+    addedNodes.forEach((node) => {
+        node._x_ignoreSelf = true
+        node._x_ignore = true
+    })
     for (let node of addedNodes) {
-       // If an element gets moved on a page, it's registered
+        // If an element gets moved on a page, it's registered
         // as both an "add" and "remove", so we want to skip those.
         if (removedNodes.includes(node)) continue
 
+        delete node._x_ignoreSelf
+        delete node._x_ignore
         onElAddeds.forEach(i => i(node))
+        node._x_ignore = true
+        node._x_ignoreSelf = true
     }
+    addedNodes.forEach((node) => {
+        delete node._x_ignoreSelf
+        delete node._x_ignore
+    })
 
     for (let node of removedNodes) {
         // If an element gets moved on a page, it's registered

+ 28 - 0
tests/cypress/integration/mutation.spec.js

@@ -110,3 +110,31 @@ test('can pause and queue mutations for later resuming/flushing',
         get('h1').should(haveText('3'))
     }
 )
+
+test('does not initialise components twice when contained in multiple mutations',
+    html`
+        <div x-data="{
+            foo: 0,
+            bar: 0,
+            test() {
+                container = document.createElement('div')
+                this.$root.appendChild(container)
+                alpineElement = document.createElement('span')
+                alpineElement.setAttribute('x-data', '{init() {this.bar++}}')
+                alpineElement.setAttribute('x-init', 'foo++')
+                container.appendChild(alpineElement)
+            }
+        }">
+            <span id="one" x-text="foo"></span>
+            <span id="two" x-text="bar"></span>
+            <button @click="test">Test</button>
+        </div>
+    `,
+    ({ get }) => {
+        get('span#one').should(haveText('0'))
+        get('span#two').should(haveText('0'))
+        get('button').click()
+        get('span#one').should(haveText('1'))
+        get('span#two').should(haveText('1'))
+    }
+)