浏览代码

Merge pull request #88 from alpinejs/refactor-to-crawler-based-nested-component-discovery

Move nested component initialization into components
Caleb Porzio 5 年之前
父节点
当前提交
8465e69d1f
共有 6 个文件被更改,包括 111 次插入30 次删除
  1. 0 0
      dist/alpine.js
  2. 0 0
      dist/alpine.js.map
  3. 43 16
      src/component.js
  4. 6 0
      src/index.js
  5. 3 5
      src/utils.js
  6. 59 9
      test/constructor.spec.js

文件差异内容过多而无法显示
+ 0 - 0
dist/alpine.js


文件差异内容过多而无法显示
+ 0 - 0
dist/alpine.js.map


+ 43 - 16
src/component.js

@@ -1,4 +1,4 @@
-import { arrayUnique, walkSkippingNestedComponents, keyToModifier, saferEval, saferEvalNoReturn, getXAttrs, debounce, transitionIn, transitionOut } from './utils'
+import { arrayUnique, walk, keyToModifier, saferEval, saferEvalNoReturn, getXAttrs, debounce, transitionIn, transitionOut } from './utils'
 
 
 export default class Component {
 export default class Component {
     constructor(el) {
     constructor(el) {
@@ -42,7 +42,7 @@ export default class Component {
         }
         }
 
 
         // Register all our listeners and set all our attribute bindings.
         // Register all our listeners and set all our attribute bindings.
-        this.initializeElements()
+        this.initializeElements(this.$el)
 
 
         // Use mutation observer to detect new elements being added within this component at run-time.
         // Use mutation observer to detect new elements being added within this component at run-time.
         // Alpine's just so darn flexible amirite?
         // Alpine's just so darn flexible amirite?
@@ -73,7 +73,7 @@ export default class Component {
                 if (self.pauseReactivity) return
                 if (self.pauseReactivity) return
 
 
                 debounce(() => {
                 debounce(() => {
-                    self.refresh()
+                    self.updateElements(self.$el)
 
 
                     // Walk through the $nextTick stack and clear it as we go.
                     // Walk through the $nextTick stack and clear it as we go.
                     while (self.nextTickStack.length > 0) {
                     while (self.nextTickStack.length > 0) {
@@ -105,9 +105,29 @@ export default class Component {
         return new Proxy(data, proxyHandler)
         return new Proxy(data, proxyHandler)
     }
     }
 
 
-    initializeElements() {
-        walkSkippingNestedComponents(this.$el, el => {
+    walkAndSkipNestedComponents(el, callback, initializeComponentCallback = () => {}) {
+        walk(el, el => {
+            // We've hit a component.
+            if (el.hasAttribute('x-data')) {
+                // If it's not the current one.
+                if (! el.isSameNode(this.$el)) {
+                    // Initialize it if it's not.
+                    if (! el.__x) initializeComponentCallback(el)
+
+                    // Now we'll let that sub-component deal with itself.
+                    return false
+                }
+            }
+
+            callback(el)
+        })
+    }
+
+    initializeElements(rootEl) {
+        this.walkAndSkipNestedComponents(rootEl, el => {
             this.initializeElement(el)
             this.initializeElement(el)
+        }, el => {
+            el.__x = new Component(el)
         })
         })
     }
     }
 
 
@@ -122,6 +142,18 @@ export default class Component {
         this.resolveBoundAttributes(el, true)
         this.resolveBoundAttributes(el, true)
     }
     }
 
 
+    updateElements(rootEl) {
+        this.walkAndSkipNestedComponents(rootEl, el => {
+            this.updateElement(el)
+        }, el => {
+            el.__x = new Component(el)
+        })
+    }
+
+    updateElement(el) {
+        this.resolveBoundAttributes(el)
+    }
+
     registerListeners(el) {
     registerListeners(el) {
         getXAttrs(el).forEach(({ type, value, modifiers, expression }) => {
         getXAttrs(el).forEach(({ type, value, modifiers, expression }) => {
             switch (type) {
             switch (type) {
@@ -221,11 +253,12 @@ export default class Component {
                     mutations[i].addedNodes.forEach(node => {
                     mutations[i].addedNodes.forEach(node => {
                         if (node.nodeType !== 1) return
                         if (node.nodeType !== 1) return
 
 
-                        if (node.matches('[x-data]')) return
-
-                        if (getXAttrs(node).length > 0) {
-                            this.initializeElement(node)
+                        if (node.matches('[x-data]')) {
+                            node.__x = new Component(node)
+                            return
                         }
                         }
+
+                        this.initializeElements(node)
                     })
                     })
                 }
                 }
               }
               }
@@ -234,12 +267,6 @@ export default class Component {
         observer.observe(targetNode, observerOptions);
         observer.observe(targetNode, observerOptions);
     }
     }
 
 
-    refresh() {
-        walkSkippingNestedComponents(this.$el, el => {
-            this.resolveBoundAttributes(el)
-        })
-    }
-
     generateExpressionForXModelListener(el, modifiers, dataKey) {
     generateExpressionForXModelListener(el, modifiers, dataKey) {
         var rightSideOfExpression = ''
         var rightSideOfExpression = ''
         if (el.type === 'checkbox') {
         if (el.type === 'checkbox') {
@@ -466,7 +493,7 @@ export default class Component {
 
 
                 // We can't just query the DOM because it's hard to filter out refs in
                 // We can't just query the DOM because it's hard to filter out refs in
                 // nested components.
                 // nested components.
-                walkSkippingNestedComponents(self.$el, el => {
+                self.walkAndSkipNestedComponents(self.$el, el => {
                     if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
                     if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
                         ref = el
                         ref = el
                     }
                     }

+ 6 - 0
src/index.js

@@ -55,8 +55,14 @@ const Alpine = {
             for (let i=0; i < mutations.length; i++){
             for (let i=0; i < mutations.length; i++){
                 if (mutations[i].addedNodes.length > 0) {
                 if (mutations[i].addedNodes.length > 0) {
                     mutations[i].addedNodes.forEach(node => {
                     mutations[i].addedNodes.forEach(node => {
+                        // Discard non-element nodes (like line-breaks)
                         if (node.nodeType !== 1) return
                         if (node.nodeType !== 1) return
 
 
+                        // Discard any changes happening within an existing component.
+                        // They will take care of themselves.
+                        if (node.closest('[x-data]')) return
+
+                        // This is a new top-level component.
                         if (node.matches('[x-data]')) callback(node)
                         if (node.matches('[x-data]')) callback(node)
                     })
                     })
                 }
                 }

+ 3 - 5
src/utils.js

@@ -44,15 +44,13 @@ export function keyToModifier(key) {
     }
     }
 }
 }
 
 
-export function walkSkippingNestedComponents(el, callback, isRoot = true) {
-    if (el.hasAttribute('x-data') && ! isRoot) return
-
-    callback(el)
+export function walk(el, callback) {
+    if (callback(el) === false) return
 
 
     let node = el.firstElementChild
     let node = el.firstElementChild
 
 
     while (node) {
     while (node) {
-        walkSkippingNestedComponents(node, callback, false)
+        walk(node, callback)
 
 
         node = node.nextElementSibling
         node = node.nextElementSibling
     }
     }

+ 59 - 9
test/constructor.spec.js

@@ -13,6 +13,52 @@ test('auto-detect new components and dont lose state of existing ones', async ()
         <div id="A" x-data="{ foo: '' }">
         <div id="A" x-data="{ foo: '' }">
             <input x-model="foo">
             <input x-model="foo">
             <span x-text="foo"></span>
             <span x-text="foo"></span>
+
+            <div id="B"></div>
+        </div>
+    `
+
+    Alpine.start()
+
+    fireEvent.input(document.querySelector('input'), { target: { value: 'bar' }})
+
+    await wait(() => { expect(document.querySelector('#A span').innerText).toEqual('bar') })
+
+    document.querySelector('#B').innerHTML = `
+        <div x-data="{foo: 'baz'}">
+            <input x-model="foo">
+            <span x-text="foo"></span>
+        </div>
+    `
+
+    runObservers[0]([
+        {
+            target: document.querySelector('#A'),
+            type: 'childList',
+            addedNodes: [ document.querySelector('#B div') ],
+        }
+    ])
+
+    await wait(() => {
+        expect(document.querySelector('#A span').innerText).toEqual('bar')
+        expect(document.querySelector('#B span').innerText).toEqual('baz')
+    })
+})
+
+test('auto-detect new components that are wrapped in non-new component tags', async () => {
+    var runObservers = []
+
+    global.MutationObserver = class {
+        constructor(callback) { runObservers.push(callback) }
+        observe() {}
+    }
+
+    document.body.innerHTML = `
+        <div id="A" x-data="{ foo: '' }">
+            <input x-model="foo">
+            <span x-text="foo"></span>
+
+            <div id="B"></div>
         </div>
         </div>
     `
     `
 
 
@@ -22,17 +68,21 @@ test('auto-detect new components and dont lose state of existing ones', async ()
 
 
     await wait(() => { expect(document.querySelector('#A span').innerText).toEqual('bar') })
     await wait(() => { expect(document.querySelector('#A span').innerText).toEqual('bar') })
 
 
-    const div = document.createElement('div')
-    div.setAttribute('id', 'B')
-    div.setAttribute('x-data', '{ foo: "baz" }')
-    div.innerHTML = `
-        <input x-model="foo">
-        <span x-text="foo"></span>
+    document.querySelector('#B').innerHTML = `
+        <section>
+            <div x-data="{foo: 'baz'}">
+                <input x-model="foo">
+                <span x-text="foo"></span>
+            </div>
+        </section>
     `
     `
-    document.body.appendChild(div)
 
 
-    runObservers[1]([
-        { addedNodes: [ div ] }
+    runObservers[0]([
+        {
+            target: document.querySelector('#A'),
+            type: 'childList',
+            addedNodes: [ document.querySelector('#B section') ],
+        }
     ])
     ])
 
 
     await wait(() => {
     await wait(() => {

部分文件因为文件数量过多而无法显示