소스 검색

chore(update with master)

Ryan Chandler 5 년 전
부모
커밋
aa047588ba
13개의 변경된 파일691개의 추가작업 그리고 163개의 파일을 삭제
  1. 7 2
      README.md
  2. 59 11
      README.ru.md
  3. 479 116
      dist/alpine-ie11.js
  4. 28 11
      dist/alpine.js
  5. 7 7
      package-lock.json
  6. 1 1
      package.json
  7. 15 4
      src/component.js
  8. 10 6
      src/directives/on.js
  9. 7 1
      src/index.js
  10. 2 1
      src/polyfills.js
  11. 2 2
      test/bind.spec.js
  12. 17 0
      test/custom-magic-properties.spec.js
  13. 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 |
 | --- | --- |
 | --- | --- |
@@ -360,6 +360,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()">`
 
 
@@ -539,7 +544,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`

+ 479 - 116
dist/alpine-ie11.js

@@ -884,60 +884,396 @@
 
 
   })();
   })();
 
 
-  // Polyfill for creating CustomEvents on IE9/10/11
-
-  // code pulled from:
-  // https://github.com/d4tocchini/customevent-polyfill
-  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill
-
-  (function() {
-    if (typeof window === 'undefined') {
-      return;
-    }
-
-    try {
-      var ce = new window.CustomEvent('test', { cancelable: true });
-      ce.preventDefault();
-      if (ce.defaultPrevented !== true) {
-        // IE has problems with .preventDefault() on custom events
-        // http://stackoverflow.com/questions/23349191
-        throw new Error('Could not prevent default');
-      }
-    } catch (e) {
-      var CustomEvent = function(event, params) {
-        var evt, origPrevent;
-        params = params || {};
-        params.bubbles = !!params.bubbles;
-        params.cancelable = !!params.cancelable;
-
-        evt = document.createEvent('CustomEvent');
-        evt.initCustomEvent(
-          event,
-          params.bubbles,
-          params.cancelable,
-          params.detail
-        );
-        origPrevent = evt.preventDefault;
-        evt.preventDefault = function() {
-          origPrevent.call(this);
-          try {
-            Object.defineProperty(this, 'defaultPrevented', {
-              get: function() {
-                return true;
-              }
-            });
-          } catch (e) {
-            this.defaultPrevented = true;
-          }
-        };
-        return evt;
-      };
+  var ApplyThisPrototype = (function() {
+    return function ApplyThisPrototype(event, target) {
+      if ((typeof target === 'object') && (target !== null)) {
+        var proto = Object.getPrototypeOf(target);
+        var property;
+
+        for (property in proto) {
+          if (!(property in event)) {
+            var descriptor = Object.getOwnPropertyDescriptor(proto, property);
+            if (descriptor) {
+              Object.defineProperty(event, property, descriptor);
+            }
+          }
+        }
+
+        for (property in target) {
+          if (!(property in event)) {
+            event[property] = target[property];
+          }
+        }
+      }
+    }
+  })();
 
 
-      CustomEvent.prototype = window.Event.prototype;
-      window.CustomEvent = CustomEvent; // expose definition to window
-    }
+  (function(ApplyThisPrototype) {
+    /**
+     * Polyfill CustomEvent
+     */
+    try {
+      var event = new window.CustomEvent('event', { bubbles: true, cancelable: true });
+    } catch (error) {
+      var CustomEventOriginal = window.CustomEvent || window.Event;
+      var CustomEvent = function(eventName, params) {
+        params = params || {};
+        var event = document.createEvent('CustomEvent');
+        event.initCustomEvent(
+          eventName,
+          (params.bubbles === void 0) ? false : params.bubbles,
+          (params.cancelable === void 0) ? false : params.cancelable,
+          (params.detail === void 0) ? {} : params.detail
+        );
+        ApplyThisPrototype(event, this);
+        return event;
+      };
+      CustomEvent.prototype = CustomEventOriginal.prototype;
+      window.CustomEvent = CustomEvent;
+    }
+  })(ApplyThisPrototype);
+
+  var EventListenerInterceptor = (function() {
+
+    if(typeof EventTarget === 'undefined') {
+      window.EventTarget = Node;
+    }
+
+    /**
+     * Event listener interceptor
+     */
+
+    var EventListenerInterceptor = {
+      interceptors: [] // { target: EventTarget, interceptors: [{ add: Function, remove: Function }, ...] }
+    };
+
+
+    /**
+     * Returns if exists a previously registered listener from a target and the normalized arguments
+     * @param target
+     * @param normalizedArguments
+     * @return {*}
+     */
+    EventListenerInterceptor.getRegisteredEventListener = function(target, normalizedArguments) {
+      var key = normalizedArguments.type + '-' + (normalizedArguments.options.capture ? '1' : '0');
+      if(
+        (target.__eventListeners !== void 0) &&
+        (target.__eventListeners[key] !== void 0)
+      ) {
+        var map = target.__eventListeners[key];
+        for(var i = 0; i < map.length; i++) {
+          if(map[i].listener === normalizedArguments.listener) {
+            return map[i];
+          }
+        }
+      }
+      return null;
+    };
+
+    /**
+     * Registers a listener on a target with some options
+     * @param target
+     * @param normalizedArguments
+     */
+    EventListenerInterceptor.registerEventListener = function(target, normalizedArguments) {
+      var key = normalizedArguments.type + '-' + (normalizedArguments.options.capture ? '1' : '0');
+
+      if(target.__eventListeners === void 0) {
+        target.__eventListeners = {};
+      }
+
+      if(target.__eventListeners[key] === void 0) {
+        target.__eventListeners[key] = [];
+      }
+
+      target.__eventListeners[key].push(normalizedArguments);
+    };
+
+    /**
+     * Unregisters a listener on a target with some options
+     * @param target
+     * @param normalizedArguments
+     */
+    EventListenerInterceptor.unregisterEventListener = function(target, normalizedArguments) {
+      var key = normalizedArguments.type + '-' + (normalizedArguments.options.capture ? '1' : '0');
+      if(
+        (target.__eventListeners !==  void 0) &&
+        (target.__eventListeners[key] !== void 0)
+      ) {
+        var map = target.__eventListeners[key];
+        for(var i = 0; i < map.length; i++) {
+          if(map[i].listener === normalizedArguments.listener) {
+            map.splice(i, 1);
+          }
+        }
+
+        if(map.length === 0) {
+          delete target.__eventListeners[key];
+        }
+      }
+    };
+
+
+
+    EventListenerInterceptor.normalizeListenerCallback = function(listener) {
+      if((typeof listener === 'function') || (listener === null) || (listener === void 0)) {
+        return listener;
+      } else if((typeof listener === 'object') && (typeof listener.handleEvent === 'function')) {
+        return listener.handleEvent;
+      } else {
+        // to support Symbol
+        return function(event) {
+          listener(event);
+        };
+      }
+    };
+
+    EventListenerInterceptor.normalizeListenerOptions = function(options) {
+      switch(typeof options) {
+        case 'boolean':
+          options = { capture: options };
+          break;
+        case 'undefined':
+          options = { capture: false };
+          break;
+        case 'object':
+          if (options === null) {
+            options = { capture: false };
+          }
+          break;
+        default:
+          throw new Error('Unsupported options type for addEventListener');
+      }
+
+      options.once      = Boolean(options.once);
+      options.passive   = Boolean(options.passive);
+      options.capture   = Boolean(options.capture);
+
+      return options;
+    };
+
+    EventListenerInterceptor.normalizeListenerArguments = function(type, listener, options) {
+      return {
+        type: type,
+        listener: this.normalizeListenerCallback(listener),
+        options: this.normalizeListenerOptions(options)
+      };
+    };
+
+
+
+    EventListenerInterceptor.intercept = function(target, interceptors) {
+      // get an interceptor with this target or null
+      var interceptor = null;
+      for (var i = 0; i < this.interceptors.length; i++) {
+        if(this.interceptors[i].target === target) {
+          interceptor = this.interceptors[i];
+        }
+      }
+
+      // if no interceptor already set
+      if (interceptor === null) {
+        interceptor = { target: target, interceptors: [interceptors] };
+        this.interceptors.push(interceptor);
+
+        this.interceptAddEventListener(target, interceptor);
+        this.interceptRemoveEventListener(target, interceptor);
+      } else { // if an interceptor already set, simply add interceptors to the list
+        interceptor.interceptors.push(interceptors);
+      }
+
+      // var release = function() {
+      //   target.prototype.addEventListener = addEventListener;
+      //   target.prototype.removeEventListener = removeEventListener;
+      // };
+      // this.interceptors.push(release);
+      // return release;
+    };
+
+    EventListenerInterceptor.interceptAddEventListener = function(target, interceptor) {
+      var _this = this;
+
+      var addEventListener = target.prototype.addEventListener;
+      target.prototype.addEventListener = function(type, listener, options) {
+        var normalizedArguments = _this.normalizeListenerArguments(type, listener, options);
+        var registeredEventListener = _this.getRegisteredEventListener(this, normalizedArguments);
+
+        if (!registeredEventListener) {
+
+          normalizedArguments.polyfilled = {
+            type: normalizedArguments.type,
+            listener: normalizedArguments.listener,
+            options: {
+              capture: normalizedArguments.options.capture,
+              once: normalizedArguments.options.once,
+              passive: normalizedArguments.options.passive
+            }
+          };
+
+          for (var i = 0; i < interceptor.interceptors.length; i++) {
+            var interceptors = interceptor.interceptors[i];
+            if (typeof interceptors.add === 'function') {
+              interceptors.add(normalizedArguments);
+            }
+          }
+
+          // console.log('normalizedArguments', normalizedArguments.polyfilled);
+
+          _this.registerEventListener(this, normalizedArguments);
+
+          addEventListener.call(
+            this,
+            normalizedArguments.polyfilled.type,
+            normalizedArguments.polyfilled.listener,
+            normalizedArguments.polyfilled.options
+          );
+        }
+      };
+
+      return function() {
+        target.prototype.addEventListener = addEventListener;
+      };
+    };
+
+    EventListenerInterceptor.interceptRemoveEventListener = function(target, interceptor) {
+      var _this = this;
+
+      var removeEventListener = target.prototype.removeEventListener;
+      target.prototype.removeEventListener = function(type, listener, options) {
+        var normalizedArguments = _this.normalizeListenerArguments(type, listener, options);
+        var registeredEventListener = _this.getRegisteredEventListener(this, normalizedArguments);
+
+        if (registeredEventListener) {
+          _this.unregisterEventListener(this, normalizedArguments);
+          removeEventListener.call(
+            this,
+            registeredEventListener.polyfilled.type,
+            registeredEventListener.polyfilled.listener,
+            registeredEventListener.polyfilled.options
+          );
+        } else {
+          removeEventListener.call(this, type, listener, options);
+        }
+      };
+
+      return function() {
+        target.prototype.removeEventListener = removeEventListener;
+      };
+    };
+
+    EventListenerInterceptor.interceptAll = function(interceptors) {
+      this.intercept(EventTarget, interceptors);
+      if(!(window instanceof EventTarget)) {
+        this.intercept(Window, interceptors);
+      }
+    };
+
+    EventListenerInterceptor.releaseAll = function() {
+      for(var i = 0, l = this.interceptors.length; i < l; i++) {
+        this.interceptors();
+      }
+    };
+
+
+    EventListenerInterceptor.error = function(error) {
+      // throw error;
+      console.error(error);
+    };
+
+    return EventListenerInterceptor;
   })();
   })();
 
 
+  (function(EventListenerInterceptor) {
+    /**
+     * Event listener options support
+     */
+
+    EventListenerInterceptor.detectSupportedOptions = function() {
+      var _this = this;
+
+      this.supportedOptions = {
+        once: false,
+        passive: false,
+        capture: false,
+
+        all: false,
+        some: false
+      };
+
+      document.createDocumentFragment().addEventListener('test', function() {}, {
+        get once() {
+          _this.supportedOptions.once = true;
+          return false;
+        },
+        get passive() {
+          _this.supportedOptions.passive = true;
+          return false;
+        },
+        get capture() {
+          _this.supportedOptions.capture = true;
+          return false;
+        }
+      });
+
+      // useful shortcuts to detect if options are all/some supported
+      this.supportedOptions.all  = this.supportedOptions.once && this.supportedOptions.passive && this.supportedOptions.capture;
+      this.supportedOptions.some = this.supportedOptions.once || this.supportedOptions.passive || this.supportedOptions.capture;
+    };
+
+    EventListenerInterceptor.polyfillListenerOptions = function() {
+      this.detectSupportedOptions();
+      if (!this.supportedOptions.all) {
+        var _this = this;
+
+        this.interceptAll({
+          add: function(normalizedArguments) {
+            // console.log('intercepted', normalizedArguments);
+
+            var once = normalizedArguments.options.once && !_this.supportedOptions.once;
+            var passive = normalizedArguments.options.passive && !_this.supportedOptions.passive;
+
+            if (once || passive) {
+              var listener = normalizedArguments.polyfilled.listener;
+
+              normalizedArguments.polyfilled.listener = function(event) {
+                if(once) {
+                  this.removeEventListener(normalizedArguments.type, normalizedArguments.listener, normalizedArguments.options);
+                }
+
+                if(passive) {
+                  event.preventDefault = function() {
+                    throw new Error('Unable to preventDefault inside passive event listener invocation.');
+                  };
+                }
+
+                return listener.call(this, event);
+              };
+            }
+
+            if (!_this.supportedOptions.some) {
+              normalizedArguments.polyfilled.options = normalizedArguments.options.capture;
+            }
+          }
+        });
+      }
+    };
+
+
+    EventListenerInterceptor.polyfillListenerOptions();
+
+
+    // var onclick = function() {
+    //   console.log('click');
+    // };
+
+    // document.body.addEventListener('click', onclick, false);
+    // document.body.addEventListener('click', onclick, { once: true });
+    // document.body.addEventListener('click', onclick, { once: true });
+    // document.body.addEventListener('click', onclick, false);
+    // document.body.addEventListener('click', onclick, false);
+
+  })(EventListenerInterceptor);
+
   // For the IE11 build.
   // For the IE11 build.
   SVGElement.prototype.contains = SVGElement.prototype.contains || HTMLElement.prototype.contains;
   SVGElement.prototype.contains = SVGElement.prototype.contains || HTMLElement.prototype.contains;
 
 
@@ -3338,6 +3674,46 @@
     }
     }
   });
   });
 
 
+  var propertyIsEnumerable = objectPropertyIsEnumerable.f;
+
+  // `Object.{ entries, values }` methods implementation
+  var createMethod$4 = function (TO_ENTRIES) {
+    return function (it) {
+      var O = toIndexedObject(it);
+      var keys = objectKeys(O);
+      var length = keys.length;
+      var i = 0;
+      var result = [];
+      var key;
+      while (length > i) {
+        key = keys[i++];
+        if (!descriptors || propertyIsEnumerable.call(O, key)) {
+          result.push(TO_ENTRIES ? [key, O[key]] : O[key]);
+        }
+      }
+      return result;
+    };
+  };
+
+  var objectToArray = {
+    // `Object.entries` method
+    // https://tc39.github.io/ecma262/#sec-object.entries
+    entries: createMethod$4(true),
+    // `Object.values` method
+    // https://tc39.github.io/ecma262/#sec-object.values
+    values: createMethod$4(false)
+  };
+
+  var $entries = objectToArray.entries;
+
+  // `Object.entries` method
+  // https://tc39.github.io/ecma262/#sec-object.entries
+  _export({ target: 'Object', stat: true }, {
+    entries: function entries(O) {
+      return $entries(O);
+    }
+  });
+
   // `SameValue` abstract operation
   // `SameValue` abstract operation
   // https://tc39.github.io/ecma262/#sec-samevalue
   // https://tc39.github.io/ecma262/#sec-samevalue
   var sameValue = Object.is || function is(x, y) {
   var sameValue = Object.is || function is(x, y) {
@@ -4671,7 +5047,7 @@
   var rtrim = RegExp(whitespace + whitespace + '*$');
   var rtrim = RegExp(whitespace + whitespace + '*$');
 
 
   // `String.prototype.{ trim, trimStart, trimEnd, trimLeft, trimRight }` methods implementation
   // `String.prototype.{ trim, trimStart, trimEnd, trimLeft, trimRight }` methods implementation
-  var createMethod$4 = function (TYPE) {
+  var createMethod$5 = function (TYPE) {
     return function ($this) {
     return function ($this) {
       var string = String(requireObjectCoercible($this));
       var string = String(requireObjectCoercible($this));
       if (TYPE & 1) string = string.replace(ltrim, '');
       if (TYPE & 1) string = string.replace(ltrim, '');
@@ -4683,13 +5059,13 @@
   var stringTrim = {
   var stringTrim = {
     // `String.prototype.{ trimLeft, trimStart }` methods
     // `String.prototype.{ trimLeft, trimStart }` methods
     // https://tc39.github.io/ecma262/#sec-string.prototype.trimstart
     // https://tc39.github.io/ecma262/#sec-string.prototype.trimstart
-    start: createMethod$4(1),
+    start: createMethod$5(1),
     // `String.prototype.{ trimRight, trimEnd }` methods
     // `String.prototype.{ trimRight, trimEnd }` methods
     // https://tc39.github.io/ecma262/#sec-string.prototype.trimend
     // https://tc39.github.io/ecma262/#sec-string.prototype.trimend
-    end: createMethod$4(2),
+    end: createMethod$5(2),
     // `String.prototype.trim` method
     // `String.prototype.trim` method
     // https://tc39.github.io/ecma262/#sec-string.prototype.trim
     // https://tc39.github.io/ecma262/#sec-string.prototype.trim
-    trim: createMethod$4(3)
+    trim: createMethod$5(3)
   };
   };
 
 
   var getOwnPropertyNames = objectGetOwnPropertyNames.f;
   var getOwnPropertyNames = objectGetOwnPropertyNames.f;
@@ -4760,46 +5136,6 @@
     redefine(global_1, NUMBER, NumberWrapper);
     redefine(global_1, NUMBER, NumberWrapper);
   }
   }
 
 
-  var propertyIsEnumerable = objectPropertyIsEnumerable.f;
-
-  // `Object.{ entries, values }` methods implementation
-  var createMethod$5 = function (TO_ENTRIES) {
-    return function (it) {
-      var O = toIndexedObject(it);
-      var keys = objectKeys(O);
-      var length = keys.length;
-      var i = 0;
-      var result = [];
-      var key;
-      while (length > i) {
-        key = keys[i++];
-        if (!descriptors || propertyIsEnumerable.call(O, key)) {
-          result.push(TO_ENTRIES ? [key, O[key]] : O[key]);
-        }
-      }
-      return result;
-    };
-  };
-
-  var objectToArray = {
-    // `Object.entries` method
-    // https://tc39.github.io/ecma262/#sec-object.entries
-    entries: createMethod$5(true),
-    // `Object.values` method
-    // https://tc39.github.io/ecma262/#sec-object.values
-    values: createMethod$5(false)
-  };
-
-  var $entries = objectToArray.entries;
-
-  // `Object.entries` method
-  // https://tc39.github.io/ecma262/#sec-object.entries
-  _export({ target: 'Object', stat: true }, {
-    entries: function entries(O) {
-      return $entries(O);
-    }
-  });
-
   var $values = objectToArray.values;
   var $values = objectToArray.values;
 
 
   // `Object.values` method
   // `Object.values` method
@@ -6322,6 +6658,9 @@
     var _this = this;
     var _this = this;
 
 
     var extraVars = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};
     var extraVars = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};
+    var options = {
+      passive: modifiers.includes('passive')
+    };
 
 
     if (modifiers.includes('camel')) {
     if (modifiers.includes('camel')) {
       event = camelCase(event);
       event = camelCase(event);
@@ -6331,7 +6670,7 @@
       var _handler = function handler(e) {
       var _handler = function handler(e) {
         _newArrowCheck(this, _this);
         _newArrowCheck(this, _this);
 
 
-        // 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
@@ -6340,12 +6679,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);
         }
         }
       }.bind(this); // Listen for this event at the root level.
       }.bind(this); // Listen for this event at the root level.
 
 
 
 
-      document.addEventListener(event, _handler);
+      document.addEventListener(event, _handler, options);
     } else {
     } else {
       var listenerTarget = modifiers.includes('window') ? window : modifiers.includes('document') ? document : el;
       var listenerTarget = modifiers.includes('window') ? window : modifiers.includes('document') ? document : el;
 
 
@@ -6356,7 +6695,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, _handler2);
+            listenerTarget.removeEventListener(event, _handler2, options);
             return;
             return;
           }
           }
         }
         }
@@ -6379,7 +6718,7 @@
             e.preventDefault();
             e.preventDefault();
           } else {
           } else {
             if (modifiers.includes('once')) {
             if (modifiers.includes('once')) {
-              listenerTarget.removeEventListener(event, _handler2);
+              listenerTarget.removeEventListener(event, _handler2, options);
             }
             }
           }
           }
         }
         }
@@ -6391,7 +6730,7 @@
         _handler2 = debounce(_handler2, wait);
         _handler2 = debounce(_handler2, wait);
       }
       }
 
 
-      listenerTarget.addEventListener(event, _handler2);
+      listenerTarget.addEventListener(event, _handler2, options);
     }
     }
   }
   }
 
 
@@ -6647,7 +6986,7 @@
     function Component(el) {
     function Component(el) {
       var _this = this;
       var _this = this;
 
 
-      var seedDataForCloning = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+      var componentForClone = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
 
 
       _classCallCheck(this, Component);
       _classCallCheck(this, Component);
 
 
@@ -6655,7 +6994,7 @@
       var dataAttr = this.$el.getAttribute('x-data');
       var dataAttr = this.$el.getAttribute('x-data');
       var dataExpression = dataAttr === '' ? '{}' : dataAttr;
       var dataExpression = dataAttr === '' ? '{}' : dataAttr;
       var initExpression = this.$el.getAttribute('x-init');
       var 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
       });
       });
       /* IE11-ONLY:START */
       /* IE11-ONLY:START */
@@ -6666,6 +7005,11 @@
       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(function (name) {
+        _newArrowCheck(this, _this);
+
+        this.unobservedData["$".concat(name)] = null;
+      }.bind(this));
       /* 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.
 
 
@@ -6696,11 +7040,26 @@
         this.watchers[property].push(callback);
         this.watchers[property].push(callback);
       }.bind(this);
       }.bind(this);
 
 
+      var canonicalComponentElementReference = componentForClone ? componentForClone.$el : this.$el; // Register custom magic properties.
+
+      Object.entries(Alpine.magicProperties).forEach(function (_ref) {
+        _newArrowCheck(this, _this);
+
+        var _ref2 = _slicedToArray(_ref, 2),
+            name = _ref2[0],
+            callback = _ref2[1];
+
+        Object.defineProperty(this.unobservedData, "$".concat(name), {
+          get: function get() {
+            return callback(canonicalComponentElementReference);
+          }
+        });
+      }.bind(this));
       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;
@@ -6947,13 +7306,13 @@
       value: function registerListeners(el, extraVars) {
       value: function registerListeners(el, extraVars) {
         var _this15 = this;
         var _this15 = this;
 
 
-        getXAttrs(el, this).forEach(function (_ref) {
+        getXAttrs(el, this).forEach(function (_ref3) {
           _newArrowCheck(this, _this15);
           _newArrowCheck(this, _this15);
 
 
-          var type = _ref.type,
-              value = _ref.value,
-              modifiers = _ref.modifiers,
-              expression = _ref.expression;
+          var type = _ref3.type,
+              value = _ref3.value,
+              modifiers = _ref3.modifiers,
+              expression = _ref3.expression;
 
 
           switch (type) {
           switch (type) {
             case 'on':
             case 'on':
@@ -6989,15 +7348,15 @@
           }
           }
         }
         }
 
 
-        attrs.forEach(function (_ref2) {
+        attrs.forEach(function (_ref4) {
           var _this17 = this;
           var _this17 = this;
 
 
           _newArrowCheck(this, _this16);
           _newArrowCheck(this, _this16);
 
 
-          var type = _ref2.type,
-              value = _ref2.value,
-              modifiers = _ref2.modifiers,
-              expression = _ref2.expression;
+          var type = _ref4.type,
+              value = _ref4.value,
+              modifiers = _ref4.modifiers,
+              expression = _ref4.expression;
 
 
           switch (type) {
           switch (type) {
             case 'model':
             case 'model':
@@ -7106,7 +7465,7 @@
               (function () {
               (function () {
                 var _this22 = this;
                 var _this22 = this;
 
 
-                var rawData = saferEval(mutations[i].target.getAttribute('x-data'), {
+                var rawData = saferEval(mutations[i].target.getAttribute('x-data') || '{}', {
                   $el: _this21.$el
                   $el: _this21.$el
                 });
                 });
                 Object.keys(rawData).forEach(function (key) {
                 Object.keys(rawData).forEach(function (key) {
@@ -7191,6 +7550,7 @@
   var Alpine = {
   var Alpine = {
     version: "2.4.1",
     version: "2.4.1",
     pauseMutationObserver: false,
     pauseMutationObserver: false,
+    magicProperties: {},
     start: function () {
     start: function () {
       var _start = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
       var _start = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
         var _this = this;
         var _this = this;
@@ -7329,8 +7689,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;
     }
     }
   };
   };
 
 

+ 28 - 11
dist/alpine.js

@@ -733,13 +733,17 @@
   }
   }
 
 
   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('camel')) {
     if (modifiers.includes('camel')) {
       event = camelCase(event);
       event = camelCase(event);
     }
     }
 
 
     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
@@ -748,12 +752,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;
 
 
@@ -762,7 +766,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;
           }
           }
         }
         }
@@ -785,7 +789,7 @@
             e.preventDefault();
             e.preventDefault();
           } else {
           } else {
             if (modifiers.includes('once')) {
             if (modifiers.includes('once')) {
-              listenerTarget.removeEventListener(event, handler);
+              listenerTarget.removeEventListener(event, handler, options);
             }
             }
           }
           }
         }
         }
@@ -797,7 +801,7 @@
         handler = debounce(handler, wait);
         handler = debounce(handler, wait);
       }
       }
 
 
-      listenerTarget.addEventListener(event, handler);
+      listenerTarget.addEventListener(event, handler, options);
     }
     }
   }
   }
 
 
@@ -1314,12 +1318,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.
@@ -1347,11 +1351,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;
@@ -1620,7 +1633,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 => {
@@ -1677,6 +1690,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();
@@ -1750,8 +1764,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]) {

+ 10 - 6
src/directives/on.js

@@ -1,13 +1,17 @@
 import { kebabCase, camelCase, debounce, isNumeric } from '../utils'
 import { kebabCase, camelCase, 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('camel')) {
     if (modifiers.includes('camel')) {
         event = camelCase(event);
         event = camelCase(event);
     }
     }
 
 
     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.
@@ -18,12 +22,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)
@@ -33,7 +37,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
                 }
                 }
             }
             }
@@ -57,7 +61,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)
                     }
                     }
                 }
                 }
             }
             }
@@ -69,7 +73,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' }">