Explorar o código

Merge pull request #1 from alpinejs/master

Update
Ryan Chandler %!s(int64=5) %!d(string=hai) anos
pai
achega
390cd71165
Modificáronse 12 ficheiros con 211 adicións e 47 borrados
  1. 7 2
      README.md
  2. 59 11
      README.ru.md
  3. 28 11
      dist/alpine.js
  4. 7 7
      package-lock.json
  5. 1 1
      package.json
  6. 15 4
      src/component.js
  7. 9 6
      src/directives/on.js
  8. 7 1
      src/index.js
  9. 2 1
      src/polyfills.js
  10. 2 2
      test/bind.spec.js
  11. 17 0
      test/custom-magic-properties.spec.js
  12. 57 1
      test/on.spec.js

+ 7 - 2
README.md

@@ -97,7 +97,7 @@ You can even use it for non-trivial things:
 
 
 ## Learn
 ## Learn
 
 
-There are 13 directives available to you:
+There are 14 directives available to you:
 
 
 | Directive | Description |
 | Directive | Description |
 | --- | --- |
 | --- | --- |
@@ -355,6 +355,11 @@ Adding `.window` to an event listener will install the listener on the global wi
 
 
 Adding the `.once` modifier to an event listener will ensure that the listener will only be handled once. This is useful for things you only want to do once, like fetching HTML partials and such.
 Adding the `.once` modifier to an event listener will ensure that the listener will only be handled once. This is useful for things you only want to do once, like fetching HTML partials and such.
 
 
+**`.passive` modifier**
+**Example:** `<button x-on:mousedown.passive="interactive = true"></button>`
+
+Adding the `.passive` modifier to an event listener will make the listener a passive one, which means `preventDefault()` will not work on any events being processed, this can help, for example with scroll performance on touch devices.
+
 **`.debounce` modifier**
 **`.debounce` modifier**
 **Example:** `<input x-on:input.debounce="fetchSomething()">`
 **Example:** `<input x-on:input.debounce="fetchSomething()">`
 
 
@@ -529,7 +534,7 @@ These behave exactly like VueJs's transition directives, except they have differ
 ### `x-spread`
 ### `x-spread`
 **Example:**
 **Example:**
 ```html
 ```html
-<div x-data="dropdown">
+<div x-data="dropdown()">
     <button x-spread="trigger">Open Dropdown</button>
     <button x-spread="trigger">Open Dropdown</button>
 
 
     <span x-spread="dialogue">Dropdown Contents</span>
     <span x-spread="dialogue">Dropdown Contents</span>

+ 59 - 11
README.ru.md

@@ -93,7 +93,7 @@ Alpine.js можно использовать и для более серьез
 
 
 ## Изучение
 ## Изучение
 
 
-Всего в Alpine 13 директив:
+Всего в Alpine 14 директив:
 
 
 | Директива | Описание |
 | Директива | Описание |
 | --- | --- |
 | --- | --- |
