浏览代码

Add the ability to register custom magic properties

Caleb Porzio 5 年之前
父节点
当前提交
d37e32ea77
共有 4 个文件被更改,包括 66 次插入15 次删除
  1. 28 11
      dist/alpine.js
  2. 14 3
      src/component.js
  3. 7 1
      src/index.js
  4. 17 0
      test/custom-magic-properties.spec.js

+ 28 - 11
dist/alpine.js

@@ -729,9 +729,13 @@
   }
 
   function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
+    const options = {
+      passive: modifiers.includes('passive')
+    };
+
     if (modifiers.includes('away')) {
       let handler = e => {
-        // Don't do anything if the click came form the element or within it.
+        // Don't do anything if the click came from the element or within it.
         if (el.contains(e.target)) return; // Don't do anything if this element isn't currently visible.
 
         if (el.offsetWidth < 1 && el.offsetHeight < 1) return; // Now that we are sure the element is visible, AND the click
@@ -740,12 +744,12 @@
         runListenerHandler(component, expression, e, extraVars);
 
         if (modifiers.includes('once')) {
-          document.removeEventListener(event, handler);
+          document.removeEventListener(event, handler, options);
         }
       }; // Listen for this event at the root level.
 
 
-      document.addEventListener(event, handler);
+      document.addEventListener(event, handler, options);
     } else {
       let listenerTarget = modifiers.includes('window') ? window : modifiers.includes('document') ? document : el;
 
@@ -754,7 +758,7 @@
         // has been removed. It's now stale.
         if (listenerTarget === window || listenerTarget === document) {
           if (!document.body.contains(el)) {
-            listenerTarget.removeEventListener(event, handler);
+            listenerTarget.removeEventListener(event, handler, options);
             return;
           }
         }
@@ -777,7 +781,7 @@
             e.preventDefault();
           } else {
             if (modifiers.includes('once')) {
-              listenerTarget.removeEventListener(event, handler);
+              listenerTarget.removeEventListener(event, handler, options);
             }
           }
         }
@@ -789,7 +793,7 @@
         handler = debounce(handler, wait);
       }
 
-      listenerTarget.addEventListener(event, handler);
+      listenerTarget.addEventListener(event, handler, options);
     }
   }
 
@@ -1306,12 +1310,12 @@
   }
 
   class Component {
-    constructor(el, seedDataForCloning = null) {
+    constructor(el, componentForClone = null) {
       this.$el = el;
       const dataAttr = this.$el.getAttribute('x-data');
       const dataExpression = dataAttr === '' ? '{}' : dataAttr;
       const initExpression = this.$el.getAttribute('x-init');
-      this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, {
+      this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, {
         $el: this.$el
       });
       // Construct a Proxy-based observable. This will be used to handle reactivity.
@@ -1339,11 +1343,20 @@
         this.watchers[property].push(callback);
       };
 
+      let canonicalComponentElementReference = componentForClone ? componentForClone.$el : this.$el; // Register custom magic properties.
+
+      Object.entries(Alpine.magicProperties).forEach(([name, callback]) => {
+        Object.defineProperty(this.unobservedData, `$${name}`, {
+          get: function get() {
+            return callback(canonicalComponentElementReference);
+          }
+        });
+      });
       this.showDirectiveStack = [];
       this.showDirectiveLastElement;
       var initReturnedCallback; // If x-init is present AND we aren't cloning (skip x-init on clone)
 
-      if (initExpression && !seedDataForCloning) {
+      if (initExpression && !componentForClone) {
         // We want to allow data manipulation, but not trigger DOM updates just yet.
         // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
         this.pauseReactivity = true;
@@ -1612,7 +1625,7 @@
           if (!(closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue;
 
           if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
-            const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {
+            const rawData = saferEval(mutations[i].target.getAttribute('x-data') || '{}', {
               $el: this.$el
             });
             Object.keys(rawData).forEach(key => {
@@ -1669,6 +1682,7 @@
   const Alpine = {
     version: "2.4.1",
     pauseMutationObserver: false,
+    magicProperties: {},
     start: async function start() {
       if (!isTesting()) {
         await domReady();
@@ -1742,8 +1756,11 @@
     },
     clone: function clone(component, newEl) {
       if (!newEl.__x) {
-        newEl.__x = new Component(newEl, component.getUnobservedData());
+        newEl.__x = new Component(newEl, component);
       }
+    },
+    addMagicProperty: function addMagicProperty(name, callback) {
+      this.magicProperties[name] = callback;
     }
   };
 

+ 14 - 3
src/component.js

@@ -8,16 +8,17 @@ import { handleIfDirective } from './directives/if'
 import { registerModelListener } from './directives/model'
 import { registerListener } from './directives/on'
 import { unwrap, wrap } from './observable'
+import Alpine from './index'
 
 export default class Component {
-    constructor(el, seedDataForCloning = null) {
+    constructor(el, componentForClone = null) {
         this.$el = el
 
         const dataAttr = this.$el.getAttribute('x-data')
         const dataExpression = dataAttr === '' ? '{}' : dataAttr
         const initExpression = this.$el.getAttribute('x-init')
 
-        this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, { $el: this.$el })
+        this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, { $el: this.$el })
 
         /* IE11-ONLY:START */
             // For IE11, add our magic properties to the original data for access.
@@ -26,6 +27,9 @@ export default class Component {
             this.unobservedData.$refs = null
             this.unobservedData.$nextTick = null
             this.unobservedData.$watch = null
+            Object.keys(Alpine.magicProperties).forEach(name => {
+                this.unobservedData[`$${name}`] = null
+            })
         /* IE11-ONLY:END */
 
         // Construct a Proxy-based observable. This will be used to handle reactivity.
@@ -50,12 +54,19 @@ export default class Component {
             this.watchers[property].push(callback)
         }
 
+        let canonicalComponentElementReference = componentForClone ? componentForClone.$el : this.$el
+
+        // Register custom magic properties.
+        Object.entries(Alpine.magicProperties).forEach(([name, callback]) => {
+            Object.defineProperty(this.unobservedData, `$${name}`, { get: function () { return callback(canonicalComponentElementReference) } });
+        })
+
         this.showDirectiveStack = []
         this.showDirectiveLastElement
 
         var initReturnedCallback
         // If x-init is present AND we aren't cloning (skip x-init on clone)
-        if (initExpression && ! seedDataForCloning) {
+        if (initExpression && ! componentForClone) {
             // We want to allow data manipulation, but not trigger DOM updates just yet.
             // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
             this.pauseReactivity = true

+ 7 - 1
src/index.js

@@ -6,6 +6,8 @@ const Alpine = {
 
     pauseMutationObserver: false,
 
+    magicProperties: {},
+
     start: async function () {
         if (! isTesting()) {
             await domReady()
@@ -95,8 +97,12 @@ const Alpine = {
 
     clone: function (component, newEl) {
         if (! newEl.__x) {
-            newEl.__x = new Component(newEl, component.getUnobservedData())
+            newEl.__x = new Component(newEl, component)
         }
+    },
+
+    addMagicProperty: function (name, callback) {
+        this.magicProperties[name] = callback
     }
 }
 

+ 17 - 0
test/custom-magic-properties.spec.js

@@ -0,0 +1,17 @@
+import Alpine from 'alpinejs'
+
+test('can register custom magic properties', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <span x-text="$foo.bar"></span>
+        </div>
+    `
+
+    Alpine.addMagicProperty('foo', () => {
+        return { bar: 'baz' }
+    })
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('baz')
+})