Browse Source

Merge branch 'master' into ahallhognason-master

Caleb Porzio 5 years ago
parent
commit
1d237ae432
8 changed files with 179 additions and 39 deletions
  1. 23 3
      README.md
  2. 51 13
      dist/alpine.js
  3. 1 1
      package.json
  4. 39 6
      src/component.js
  5. 10 13
      src/index.js
  6. 6 2
      src/utils.js
  7. 26 0
      test/if.spec.js
  8. 23 1
      test/on.spec.js

+ 23 - 3
README.md

@@ -12,7 +12,7 @@ Think of it like [Tailwind](https://tailwindcss.com/) for JavaScript.
 
 
 **From CDN:** Add the following script to the end of your `<head>` section.
 **From CDN:** Add the following script to the end of your `<head>` section.
 ```html
 ```html
-<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v1.0.0/dist/alpine.min.js" defer></script>
+<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v1.1.0/dist/alpine.min.js" defer></script>
 ```
 ```
 
 
 That's it. It will initialize itself.
 That's it. It will initialize itself.
@@ -88,6 +88,7 @@ There are 7 directives available to you:
 | [`x-model`](#x-model) |
 | [`x-model`](#x-model) |
 | [`x-text`](#x-text) |
 | [`x-text`](#x-text) |
 | [`x-ref`](#x-ref) |
 | [`x-ref`](#x-ref) |
+| [`x-if`](#x-if) |
 | [`x-cloak`](#x-cloak) |
 | [`x-cloak`](#x-cloak) |
 
 
 Here's how they each work:
 Here's how they each work:
@@ -106,7 +107,7 @@ Think of it like the `data` property of a Vue component.
 
 
 **Extract Component Logic**
 **Extract Component Logic**
 
 
-You can extract data (and behavior) into reausable functions:
+You can extract data (and behavior) into reusable functions:
 
 
 ```html
 ```html
 <div x-data="dropdown()">
 <div x-data="dropdown()">
@@ -153,7 +154,7 @@ You can also mix-in multiple data objects using object destructuring:
 
 
 `x-bind` sets the value of an attribute to the result of a JavaScript expression. The expression has access to all the keys of the component's data object, and will update every-time it's data is updated.
 `x-bind` sets the value of an attribute to the result of a JavaScript expression. The expression has access to all the keys of the component's data object, and will update every-time it's data is updated.
 
 
-> Note: attribute bindings ONLY update when their dependancies update. The framework is smart enough to observe data changes and detect which bindings care about them.
+> Note: attribute bindings ONLY update when their dependencies update. The framework is smart enough to observe data changes and detect which bindings care about them.
 
 
 **`x-bind` for class attributes**
 **`x-bind` for class attributes**
 
 
@@ -189,6 +190,14 @@ Most common boolean attributes are supported, like `readonly`, `required`, etc.
 
 
 If any data is modified in the expression, other element attributes "bound" to this data, will be updated.
 If any data is modified in the expression, other element attributes "bound" to this data, will be updated.
 
 
+**`keydown` modifiers**
+
+**Example:** `<input type="text" x-on:keydown.escape="open = false">`
+
+You can specify specific keys to listen for using keydown modifiers appended to the `x-on:keydown` directive. Note that the modifiers are kebab-cased versions of `Event.key` values.
+
+Examples: `enter`, `escape`, `arrow-up`, `arrow-down`
+
 **`.away` modifier**
 **`.away` modifier**
 
 
 **Example:** `<div x-on:click.away="showModal = false"></div>`
 **Example:** `<div x-on:click.away="showModal = false"></div>`
@@ -250,6 +259,17 @@ This is a helpful alternative to setting ids and using `document.querySelector`
 
 
 ---
 ---
 
 
+### `x-if`
+**Example:** `<template x-if="true"><div>Some Element</div></template>`
+
+**Structure:** `<template x-if="[expression]"><div>Some Element</div></template>`
+
+For cases where `x-show` isn't sufficient (`x-show` sets an element to `display: none` if it's false), `x-if` can be used to  actually remove an element completely from the DOM.
+
+It's important that `x-if` is used on a `<template></template>` tag because Alpine doesn't use a virtual DOM. This implementation allows Alpine to stay rugged and use the real DOM to work it's magic.
+
+---
+
 ### `x-cloak`
 ### `x-cloak`
 **Example:** `<div x-data="{}" x-cloak></div>`
 **Example:** `<div x-data="{}" x-cloak></div>`
 
 

+ 51 - 13
dist/alpine.js

@@ -982,6 +982,14 @@ function () {
 
 
             break;
             break;
 
 
+          case 'if':
+            var _this2$evaluateReturn5 = _this2.evaluateReturnExpression(expression),
+                output = _this2$evaluateReturn5.output;
+
+            _this2.updatePresence(el, output);
+
+            break;
+
           case 'cloak':
           case 'cloak':
             el.removeAttribute('x-cloak');
             el.removeAttribute('x-cloak');
             break;
             break;
@@ -1090,6 +1098,19 @@ function () {
 
 
               break;
               break;
 
 
+            case 'if':
+              var _self$evaluateReturnE5 = self.evaluateReturnExpression(expression),
+                  output = _self$evaluateReturnE5.output,
+                  deps = _self$evaluateReturnE5.deps;
+
+              if (self.concernedData.filter(function (i) {
+                return deps.includes(i);
+              }).length > 0) {
+                self.updatePresence(el, output);
+              }
+
+              break;
+
             default:
             default:
               break;
               break;
           }
           }
@@ -1149,6 +1170,10 @@ function () {
         var node = modifiers.includes('window') ? window : el;
         var node = modifiers.includes('window') ? window : el;
 
 
         var _handler = function _handler(e) {
         var _handler = function _handler(e) {
+          var modifiersWithoutWindow = modifiers.filter(function (i) {
+            return i !== 'window';
+          });
+          if (event === 'keydown' && modifiersWithoutWindow.length > 0 && !modifiersWithoutWindow.includes(Object(_utils__WEBPACK_IMPORTED_MODULE_0__["kebabCase"])(e.key))) return;
           if (modifiers.includes('prevent')) e.preventDefault();
           if (modifiers.includes('prevent')) e.preventDefault();
           if (modifiers.includes('stop')) e.stopPropagation();
           if (modifiers.includes('stop')) e.stopPropagation();
 
 
@@ -1227,6 +1252,20 @@ function () {
         }
         }
       }
       }
     }
     }
+  }, {
+    key: "updatePresence",
+    value: function updatePresence(el, expressionResult) {
+      if (el.nodeName.toLowerCase() !== 'template') console.warn("Alpine: [x-if] directive should only be added to <template> tags.");
+      var elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true;
+
+      if (expressionResult && !elementHasAlreadyBeenAdded) {
+        var clone = document.importNode(el.content, true);
+        el.parentElement.insertBefore(clone, el.nextElementSibling);
+        el.nextElementSibling.__x_inserted_me = true;
+      } else if (!expressionResult && elementHasAlreadyBeenAdded) {
+        el.nextElementSibling.remove();
+      }
+    }
   }, {
   }, {
     key: "updateAttributeValue",
     key: "updateAttributeValue",
     value: function updateAttributeValue(el, attrName, value) {
     value: function updateAttributeValue(el, attrName, value) {
@@ -1337,8 +1376,6 @@ __webpack_require__.r(__webpack_exports__);
 /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils */ "./src/utils.js");
 /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils */ "./src/utils.js");
 
 
 
 
-/* @flow */
-
 
 
 var Alpine = {
 var Alpine = {
   start: function start() {
   start: function start() {
@@ -1358,17 +1395,17 @@ var Alpine = {
 
 
           case 3:
           case 3:
             this.discoverComponents(function (el) {
             this.discoverComponents(function (el) {
-              _this.initializeElement(el);
+              _this.initializeComponent(el);
             }); // It's easier and more performant to just support Turbolinks than listen
             }); // It's easier and more performant to just support Turbolinks than listen
             // to MutationOberserver mutations at the document level.
             // to MutationOberserver mutations at the document level.
 
 
             document.addEventListener("turbolinks:load", function () {
             document.addEventListener("turbolinks:load", function () {
               _this.discoverUninitializedComponents(function (el) {
               _this.discoverUninitializedComponents(function (el) {
-                _this.initializeElement(el);
+                _this.initializeComponent(el);
               });
               });
             });
             });
             this.listenForNewUninitializedComponentsAtRunTime(function (el) {
             this.listenForNewUninitializedComponentsAtRunTime(function (el) {
-              _this.initializeElement(el);
+              _this.initializeComponent(el);
             });
             });
 
 
           case 6:
           case 6:
@@ -1404,17 +1441,14 @@ var Alpine = {
         if (mutations[i].addedNodes.length > 0) {
         if (mutations[i].addedNodes.length > 0) {
           mutations[i].addedNodes.forEach(function (node) {
           mutations[i].addedNodes.forEach(function (node) {
             if (node.nodeType !== 1) return;
             if (node.nodeType !== 1) return;
-
-            if (node.matches('[x-data]')) {
-              callback(node);
-            }
+            if (node.matches('[x-data]')) callback(node);
           });
           });
         }
         }
       }
       }
     });
     });
     observer.observe(targetNode, observerOptions);
     observer.observe(targetNode, observerOptions);
   },
   },
-  initializeElement: function initializeElement(el) {
+  initializeComponent: function initializeComponent(el) {
     el.__x = new _component__WEBPACK_IMPORTED_MODULE_1__["default"](el);
     el.__x = new _component__WEBPACK_IMPORTED_MODULE_1__["default"](el);
   }
   }
 };
 };
@@ -1432,13 +1466,14 @@ if (!window.Alpine && !Object(_utils__WEBPACK_IMPORTED_MODULE_2__["isTesting"])(
 /*!**********************!*\
 /*!**********************!*\
   !*** ./src/utils.js ***!
   !*** ./src/utils.js ***!
   \**********************/
   \**********************/
-/*! exports provided: domReady, isTesting, walkSkippingNestedComponents, debounce, onlyUnique, saferEval, saferEvalNoReturn, isXAttr, getXAttrs */
+/*! exports provided: domReady, isTesting, kebabCase, walkSkippingNestedComponents, debounce, onlyUnique, saferEval, saferEvalNoReturn, isXAttr, getXAttrs */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 
 "use strict";
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "domReady", function() { return domReady; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "domReady", function() { return domReady; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isTesting", function() { return isTesting; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isTesting", function() { return isTesting; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "kebabCase", function() { return kebabCase; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "walkSkippingNestedComponents", function() { return walkSkippingNestedComponents; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "walkSkippingNestedComponents", function() { return walkSkippingNestedComponents; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return debounce; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return debounce; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onlyUnique", function() { return onlyUnique; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onlyUnique", function() { return onlyUnique; });
@@ -1468,6 +1503,9 @@ function domReady() {
 function isTesting() {
 function isTesting() {
   return navigator.userAgent, navigator.userAgent.includes("Node.js") || navigator.userAgent.includes("jsdom");
   return navigator.userAgent, navigator.userAgent.includes("Node.js") || navigator.userAgent.includes("jsdom");
 }
 }
+function kebabCase(subject) {
+  return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase();
+}
 function walkSkippingNestedComponents(el, callback) {
 function walkSkippingNestedComponents(el, callback) {
   var isRoot = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
   var isRoot = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
   if (el.hasAttribute('x-data') && !isRoot) return;
   if (el.hasAttribute('x-data') && !isRoot) return;
@@ -1509,12 +1547,12 @@ function saferEvalNoReturn(expression, dataContext) {
   return new Function(['$data'].concat(_toConsumableArray(Object.keys(additionalHelperVariables))), "with($data) { ".concat(expression, " }")).apply(void 0, [dataContext].concat(_toConsumableArray(Object.values(additionalHelperVariables))));
   return new Function(['$data'].concat(_toConsumableArray(Object.keys(additionalHelperVariables))), "with($data) { ".concat(expression, " }")).apply(void 0, [dataContext].concat(_toConsumableArray(Object.values(additionalHelperVariables))));
 }
 }
 function isXAttr(attr) {
 function isXAttr(attr) {
-  var xAttrRE = /x-(on|bind|data|text|model|show|cloak|ref)/;
+  var xAttrRE = /x-(on|bind|data|text|model|if|show|cloak|ref)/;
   return xAttrRE.test(attr.name);
   return xAttrRE.test(attr.name);
 }
 }
 function getXAttrs(el, type) {
 function getXAttrs(el, type) {
   return Array.from(el.attributes).filter(isXAttr).map(function (attr) {
   return Array.from(el.attributes).filter(isXAttr).map(function (attr) {
-    var typeMatch = attr.name.match(/x-(on|bind|data|text|model|show|cloak|ref)/);
+    var typeMatch = attr.name.match(/x-(on|bind|data|text|model|if|show|cloak|ref)/);
     var valueMatch = attr.name.match(/:([a-zA-Z\-]+)/);
     var valueMatch = attr.name.match(/:([a-zA-Z\-]+)/);
     var modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
     var modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
     return {
     return {

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
 {
   "main": "dist/alpine.js",
   "main": "dist/alpine.js",
   "name": "alpinejs",
   "name": "alpinejs",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "git://github.com/alpinejs/alpine.git"
     "url": "git://github.com/alpinejs/alpine.git"

+ 39 - 6
src/component.js

@@ -1,4 +1,4 @@
-import { walkSkippingNestedComponents, saferEval, saferEvalNoReturn, getXAttrs, debounce } from './utils'
+import { walkSkippingNestedComponents, kebabCase, saferEval, saferEvalNoReturn, getXAttrs, debounce } from './utils'
 
 
 export default class Component {
 export default class Component {
     constructor(el) {
     constructor(el) {
@@ -91,6 +91,11 @@ export default class Component {
                     this.updateVisibility(el, output)
                     this.updateVisibility(el, output)
                     break;
                     break;
 
 
+                case 'if':
+                    var { output } = this.evaluateReturnExpression(expression)
+                    this.updatePresence(el, output)
+                    break;
+
                 case 'cloak':
                 case 'cloak':
                     el.removeAttribute('x-cloak')
                     el.removeAttribute('x-cloak')
                     break;
                     break;
@@ -102,16 +107,16 @@ export default class Component {
     }
     }
 
 
     listenForNewElementsToInitialize() {
     listenForNewElementsToInitialize() {
-        var targetNode = this.el
+        const targetNode = this.el
 
 
-        var observerOptions = {
+        const observerOptions = {
             childList: true,
             childList: true,
             attributes: false,
             attributes: false,
             subtree: true,
             subtree: true,
         }
         }
 
 
-        var observer = new MutationObserver((mutations) => {
-            for (var i=0; i < mutations.length; i++){
+        const observer = new MutationObserver((mutations) => {
+            for (let i=0; i < mutations.length; i++){
                 if (mutations[i].addedNodes.length > 0) {
                 if (mutations[i].addedNodes.length > 0) {
                     mutations[i].addedNodes.forEach(node => {
                     mutations[i].addedNodes.forEach(node => {
                         if (node.nodeType !== 1) return
                         if (node.nodeType !== 1) return
@@ -124,7 +129,7 @@ export default class Component {
                     })
                     })
                 }
                 }
               }
               }
-        });
+        })
 
 
         observer.observe(targetNode, observerOptions);
         observer.observe(targetNode, observerOptions);
     }
     }
@@ -173,6 +178,14 @@ export default class Component {
                         }
                         }
                         break;
                         break;
 
 
+                    case 'if':
+                        var { output, deps } = self.evaluateReturnExpression(expression)
+
+                        if (self.concernedData.filter(i => deps.includes(i)).length > 0) {
+                            self.updatePresence(el, output)
+                        }
+                        break;
+
                     default:
                     default:
                         break;
                         break;
                 }
                 }
@@ -233,6 +246,10 @@ export default class Component {
             const node = modifiers.includes('window') ? window : el
             const node = modifiers.includes('window') ? window : el
 
 
             const handler = e => {
             const handler = e => {
+                const modifiersWithoutWindow = modifiers.filter(i => i !== 'window')
+
+                if (event === 'keydown' && modifiersWithoutWindow.length > 0 && ! modifiersWithoutWindow.includes(kebabCase(e.key))) return
+
                 if (modifiers.includes('prevent')) e.preventDefault()
                 if (modifiers.includes('prevent')) e.preventDefault()
                 if (modifiers.includes('stop')) e.stopPropagation()
                 if (modifiers.includes('stop')) e.stopPropagation()
 
 
@@ -307,6 +324,22 @@ export default class Component {
         }
         }
     }
     }
 
 
+    updatePresence(el, expressionResult) {
+        if (el.nodeName.toLowerCase() !== 'template') console.warn(`Alpine: [x-if] directive should only be added to <template> tags.`)
+
+        const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true
+
+        if (expressionResult && ! elementHasAlreadyBeenAdded) {
+            const clone = document.importNode(el.content, true);
+
+            el.parentElement.insertBefore(clone, el.nextElementSibling)
+
+            el.nextElementSibling.__x_inserted_me = true
+        } else if (! expressionResult && elementHasAlreadyBeenAdded) {
+            el.nextElementSibling.remove()
+        }
+    }
+
     updateAttributeValue(el, attrName, value) {
     updateAttributeValue(el, attrName, value) {
         if (attrName === 'value') {
         if (attrName === 'value') {
             if (el.type === 'radio') {
             if (el.type === 'radio') {

+ 10 - 13
src/index.js

@@ -1,4 +1,3 @@
-/* @flow */
 import Component from './component'
 import Component from './component'
 import { domReady, isTesting } from './utils'
 import { domReady, isTesting } from './utils'
 
 
@@ -9,19 +8,19 @@ const Alpine = {
         }
         }
 
 
         this.discoverComponents(el => {
         this.discoverComponents(el => {
-            this.initializeElement(el)
+            this.initializeComponent(el)
         })
         })
 
 
         // It's easier and more performant to just support Turbolinks than listen
         // It's easier and more performant to just support Turbolinks than listen
         // to MutationOberserver mutations at the document level.
         // to MutationOberserver mutations at the document level.
         document.addEventListener("turbolinks:load", () => {
         document.addEventListener("turbolinks:load", () => {
             this.discoverUninitializedComponents(el => {
             this.discoverUninitializedComponents(el => {
-                this.initializeElement(el)
+                this.initializeComponent(el)
             })
             })
         })
         })
 
 
         this.listenForNewUninitializedComponentsAtRunTime(el => {
         this.listenForNewUninitializedComponentsAtRunTime(el => {
-            this.initializeElement(el)
+            this.initializeComponent(el)
         })
         })
     },
     },
 
 
@@ -44,32 +43,30 @@ const Alpine = {
     },
     },
 
 
     listenForNewUninitializedComponentsAtRunTime: function (callback) {
     listenForNewUninitializedComponentsAtRunTime: function (callback) {
-        var targetNode = document.querySelector('body');
+        const targetNode = document.querySelector('body');
 
 
-        var observerOptions = {
+        const observerOptions = {
             childList: true,
             childList: true,
             attributes: true,
             attributes: true,
             subtree: true,
             subtree: true,
         }
         }
 
 
-        var observer = new MutationObserver((mutations) => {
-            for (var i=0; i < mutations.length; i++){
+        const observer = new MutationObserver((mutations) => {
+            for (let i=0; i < mutations.length; i++){
                 if (mutations[i].addedNodes.length > 0) {
                 if (mutations[i].addedNodes.length > 0) {
                     mutations[i].addedNodes.forEach(node => {
                     mutations[i].addedNodes.forEach(node => {
                         if (node.nodeType !== 1) return
                         if (node.nodeType !== 1) return
 
 
-                        if (node.matches('[x-data]')) {
-                            callback(node)
-                        }
+                        if (node.matches('[x-data]')) callback(node)
                     })
                     })
                 }
                 }
               }
               }
-        });
+        })
 
 
         observer.observe(targetNode, observerOptions)
         observer.observe(targetNode, observerOptions)
     },
     },
 
 
-    initializeElement: function (el) {
+    initializeComponent: function (el) {
         el.__x = new Component(el)
         el.__x = new Component(el)
     }
     }
 }
 }

+ 6 - 2
src/utils.js

@@ -16,6 +16,10 @@ export function isTesting() {
         || navigator.userAgent.includes("jsdom")
         || navigator.userAgent.includes("jsdom")
 }
 }
 
 
+export function kebabCase(subject) {
+    return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
+}
+
 export function walkSkippingNestedComponents(el, callback, isRoot = true) {
 export function walkSkippingNestedComponents(el, callback, isRoot = true) {
     if (el.hasAttribute('x-data') && ! isRoot) return
     if (el.hasAttribute('x-data') && ! isRoot) return
 
 
@@ -62,7 +66,7 @@ export function saferEvalNoReturn(expression, dataContext, additionalHelperVaria
 }
 }
 
 
 export function isXAttr(attr) {
 export function isXAttr(attr) {
-    const xAttrRE = /x-(on|bind|data|text|model|show|cloak|ref)/
+    const xAttrRE = /x-(on|bind|data|text|model|if|show|cloak|ref)/
 
 
     return xAttrRE.test(attr.name)
     return xAttrRE.test(attr.name)
 }
 }
@@ -71,7 +75,7 @@ export function getXAttrs(el, type) {
     return Array.from(el.attributes)
     return Array.from(el.attributes)
         .filter(isXAttr)
         .filter(isXAttr)
         .map(attr => {
         .map(attr => {
-            const typeMatch = attr.name.match(/x-(on|bind|data|text|model|show|cloak|ref)/)
+            const typeMatch = attr.name.match(/x-(on|bind|data|text|model|if|show|cloak|ref)/)
             const valueMatch = attr.name.match(/:([a-zA-Z\-]+)/)
             const valueMatch = attr.name.match(/:([a-zA-Z\-]+)/)
             const modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
             const modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
 
 

+ 26 - 0
test/if.spec.js

@@ -0,0 +1,26 @@
+import Alpine from 'alpinejs'
+import { wait } from '@testing-library/dom'
+
+global.MutationObserver = class {
+    observe() {}
+}
+
+test('x-if', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ show: false }">
+            <button x-on:click="show = ! show"></button>
+
+            <template x-if="show">
+                <p></p>
+            </template>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('p')).toBeFalsy()
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('p')).toBeTruthy() })
+})

+ 23 - 1
test/on.spec.js

@@ -1,5 +1,5 @@
 import Alpine from 'alpinejs'
 import Alpine from 'alpinejs'
-import { wait } from '@testing-library/dom'
+import { wait, fireEvent } from '@testing-library/dom'
 const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
 const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
 
 
 global.MutationObserver = class {
 global.MutationObserver = class {
@@ -121,6 +121,28 @@ test('.once modifier', async () => {
     expect(document.querySelector('span').getAttribute('foo')).toEqual('1')
     expect(document.querySelector('span').getAttribute('foo')).toEqual('1')
 })
 })
 
 
+test('keydown modifiers', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ count: 0 }">
+            <input type="text" x-on:keydown="count++" x-on:keydown.enter="count++">
+
+            <span x-text="count"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual(0)
+
+    fireEvent.keyDown(document.querySelector('input'), { key: 'Enter' })
+
+    await wait(() => { expect(document.querySelector('span').innerText).toEqual(2) })
+
+    fireEvent.keyDown(document.querySelector('input'), { key: 'Escape' })
+
+    await wait(() => { expect(document.querySelector('span').innerText).toEqual(3) })
+})
+
 test('click away', async () => {
 test('click away', async () => {
     // Because jsDom doesn't support .offsetHeight and offsetWidth, we have to
     // Because jsDom doesn't support .offsetHeight and offsetWidth, we have to
     // make our own implementation using a specific class added to the class. Ugh.
     // make our own implementation using a specific class added to the class. Ugh.