Selaa lähdekoodia

Refactor interceptors to allow for deep reactivity

Caleb Porzio 4 vuotta sitten
vanhempi
commit
2633936ca1

+ 3 - 1
packages/alpinejs/src/alpine.js

@@ -1,4 +1,4 @@
-import { setReactivityEngine, reactive, effect, release, raw } from './reactivity'
+import { setReactivityEngine, disableEffectScheduling, reactive, effect, release, raw } from './reactivity'
 import { mapAttributes, directive, setPrefix as prefix } from './directives'
 import { setEvaluator, evaluate, evaluateLater } from './evaluator'
 import { start, addRootSelector, closestRoot } from './lifecycle'
@@ -17,12 +17,14 @@ let Alpine = {
     get effect() { return effect },
     get raw() { return raw },
     version: ALPINE_VERSION,
+    disableEffectScheduling,
     setReactivityEngine,
     addRootSelector,
     mapAttributes,
     evaluateLater,
     setEvaluator,
     closestRoot,
+    // Warning: interceptor is not public API and is subject to change without major release.
     interceptor,
     mutateDom,
     directive,

+ 2 - 2
packages/alpinejs/src/directives/x-data.js

@@ -25,12 +25,12 @@ directive('data', skipDuringClone((el, { expression }, { cleanup }) => {
         data = evaluate(el, expression)
     }
 
-    initInterceptors(data)
-
     injectMagics(data, el)
 
     let reactiveData = reactive(data)
 
+    initInterceptors(reactiveData)
+
     let undo = addScopeToNode(el, reactiveData)
 
     if (reactiveData['init']) reactiveData['init']()

+ 46 - 49
packages/alpinejs/src/interceptor.js

@@ -1,3 +1,6 @@
+// Warning: The concept of "interceptors" in Alpine is not public API and is subject to change
+// without tagging a major release.
+
 export function initInterceptors(data) {
     let isObject = val => typeof val === 'object' && !Array.isArray(val) && val !== null
 
@@ -5,14 +8,12 @@ export function initInterceptors(data) {
         Object.entries(obj).forEach(([key, value]) => {
             let path = basePath === '' ? key : `${basePath}.${key}`
 
-            if (typeof value === 'function' && value.interceptor) {
-                let result = value(key, path)
-
-                Object.defineProperty(obj, key, result[0])
-            }
-
-            if (isObject(value) && value !== obj) {
-                recurse(value, path)
+            if (typeof value === 'object' && value !== null && value._x_interceptor) {
+                obj[key] = value.initialize(data, path, key)
+            } else {
+                if (isObject(value) && value !== obj) {
+                    recurse(value, path)
+                }
             }
         })
     }
@@ -20,58 +21,54 @@ export function initInterceptors(data) {
     return recurse(data)
 }
 
-export function interceptor(callback, mutateFunc = () => {}) {
-    return initialValue => {
-        function func(key, path) {
-            let parentFunc = func.parent
-                ? func.parent
-                : (key, path) => ([{}, { initer() {}, setter() {} }])
-
-            let [parentNoop, { initer: parentIniter, setter: parentSetter, initialValue: parentInitialValue }] = parentFunc(key, path)
+export function interceptor(callback, mutateObj = () => {}) {
+    let obj = {
+        initialValue: undefined,
 
-            let store = parentInitialValue === undefined ? initialValue : parentInitialValue
+        _x_interceptor: true,
 
-            let { init: initer, set: setter } = callback(key, path)
+        initialize(data, path, key) {
+            return callback(this.initialValue, () => get(data, path), (value) => set(data, path, value), path, key)
+        }
+    }
 
-            let inited = false
+    mutateObj(obj)
 
-            let setValue = i => store = i
-            let reactiveSetValue = function (i) { this[key] = i }
+    return initialValue => {
+        if (typeof initialValue === 'object' && value !== null && initialValue._x_interceptor) {
+            // Support nesting interceptors.
+            let initialize = obj.initialize.bind(obj)
 
-            let setup = (context) => {
-                if (inited) return
+            obj.initialize = (data, path, key) => {
+                let innerValue = initialValue.initialize(data, path, key)
 
-                parentIniter.bind(context)(store, setValue, reactiveSetValue.bind(context))
-                initer.bind(context)(store, setValue, reactiveSetValue.bind(context))
+                obj.initialValue = innerValue
 
-                inited = true
+                return initialize(data, path, key)
             }
-
-            return [{
-                get() {
-                    setup(this)
-
-                    return store
-                },
-                set(value) {
-                    setup(this)
-
-                    parentSetter.bind(this)(value, setValue, reactiveSetValue.bind(this))
-                    setter.bind(this)(value, setValue, reactiveSetValue.bind(this))
-                },
-                enumerable: true,
-                configurable: true,
-            }, { initer, setter, initialValue }]
+        } else {
+            obj.initialValue = initialValue
         }
 
-        func.interceptor = true
-
-        mutateFunc(func)
+        return obj
+    }
+}
 
-        if (typeof initialValue === 'function' && initialValue.interceptor) {
-            func.parent = initialValue
-        }
+function get(obj, path) {
+    return path.split('.').reduce((carry, segment) => carry[segment], obj)
+}
 
-        return func
+function set(obj, path, value) {
+    if (typeof path === 'string') path = path.split('.')
+
+    if (path.length === 1) obj[path[0]] = value;
+       else if (path.length === 0) throw error;
+    else {
+       if (obj[path[0]])
+          return set(obj[path[0]], path.slice(1), value);
+       else {
+          obj[path[0]] = {};
+          return set(obj[path[0]], path.slice(1), value);
+       }
     }
 }

+ 16 - 1
packages/alpinejs/src/reactivity.js

@@ -3,10 +3,25 @@ import { scheduler } from './scheduler'
 
 let reactive, effect, release, raw
 
+let shouldSchedule = true
+export function disableEffectScheduling(callback) {
+    shouldSchedule = false
+
+    callback()
+
+    shouldSchedule = true
+}
+
 export function setReactivityEngine(engine) {
     reactive = engine.reactive
     release = engine.release
-    effect = (callback) => engine.effect(callback, { scheduler })
+    effect = (callback) => engine.effect(callback, { scheduler: task => {
+        if (shouldSchedule) {
+            scheduler(task)
+        } else {
+            task()
+        }
+    } })
     raw = engine.raw
 }
 

+ 33 - 31
packages/history/src/index.js

@@ -2,55 +2,57 @@ export default function history(Alpine) {
     Alpine.magic('queryString', (el, { interceptor }) =>  {
         let alias
 
-        return interceptor((key, path) => {
+        return interceptor((initialValue, getter, setter, path, key) => {
             let pause = false
             let queryKey = alias || path
 
-            return {
-                init(initialValue, set, reactiveSet, func) {
-                    let value = initialValue
-                    let url = new URL(window.location.href)
+            let value = initialValue
+            let url = new URL(window.location.href)
 
-                    if (url.searchParams.has(queryKey)) {
-                        set(url.searchParams.get(queryKey))
-                        value = url.searchParams.get(queryKey)
-                    }
+            if (url.searchParams.has(queryKey)) {
+                value = url.searchParams.get(queryKey)
+            }
 
-                    let object = { value }
+            setter(value)
 
-                    url.searchParams.set(queryKey, value)
+            let object = { value }
 
-                    replace(url.toString(), path, object)
+            url.searchParams.set(queryKey, value)
 
-                    window.addEventListener('popstate', (e) => {
-                        if (! e.state) return
-                        if (! e.state.alpine) return
+            replace(url.toString(), path, object)
 
-                        Object.entries(e.state.alpine).forEach(([newKey, { value }]) => {
-                            if (newKey !== key) return
+            window.addEventListener('popstate', (e) => {
+                if (! e.state) return
+                if (! e.state.alpine) return
 
-                            pause = true
+                Object.entries(e.state.alpine).forEach(([newKey, { value }]) => {
+                    if (newKey !== key) return
 
-                            reactiveSet(value)
+                    pause = true
 
-                            pause = false
-                        })
+                    Alpine.disableEffectScheduling(() => {
+                        setter(value)
                     })
-                },
-                set(value, set) {
-                    set(value)
 
-                    if (pause) return
+                    pause = false
+                })
+            })
 
-                    let object = { value }
+            Alpine.effect(() => {
+                let value = getter()
 
-                    let url = new URL(window.location.href)
+                if (pause) return
 
-                    url.searchParams.set(queryKey, value)
+                let object = { value }
 
-                    push(url.toString(), path, object)
-                },
-            }
+                let url = new URL(window.location.href)
+
+                url.searchParams.set(queryKey, value)
+
+                push(url.toString(), path, object)
+            })
+
+            return value
         }, func => {
             func.as = key => { alias = key; return func }
         })

+ 14 - 14
packages/persist/src/index.js

@@ -1,21 +1,21 @@
 
 export default function (Alpine) {
     Alpine.magic('persist', (el, { interceptor }) => {
-        return interceptor((key, path) => {
-            return {
-                init(initialValue, setter) {
-                    if (localStorage.getItem(path)) {
-                        setter(localStorage.getItem(path))
-                    } else {
-                        setter(initialValue)
-                    }
-                },
-                set(value, setter) {
-                    localStorage.setItem(path, value)
+        return interceptor((initialValue, getter, setter, path, key) => {
+            let initial = localStorage.getItem(path)
+                ? localStorage.getItem(path)
+                : initialValue
 
-                    setter(value)
-                },
-            }
+            setter(initialValue)
+
+            Alpine.effect(() => {
+                let value = getter()
+                localStorage.setItem(path, value)
+
+                setter(value)
+            })
+
+            return initial
         })
     })
 }

+ 4 - 18
tests/cypress/integration/custom-interceptors.spec.js

@@ -8,15 +8,8 @@ test('can register custom interceptors',
     `,
     `
         Alpine.magic('magic', () => {
-            return Alpine.interceptor((key, path) => {
-                return {
-                    init(initialValue, setter) {
-                        setter(key+path)
-                    },
-                    set(value, setter) {
-                        setter(value)
-                    },
-                }
+            return Alpine.interceptor((initialValue, getter, setter, path, key) => {
+                return key+path
             })
         })
     `],
@@ -31,15 +24,8 @@ test('interceptors are nesting aware',
     `,
     `
         Alpine.magic('magic', () => {
-            return Alpine.interceptor((key, path) => {
-                return {
-                    init(initialValue, setter) {
-                        setter(key+path)
-                    },
-                    set(value, setter) {
-                        setter(value)
-                    },
-                }
+            return Alpine.interceptor((initialValue, getter, setter, path, key) => {
+                return key+path
             })
         })
     `],