Browse Source

Heave refactor (lose all the dependancy tracking stuff, don't think it really matters)

Caleb Porzio 5 years ago
parent
commit
f604a15f38
6 changed files with 114 additions and 130 deletions
  1. 0 0
      dist/alpine.js
  2. 0 0
      dist/alpine.js.map
  3. 24 0
      index.html
  4. 47 114
      src/component.js
  5. 10 16
      src/utils.js
  6. 33 0
      test/data.spec.js

File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js


File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js.map


+ 24 - 0
index.html

@@ -225,6 +225,30 @@
                     </td>
                 </tr>
 
+                <tr>
+                    <td>Init function callback access refs and mutate data</td>
+                    <td>
+                        <div x-data="initialData()" x-init="init()">
+                            <div :style="'width: '+width+'px; background: purple;'">hey</div>
+
+                            <button x-ref="inc">increase</button>
+                        </div>
+
+                        <script>
+                            function initialData() {
+                                return {
+                                    width: 20,
+                                    init() {
+                                        this.$refs.inc.addEventListener('click', () => {
+                                            this.width = this.width + 20
+                                        })
+                                    }
+                                }
+                            }
+                        </script>
+                    </td>
+                </tr>
+
                 <tr>
                     <td>Cloak</td>
                     <td>

+ 47 - 114
src/component.js

@@ -17,24 +17,23 @@ export default class Component {
 
         // Walk through the raw data and set the "this" context of any functions
         // to the observable, so data manipulations are reactive.
-        Object.keys(unobservedData).forEach(key => {
-            if (typeof unobservedData[key] === 'function') {
-                unobservedData[key] = unobservedData[key].bind(this.$data)
-            }
-        })
+        // Object.keys(unobservedData).forEach(key => {
+        //     if (typeof unobservedData[key] === 'function') {
+        //         unobservedData[key] = unobservedData[key].bind(this.$data)
+        //     }
+        // })
 
         // After making user-supplied data methods reactive, we can now add
         // our magic properties to the original data for access.
         unobservedData.$el = this.$el
         unobservedData.$refs = this.getRefsProxy()
+
+        // For $nextTick()
+        this.nextTickStack = []
         unobservedData.$nextTick = (callback) => {
-            this.delayRunByATick(callback)
+            this.nextTickStack.push(callback)
         }
 
-        // For $nextTick().
-        this.tickStack = []
-        this.collectingTickCallbacks = false
-
         var initReturnedCallback
         if (initExpression) {
             // We want to allow data manipulation, but not trigger DOM updates just yet.
@@ -73,70 +72,46 @@ export default class Component {
     }
 
     wrapDataInObservable(data) {
-        this.concernedData = []
-
         var self = this
 
-        const proxyHandler = keyPrefix => ({
+        const proxyHandler = {
             set(obj, property, value) {
-                const propertyName = keyPrefix ? `${keyPrefix}.${property}` : property
-
                 const setWasSuccessful = Reflect.set(obj, property, value)
 
                 // Don't react to data changes for cases like the `x-created` hook.
                 if (self.pauseReactivity) return
 
-                if (self.concernedData.indexOf(propertyName) === -1) {
-                    self.concernedData.push(propertyName)
-                }
+                debounce(() => {
+                    self.refresh()
 
-                self.refresh()
+                    // Walk through the $nextTick stack and clear it as we go.
+                    while (self.nextTickStack.length > 0) {
+                        self.nextTickStack.shift()()
+                    }
+                }, 0)()
 
                 return setWasSuccessful
             },
             get(target, key) {
-                // This is because there is no way to do something like `typeof foo === 'Proxy'`.
-                if (key === 'isProxy') return true
-
                 // If the property we are trying to get is a proxy, just return it.
                 // Like in the case of $refs
-                if (target[key] && target[key].isProxy) return target[key]
+                if (target[key] && target[key].isRefsProxy) return target[key]
 
                 // If property is a DOM node, just return it. (like in the case of this.$el)
                 if (target[key] && target[key] instanceof Node) return target[key]
 
                 // If accessing a nested property, retur this proxy recursively.
+                // This enables reactivity on setting nested data.
                 if (typeof target[key] === 'object' && target[key] !== null) {
-                    const propertyName = keyPrefix ? `${keyPrefix}.${key}` : key
-
-                    return new Proxy(target[key], proxyHandler(propertyName))
+                    return new Proxy(target[key], proxyHandler)
                 }
 
                 // If none of the above, just return the flippin' value. Gawsh.
                 return target[key]
             }
-        })
-
-        return new Proxy(data, proxyHandler())
-    }
-
-    delayRunByATick(callback) {
-        if (this.collectingTickCallbacks) {
-            this.tickStack.push(callback)
-        } else {
-            callback()
         }
-    }
 
-    startTick() {
-        this.collectingTickCallbacks = true
-    }
-
-    clearAndEndTick() {
-        this.tickStack.forEach(callable => callable())
-        this.tickStack = []
-
-        this.collectingTickCallbacks = false
+        return new Proxy(data, proxyHandler)
     }
 
     initializeElements() {
@@ -146,6 +121,11 @@ export default class Component {
     }
 
     initializeElement(el) {
+        this.registerListeners(el)
+        this.resolveBoundAttributes(el, true)
+    }
+
+    registerListeners(el) {
         getXAttrs(el).forEach(({ type, value, modifiers, expression }) => {
             switch (type) {
                 case 'on':
@@ -164,35 +144,45 @@ export default class Component {
                     const listenerExpression = this.generateExpressionForXModelListener(el, modifiers, expression)
 
                     this.registerListener(el, event, modifiers, listenerExpression)
+                    break;
+                default:
+                    break;
+            }
+        })
+    }
 
+    resolveBoundAttributes(el, initialUpdate = false) {
+        getXAttrs(el).forEach(({ type, value, modifiers, expression }) => {
+            switch (type) {
+                case 'model':
                     var attrName = 'value'
-                    var { output } = this.evaluateReturnExpression(expression)
+                    var output = this.evaluateReturnExpression(expression)
                     this.updateAttributeValue(el, attrName, output)
                     break;
 
                 case 'bind':
                     var attrName = value
-                    var { output } = this.evaluateReturnExpression(expression)
+                    var output = this.evaluateReturnExpression(expression)
                     this.updateAttributeValue(el, attrName, output)
                     break;
 
                 case 'text':
-                    var { output } = this.evaluateReturnExpression(expression)
+                    var output = this.evaluateReturnExpression(expression)
                     this.updateTextValue(el, output)
                     break;
 
                 case 'html':
-                    var { output } = this.evaluateReturnExpression(expression)
+                    var output = this.evaluateReturnExpression(expression)
                     this.updateHtmlValue(el, output)
                     break;
 
                 case 'show':
-                    var { output } = this.evaluateReturnExpression(expression)
-                    this.updateVisibility(el, output, true)
+                    var output = this.evaluateReturnExpression(expression)
+                    this.updateVisibility(el, output, initialUpdate)
                     break;
 
                 case 'if':
-                    var { output } = this.evaluateReturnExpression(expression)
+                    var output = this.evaluateReturnExpression(expression)
                     this.updatePresence(el, output)
                     break;
 
@@ -248,36 +238,8 @@ export default class Component {
     }
 
     refresh() {
-        var self = this
-
-        const actionByDirectiveType = {
-            'model': ({el, output}) => { self.updateAttributeValue(el, 'value', output) },
-            'bind': ({el, attrName, output}) => { self.updateAttributeValue(el, attrName, output) },
-            'text': ({el, output}) => { self.updateTextValue(el, output) },
-            'html': ({el, output}) => { self.updateHtmlValue(el, output) },
-            'show': ({el, output}) => { self.updateVisibility(el, output) },
-            'if': ({el, output}) => { self.updatePresence(el, output) },
-        }
-
-        const walkThenClearDependancyTracker = (rootEl, callback) => {
-            walkSkippingNestedComponents(rootEl, callback)
-
-            self.concernedData = []
-            self.clearAndEndTick()
-        }
-
-        this.startTick()
-
-        debounce(walkThenClearDependancyTracker, 5)(this.$el, function (el) {
-            getXAttrs(el).forEach(({ type, value, expression }) => {
-                if (! actionByDirectiveType[type]) return
-
-                var { output, deps } = self.evaluateReturnExpression(expression)
-
-                if (self.concernedData.filter(i => deps.includes(i)).length > 0) {
-                    (actionByDirectiveType[type])({ el, attrName: value, output })
-                }
-            })
+        walkSkippingNestedComponents(this.$el, el => {
+            this.resolveBoundAttributes(el)
         })
     }
 
@@ -361,36 +323,7 @@ export default class Component {
     }
 
     evaluateReturnExpression(expression) {
-        var affectedDataKeys = []
-
-        const proxyHandler = prefix => ({
-            get(object, prop) {
-                // Sometimes non-proxyable values are accessed. These are of type "symbol".
-                // We can ignore them.
-                if (typeof prop === 'symbol') return
-
-                const propertyName = prefix ? `${prefix}.${prop}` : prop
-
-                // If we are accessing an object prop, we'll make this proxy recursive to build
-                // a nested dependancy key.
-                if (typeof object[prop] === 'object' && object[prop] !== null && ! Array.isArray(object[prop])) {
-                    return new Proxy(object[prop], proxyHandler(propertyName))
-                }
-
-                affectedDataKeys.push(propertyName)
-
-                return object[prop]
-            }
-        })
-
-        const proxiedData = new Proxy(this.$data, proxyHandler())
-
-        const result = saferEval(expression, proxiedData)
-
-        return {
-            output: result,
-            deps: affectedDataKeys
-        }
+        return saferEval(expression, this.$data)
     }
 
     evaluateCommandExpression(expression, extraData) {
@@ -510,7 +443,7 @@ export default class Component {
         // For this reason, I'm using an "on-demand" proxy to fake a "$refs" object.
         return new Proxy({}, {
             get(object, property) {
-                if (property === 'isProxy') return true
+                if (property === 'isRefsProxy') return true
 
                 var ref
 

+ 10 - 16
src/utils.js

@@ -24,7 +24,7 @@ export function keyToModifier(key) {
     switch (key) {
         case ' ':
         case 'Spacebar':
-            return 'space'            
+            return 'space'
         default:
             return kebabCase(key)
     }
@@ -44,23 +44,17 @@ export function walkSkippingNestedComponents(el, callback, isRoot = true) {
     }
 }
 
-export function debounce(func, wait, immediate) {
-    var timeout;
+export function debounce(func, wait) {
+    var timeout
     return function () {
-        var context = this, args = arguments;
+        var context = this, args = arguments
         var later = function () {
-            timeout = null;
-            if (!immediate) func.apply(context, args);
-        };
-        var callNow = immediate && !timeout;
-        clearTimeout(timeout);
-        timeout = setTimeout(later, wait);
-        if (callNow) func.apply(context, args);
-    };
-};
-
-export function onlyUnique(value, index, self) {
-    return self.indexOf(value) === index;
+            timeout = null
+            func.apply(context, args)
+        }
+        clearTimeout(timeout)
+        timeout = setTimeout(later, wait)
+    }
 }
 
 export function saferEval(expression, dataContext, additionalHelperVariables = {}) {

+ 33 - 0
test/data.spec.js

@@ -30,3 +30,36 @@ test('x-data attribute value is optional', async () => {
 
     expect(document.querySelector('span').innerText).toEqual('foo')
 })
+
+test('x-data can use attributes from a reusable function', async () => {
+    document.body.innerHTML = `
+        <div x-data="test()">
+            <span x-text="foo"></span>
+        </div>
+    `
+        test = function() {
+            return {
+                foo: 'bar',
+            }
+        }
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('bar')
+})
+
+test('functions in x-data are reactive', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ foo: 'bar', getFoo() {return this.foo}}">
+            <span x-text="getFoo()"></span>
+            <button x-on:click="foo = 'baz'"></button>
+        </div>
+    `
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('bar')
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').innerText).toEqual('baz') })
+})

Some files were not shown because too many files changed in this diff