@@ -109,12 +109,12 @@ Alpine.js можно использовать и для более серьез
 | [`x-if`](#x-if) | При невыполнении переданного условия полностью удаляет элемент из DOM. Должна использоваться в теге `<template>`. |
 | [`x-if`](#x-if) | При невыполнении переданного условия полностью удаляет элемент из DOM. Должна использоваться в теге `<template>`. |
 | [`x-for`](#x-for) | Создает новые DOM узлы для каждого элемента в массиве. Должна использоваться в теге `<template>`. |
 | [`x-for`](#x-for) | Создает новые DOM узлы для каждого элемента в массиве. Должна использоваться в теге `<template>`. |
 | [`x-transition`](#x-transition) | Директивы для добавления классов различным стадиям перехода (transition) элемента |
 | [`x-transition`](#x-transition) | Директивы для добавления классов различным стадиям перехода (transition) элемента |
-| [`x-spread`](#x-spread) | Позволяет привязать объект директивы Alpine к элементу для более удобного повторного использования |
+| [`x-spread`](#x-spread) | Позволяет вам привязывать объект с директивами Alpine к элементам, улучшая переиспользуемость. |
 | [`x-cloak`](#x-cloak) | Удаляется при инициализации Alpine. Полезна для скрытия DOM до инициализации. |
 | [`x-cloak`](#x-cloak) | Удаляется при инициализации Alpine. Полезна для скрытия DOM до инициализации. |
 
 
-И 6 волшебных свойств (magic properties):
+И 6 магических свойств (magic properties):
 
 
-| Волшебное свойство | Описание |
+| Магическое свойство | Описание |
 | --- | --- |
 | --- | --- |
 | [`$el`](#el) |  Получить DOM-узел корневого компонента. |
 | [`$el`](#el) |  Получить DOM-узел корневого компонента. |
 | [`$refs`](#refs) | Получить DOM-элементы компонента, отмеченные `x-ref`. |
 | [`$refs`](#refs) | Получить DOM-элементы компонента, отмеченные `x-ref`. |
@@ -301,6 +301,12 @@ Alpine.js можно использовать и для более серьез
 
 
 Если в этом выражении меняются какие-либо данные, другие элементы, "привязанные" к этим данным, будут обновлены.
 Если в этом выражении меняются какие-либо данные, другие элементы, "привязанные" к этим данным, будут обновлены.
 
 
+> Замечание: Вы также можете задать имя JS-функции.
+
+**Пример:** `<button x-on:click="myFunction"></button>`
+
+Это равноценно: `<button x-on:click="myFunction($event)"></button>`
+
 **Модификатор `keydown`**
 **Модификатор `keydown`**
 
 
 **Пример:** `<input type="text" x-on:keydown.escape="open = false">`
 **Пример:** `<input type="text" x-on:keydown.escape="open = false">`
@@ -525,12 +531,12 @@ Alpine предлагает 6 разных transition-директив для д
 ---
 ---
 
 
 ### `x-spread`
 ### `x-spread`
-**Example:**
+**Пример:**
 ```html
 ```html
 <div x-data="dropdown">
 <div x-data="dropdown">
-    <button x-spread="trigger">Open Dropdown</button>
+    <button x-spread="trigger">Открыть дропдаун</button>
 
 
-    <span x-spread="dialogue">Dropdown Contents</span>
+    <span x-spread="dialogue">Содержимое дропдауна</span>
 </div>
 </div>
 
 
 <script>
 <script>
@@ -555,11 +561,11 @@ Alpine предлагает 6 разных transition-директив для д
 </script>
 </script>
 ```
 ```
 
 
-`x-spread` позволяет извлекать элементы привязок в объекты многоразового использования.
+`x-spread` позволяем вам вынести привязки Alpine из элементов в переиспользуемый объект.
 
 
-Ключами объекта являются директивы (любые, включая модификаторы), а значения — колбэками, которые будет оценивать Alpine.
+Ключи объекта – это директивы (любые, в том числе и с модификаторами), а значения – колбэки, с которыми будет работать Alpine.
 
 
-> Примечание: Единственная аномалия с x-spread — при использование с `x-for`. В это случае вы должны вернуть строку нормального выражения из колбэка. Например: `['x-for']() { return 'item in items' }`.
+> Замечание: Единственная особенность при работе с x-spread – это то, как обрабатывается `x-for`. Когда директива, используемая в x-spread – это `x-for`, в колбэке необходимо возвращать выражение в виде строки. К примеру: `['x-for']() { return 'item in items' }`.
 
 
 ---
 ---
 
 
@@ -574,7 +580,9 @@ Alpine предлагает 6 разных transition-директив для д
 </style>
 </style>
 ```
 ```
 
 
-### Magic Properties
+### Магические свойства
+
+> Не считая `$el`, магические свойства **не доступны внутри `x-data`**, так как компонент еще не инициализирован.
 
 
 ---
 ---
 
 
@@ -588,6 +596,12 @@ Alpine предлагает 6 разных transition-директив для д
 
 
 `$el` – магическое свойство, которое используется для получения корневого компонента DOM-узла.
 `$el` – магическое свойство, которое используется для получения корневого компонента DOM-узла.
 
 
+> Замечание: Свойство $event доступно только в DOM-выражениях.
+
+Если вам нужен доступ к $event внутри JS-функции, вы можете передать его напрямую:
+
+`<button x-on:click="myFunction($event)"></button>`
+
 ### `$refs`
 ### `$refs`
 **Пример:**
 **Пример:**
 ```html
 ```html
@@ -619,6 +633,34 @@ Alpine предлагает 6 разных transition-директив для д
 </div>
 </div>
 ```
 ```
 
 
+**Примечание по распространению событий (event propagation)**
+
+Когда вам нужно перехватить событие, вызванное из узла на том же уровне вложенности, вам нужно использовать модификатор [`.window`](https://github.com/alpinejs/alpine/blob/master/README.ru.md#x-on):
+
+**Неправильный пример:**
+
+```html
+<div x-data>
+    <span @custom-event="console.log($event.detail.foo)"></span>
+    <button @click="$dispatch('custom-event', { foo: 'bar' })">
+<div>
+```
+
+> Это не будет работать, потому что, когда вызывается `custom-event`, он сразу всплывает ([event bubbling](https://en.wikipedia.org/wiki/Event_bubbling)) к родителю `div`.
+
+**Диспатч для компонентов**
+
+Вы также можете использовать предыдущую технику для общения компонентов друг с другом:
+
+**Пример:**
+
+```html
+<div x-data @custom-event.window="console.log($event.detail)"></div>
+
+<button x-data @click="$dispatch('custom-event', 'Hello World!')">
+<!-- При нажатии в консоль выведется "Hello World!". -->
+```
+
 `$dispatch` – это сокращение для создания `CustomEvent` и его вызова (диспатча) с помощью `.dispatchEvent()`. Существует множество сценариев использования передачи данных между компонентами с помощью пользовательских событий. [Пройдите по этой ссылке](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events), чтобы узнать больше о системе, лежащей в основе `CustomEvent` в браузерах.
 `$dispatch` – это сокращение для создания `CustomEvent` и его вызова (диспатча) с помощью `.dispatchEvent()`. Существует множество сценариев использования передачи данных между компонентами с помощью пользовательских событий. [Пройдите по этой ссылке](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events), чтобы узнать больше о системе, лежащей в основе `CustomEvent` в браузерах.
 
 
 Любые данные, переданные как второй параметр в `$dispatch('some-event', { some: 'data' })`, становятся доступны через свойство "detail" события: `$event.detail.some`. Добавление событию пользовательских данных через свойство `.detail` – стандартная практика для `CustomEvent` в браузерах. [Подробнее здесь](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail).
 Любые данные, переданные как второй параметр в `$dispatch('some-event', { some: 'data' })`, становятся доступны через свойство "detail" события: `$event.detail.some`. Добавление событию пользовательских данных через свойство `.detail` – стандартная практика для `CustomEvent` в браузерах. [Подробнее здесь](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail).
@@ -634,6 +676,12 @@ Alpine предлагает 6 разных transition-директив для д
 </div>
 </div>
 ```
 ```
 
 
+> Замечание: Свойство $dispatch доступно только в DOM-выражениях.
+
+Если вам нужен доступ к $dispatch внутри JS-функции, вы можете передать его напрямую:
+
+`<button x-on:click="myFunction($dispatch)"></button>`
+
 ---
 ---
 
 
 ### `$nextTick`
 ### `$nextTick`

+ 28 - 11
dist/alpine.js

@@ -729,9 +729,13 @@
   }
   }
 
 
   function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
   function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
+    const options = {
+      passive: modifiers.includes('passive')
+    };
+
     if (modifiers.includes('away')) {
     if (modifiers.includes('away')) {
       let handler = e => {
       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.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
         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);
         runListenerHandler(component, expression, e, extraVars);
 
 
         if (modifiers.includes('once')) {
         if (modifiers.includes('once')) {
-          document.removeEventListener(event, handler);
+          document.removeEventListener(event, handler, options);
         }
         }
       }; // Listen for this event at the root level.
       }; // Listen for this event at the root level.
 
 
 
 
-      document.addEventListener(event, handler);
+      document.addEventListener(event, handler, options);
     } else {
     } else {
       let listenerTarget = modifiers.includes('window') ? window : modifiers.includes('document') ? document : el;
       let listenerTarget = modifiers.includes('window') ? window : modifiers.includes('document') ? document : el;
 
 
@@ -754,7 +758,7 @@
         // has been removed. It's now stale.
         // has been removed. It's now stale.
         if (listenerTarget === window || listenerTarget === document) {
         if (listenerTarget === window || listenerTarget === document) {
           if (!document.body.contains(el)) {
           if (!document.body.contains(el)) {
-            listenerTarget.removeEventListener(event, handler);
+            listenerTarget.removeEventListener(event, handler, options);
             return;
             return;
           }
           }
         }
         }
@@ -777,7 +781,7 @@
             e.preventDefault();
             e.preventDefault();
           } else {
           } else {
             if (modifiers.includes('once')) {
             if (modifiers.includes('once')) {
-              listenerTarget.removeEventListener(event, handler);
+              listenerTarget.removeEventListener(event, handler, options);
             }
             }
           }
           }
         }
         }
@@ -789,7 +793,7 @@
         handler = debounce(handler, wait);
         handler = debounce(handler, wait);
       }
       }
 
 
-      listenerTarget.addEventListener(event, handler);
+      listenerTarget.addEventListener(event, handler, options);
     }
     }
   }
   }
 
 
@@ -1306,12 +1310,12 @@
   }
   }
 
 
   class Component {
   class Component {
-    constructor(el, seedDataForCloning = null) {
+    constructor(el, componentForClone = null) {
       this.$el = el;
       this.$el = el;
       const dataAttr = this.$el.getAttribute('x-data');
       const dataAttr = this.$el.getAttribute('x-data');
       const dataExpression = dataAttr === '' ? '{}' : dataAttr;
       const dataExpression = dataAttr === '' ? '{}' : dataAttr;
       const initExpression = this.$el.getAttribute('x-init');
       const initExpression = this.$el.getAttribute('x-init');
-      this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, {
+      this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, {
         $el: this.$el
         $el: this.$el
       });
       });
       // Construct a Proxy-based observable. This will be used to handle reactivity.
       // Construct a Proxy-based observable. This will be used to handle reactivity.
@@ -1339,11 +1343,20 @@
         this.watchers[property].push(callback);
         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.showDirectiveStack = [];
       this.showDirectiveLastElement;
       this.showDirectiveLastElement;
       var initReturnedCallback; // If x-init is present AND we aren't cloning (skip x-init on clone)
       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 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.
         // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
         this.pauseReactivity = true;
         this.pauseReactivity = true;
@@ -1612,7 +1625,7 @@
           if (!(closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue;
           if (!(closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue;
 
 
           if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
           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
               $el: this.$el
             });
             });
             Object.keys(rawData).forEach(key => {
             Object.keys(rawData).forEach(key => {
@@ -1669,6 +1682,7 @@
   const Alpine = {
   const Alpine = {
     version: "2.4.1",
     version: "2.4.1",
     pauseMutationObserver: false,
     pauseMutationObserver: false,
+    magicProperties: {},
     start: async function start() {
     start: async function start() {
       if (!isTesting()) {
       if (!isTesting()) {
         await domReady();
         await domReady();
@@ -1742,8 +1756,11 @@
     },
     },
     clone: function clone(component, newEl) {
     clone: function clone(component, newEl) {
       if (!newEl.__x) {
       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;
     }
     }
   };
   };
 
 

+ 7 - 7
package-lock.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "alpinejs",
     "name": "alpinejs",
-    "version": "2.3.5",
+    "version": "2.4.1",
     "lockfileVersion": 1,
     "lockfileVersion": 1,
     "requires": true,
     "requires": true,
     "dependencies": {
     "dependencies": {
@@ -4580,12 +4580,6 @@
                 }
                 }
             }
             }
         },
         },
-        "custom-event-polyfill": {
-            "version": "1.0.7",
-            "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
-            "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==",
-            "dev": true
-        },
         "dashdash": {
         "dashdash": {
             "version": "1.14.1",
             "version": "1.14.1",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -4838,6 +4832,12 @@
             "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
             "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
             "dev": true
             "dev": true
         },
         },
+        "events-polyfill": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/events-polyfill/-/events-polyfill-2.1.2.tgz",
+            "integrity": "sha512-vx4kpGzymyD3CEjmg2wTQA6k5e0RhGTkX3ZwfC9m/Ol7+me2tbVuJ0GjSd8eIJxFioubicA0nUL0SIOAyfrgZA==",
+            "dev": true
+        },
         "exec-sh": {
         "exec-sh": {
             "version": "0.3.4",
             "version": "0.3.4",
             "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
             "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",

+ 1 - 1
package.json

@@ -27,9 +27,9 @@
         "classlist-polyfill": "^1.2.0",
         "classlist-polyfill": "^1.2.0",
         "concurrently": "^5.2.0",
         "concurrently": "^5.2.0",
         "core-js": "^3.6.5",
         "core-js": "^3.6.5",
-        "custom-event-polyfill": "^1.0.7",
         "element-closest": "^3.0.2",
         "element-closest": "^3.0.2",
         "element-remove": "^1.0.4",
         "element-remove": "^1.0.4",
+        "events-polyfill": "^2.1.2",
         "jest": "^25.5.4",
         "jest": "^25.5.4",
         "jsdom-simulant": "^1.1.2",
         "jsdom-simulant": "^1.1.2",
         "observable-membrane": "^0.26.1",
         "observable-membrane": "^0.26.1",

+ 15 - 4
src/component.js

@@ -8,16 +8,17 @@ import { handleIfDirective } from './directives/if'
 import { registerModelListener } from './directives/model'
 import { registerModelListener } from './directives/model'
 import { registerListener } from './directives/on'
 import { registerListener } from './directives/on'
 import { unwrap, wrap } from './observable'
 import { unwrap, wrap } from './observable'
+import Alpine from './index'
 
 
 export default class Component {
 export default class Component {
-    constructor(el, seedDataForCloning = null) {
+    constructor(el, componentForClone = null) {
         this.$el = el
         this.$el = el
 
 
         const dataAttr = this.$el.getAttribute('x-data')
         const dataAttr = this.$el.getAttribute('x-data')
         const dataExpression = dataAttr === '' ? '{}' : dataAttr
         const dataExpression = dataAttr === '' ? '{}' : dataAttr
         const initExpression = this.$el.getAttribute('x-init')
         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 */
         /* IE11-ONLY:START */
             // For IE11, add our magic properties to the original data for access.
             // 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.$refs = null
             this.unobservedData.$nextTick = null
             this.unobservedData.$nextTick = null
             this.unobservedData.$watch = null
             this.unobservedData.$watch = null
+            Object.keys(Alpine.magicProperties).forEach(name => {
+                this.unobservedData[`$${name}`] = null
+            })
         /* IE11-ONLY:END */
         /* IE11-ONLY:END */
 
 
         // Construct a Proxy-based observable. This will be used to handle reactivity.
         // 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)
             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.showDirectiveStack = []
         this.showDirectiveLastElement
         this.showDirectiveLastElement
 
 
         var initReturnedCallback
         var initReturnedCallback
         // If x-init is present AND we aren't cloning (skip x-init on clone)
         // 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 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.
             // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
             this.pauseReactivity = true
             this.pauseReactivity = true
@@ -343,7 +354,7 @@ export default class Component {
                 if (! (closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue
                 if (! (closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue
 
 
                 if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
                 if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
-                    const rawData = saferEval(mutations[i].target.getAttribute('x-data'), { $el: this.$el })
+                    const rawData = saferEval(mutations[i].target.getAttribute('x-data') || '{}', { $el: this.$el })
 
 
                     Object.keys(rawData).forEach(key => {
                     Object.keys(rawData).forEach(key => {
                         if (this.$data[key] !== rawData[key]) {
                         if (this.$data[key] !== rawData[key]) {

+ 9 - 6
src/directives/on.js

@@ -1,9 +1,12 @@
 import { kebabCase, debounce, isNumeric } from '../utils'
 import { kebabCase, debounce, isNumeric } from '../utils'
 
 
 export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
 export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
+    const options = {
+        passive: modifiers.includes('passive'),
+    };
     if (modifiers.includes('away')) {
     if (modifiers.includes('away')) {
         let handler = e => {
         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
             if (el.contains(e.target)) return
 
 
             // Don't do anything if this element isn't currently visible.
             // Don't do anything if this element isn't currently visible.
@@ -14,12 +17,12 @@ export function registerListener(component, el, event, modifiers, expression, ex
             runListenerHandler(component, expression, e, extraVars)
             runListenerHandler(component, expression, e, extraVars)
 
 
             if (modifiers.includes('once')) {
             if (modifiers.includes('once')) {
-                document.removeEventListener(event, handler)
+                document.removeEventListener(event, handler, options)
             }
             }
         }
         }
 
 
         // Listen for this event at the root level.
         // Listen for this event at the root level.
-        document.addEventListener(event, handler)
+        document.addEventListener(event, handler, options)
     } else {
     } else {
         let listenerTarget = modifiers.includes('window')
         let listenerTarget = modifiers.includes('window')
             ? window : (modifiers.includes('document') ? document : el)
             ? window : (modifiers.includes('document') ? document : el)
@@ -29,7 +32,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
             // has been removed. It's now stale.
             // has been removed. It's now stale.
             if (listenerTarget === window || listenerTarget === document) {
             if (listenerTarget === window || listenerTarget === document) {
                 if (! document.body.contains(el)) {
                 if (! document.body.contains(el)) {
-                    listenerTarget.removeEventListener(event, handler)
+                    listenerTarget.removeEventListener(event, handler, options)
                     return
                     return
                 }
                 }
             }
             }
@@ -53,7 +56,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
                     e.preventDefault()
                     e.preventDefault()
                 } else {
                 } else {
                     if (modifiers.includes('once')) {
                     if (modifiers.includes('once')) {
-                        listenerTarget.removeEventListener(event, handler)
+                        listenerTarget.removeEventListener(event, handler, options)
                     }
                     }
                 }
                 }
             }
             }
@@ -65,7 +68,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
             handler = debounce(handler, wait, this)
             handler = debounce(handler, wait, this)
         }
         }
 
 
-        listenerTarget.addEventListener(event, handler)
+        listenerTarget.addEventListener(event, handler, options)
     }
     }
 }
 }
 
 

+ 7 - 1
src/index.js

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

+ 2 - 1
src/polyfills.js

@@ -5,6 +5,7 @@ import "element-closest/browser.js"
 import "element-remove"
 import "element-remove"
 import "classlist-polyfill"
 import "classlist-polyfill"
 import "@webcomponents/template"
 import "@webcomponents/template"
-import "custom-event-polyfill"
+import "events-polyfill/src/constructors/CustomEvent"
+import "events-polyfill/src/ListenerOptions"
 
 
 SVGElement.prototype.contains = SVGElement.prototype.contains || HTMLElement.prototype.contains
 SVGElement.prototype.contains = SVGElement.prototype.contains || HTMLElement.prototype.contains

+ 2 - 2
test/bind.spec.js

@@ -112,7 +112,7 @@ test('class attribute bindings are added by object syntax', async () => {
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
 })
 })
 
 
-test('multiple classes are added by object syntax', async () => {
+test('multiple classes are removed by object syntax', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
         <div x-data="{ isOn: false }">
         <div x-data="{ isOn: false }">
             <span class="foo bar" x-bind:class="{ 'foo bar': isOn }"></span>
             <span class="foo bar" x-bind:class="{ 'foo bar': isOn }"></span>
@@ -125,7 +125,7 @@ test('multiple classes are added by object syntax', async () => {
     expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
     expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
 })
 })
 
 
-test('multiple classes are removed by object syntax', async () => {
+test('multiple classes are added by object syntax', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
         <div x-data="{ isOn: true }">
         <div x-data="{ isOn: true }">
             <span x-bind:class="{ 'foo bar': isOn }"></span>
             <span x-bind:class="{ 'foo bar': isOn }"></span>

+ 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')
+})

+ 57 - 1
test/on.spec.js

@@ -42,6 +42,30 @@ test('nested data modified in event listener updates affected attribute bindings
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
 })
 })
 
 
+test('.passive modifier should disable e.preventDefault()', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ defaultPrevented: null }">
+            <button
+                x-on:mousedown.passive="
+                    $event.preventDefault();
+                    defaultPrevented = $event.defaultPrevented;
+                "
+            >
+                <span></span>
+            </button>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(null)
+
+    fireEvent.mouseDown(document.querySelector('button'))
+
+    await wait(() => {
+        expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(false)
+    })
+})
 
 
 test('.stop modifier', async () => {
 test('.stop modifier', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
@@ -336,6 +360,39 @@ test('click away', async () => {
     await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false) })
     await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false) })
 })
 })
 
 
+test('.passive + .away modifier still disables e.preventDefault()', async () => {
+    // Pretend like all the elements are visible
+    Object.defineProperties(window.HTMLElement.prototype, {
+        offsetHeight: {
+            get: () => 1
+        },
+        offsetWidth: {
+            get: () => 1
+        }
+    });
+    document.body.innerHTML = `
+        <div x-data="{ defaultPrevented: null }">
+            <button
+                x-on:mousedown.away.passive="
+                    $event.preventDefault();
+                    defaultPrevented = $event.defaultPrevented;
+                "
+            ></button>
+            <span></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(null)
+
+    fireEvent.mouseDown(document.querySelector('span'))
+
+    await wait(() => {
+        expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(false)
+    })
+})
+
 test('supports short syntax', async () => {
 test('supports short syntax', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">
         <div x-data="{ foo: 'bar' }">
@@ -353,7 +410,6 @@ test('supports short syntax', async () => {
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
 })
 })
 
 
-
 test('event with colon', async () => {
 test('event with colon', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">
         <div x-data="{ foo: 'bar' }">