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.
 ```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.
@@ -88,6 +88,7 @@ There are 7 directives available to you:
 | [`x-model`](#x-model) |
 | [`x-text`](#x-text) |
 | [`x-ref`](#x-ref) |
+| [`x-if`](#x-if) |
 | [`x-cloak`](#x-cloak) |
 
 Here's how they each work:
@@ -106,7 +107,7 @@ Think of it like the `data` property of a Vue component.
 
 **Extract Component Logic**
 
-You can extract data (and behavior) into reausable functions:
+You can extract data (and behavior) into reusable functions:
 
 ```html
 <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.
 
-> 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**
 
@@ -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.
 
+**`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**
 
 **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`
 **Example:** `<div x-data="{}" x-cloak></div>`
 

+ 51 - 13
dist/alpine.js

@@ -982,6 +982,14 @@ function () {
 
             break;
 
+          case 'if':
+            var _this2$evaluateReturn5 = _this2.evaluateReturnExpression(expression),
+                output = _this2$evaluateReturn5.output;
+
+            _this2.updatePresence(el, output);
+
+            break;
+
           case 'cloak':
             el.removeAttribute('x-cloak');
             break;
@@ -1090,6 +1098,19 @@ function () {
 
               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:
               break;
           }
@@ -1149,6 +1170,10 @@ function () {
         var node = modifiers.includes('window') ? window : el;
 
         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('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",
     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");
 
 
-/* @flow */
-
 
 var Alpine = {
   start: function start() {
@@ -1358,17 +1395,17 @@ var Alpine = {
 
           case 3:
             this.discoverComponents(function (el) {
-              _this.initializeElement(el);
+              _this.initializeComponent(el);
             }); // It's easier and more performant to just support Turbolinks than listen
             // to MutationOberserver mutations at the document level.
 
             document.addEventListener("turbolinks:load", function () {
               _this.discoverUninitializedComponents(function (el) {
-                _this.initializeElement(el);
+                _this.initializeComponent(el);
               });
             });
             this.listenForNewUninitializedComponentsAtRunTime(function (el) {
-              _this.initializeElement(el);
+              _this.initializeComponent(el);
             });
 
           case 6:
@@ -1404,17 +1441,14 @@ var Alpine = {
         if (mutations[i].addedNodes.length > 0) {
           mutations[i].addedNodes.forEach(function (node) {
             if (node.nodeType !== 1) return;
-
-            if (node.matches('[x-data]')) {
-              callback(node);
-            }
+            if (node.matches('[x-data]')) callback(node);
           });
         }
       }
     });
     observer.observe(targetNode, observerOptions);
   },
-  initializeElement: function initializeElement(el) {
+  initializeComponent: function initializeComponent(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 ***!
   \**********************/
-/*! 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__) {
 
 "use strict";
 __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__, "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__, "debounce", function() { return debounce; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onlyUnique", function() { return onlyUnique; });
@@ -1468,6 +1503,9 @@ function domReady() {
 function isTesting() {
   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) {
   var isRoot = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
   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))));
 }
 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);
 }
 function getXAttrs(el, type) {
   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 modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
     return {

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "main": "dist/alpine.js",
   "name": "alpinejs",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "repository": {
     "type": "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 {
     constructor(el) {
@@ -91,6 +91,11 @@ export default class Component {
                     this.updateVisibility(el, output)
                     break;
 
+                case 'if':
+                    var { output } = this.evaluateReturnExpression(expression)
+                    this.updatePresence(el, output)
+                    break;
+
                 case 'cloak':
                     el.removeAttribute('x-cloak')
                     break;
@@ -102,16 +107,16 @@ export default class Component {
     }
 
     listenForNewElementsToInitialize() {
-        var targetNode = this.el
+        const targetNode = this.el
 
-        var observerOptions = {
+        const observerOptions = {
             childList: true,
             attributes: false,
             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) {
                     mutations[i].addedNodes.forEach(node => {
                         if (node.nodeType !== 1) return
@@ -124,7 +129,7 @@ export default class Component {
                     })
                 }
               }
-        });
+        })
 
         observer.observe(targetNode, observerOptions);
     }
@@ -173,6 +178,14 @@ export default class Component {
                         }
                         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:
                         break;
                 }
@@ -233,6 +246,10 @@ export default class Component {
             const node = modifiers.includes('window') ? window : el
 
             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('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) {
         if (attrName === 'value') {
             if (el.type === 'radio') {

+ 10 - 13
src/index.js

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

+ 6 - 2
src/utils.js

@@ -16,6 +16,10 @@ export function isTesting() {
         || 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) {
     if (el.hasAttribute('x-data') && ! isRoot) return
 
@@ -62,7 +66,7 @@ export function saferEvalNoReturn(expression, dataContext, additionalHelperVaria
 }
 
 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)
 }
@@ -71,7 +75,7 @@ export function getXAttrs(el, type) {
     return Array.from(el.attributes)
         .filter(isXAttr)
         .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 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 { wait } from '@testing-library/dom'
+import { wait, fireEvent } from '@testing-library/dom'
 const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
 
 global.MutationObserver = class {
@@ -121,6 +121,28 @@ test('.once modifier', async () => {
     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 () => {
     // Because jsDom doesn't support .offsetHeight and offsetWidth, we have to
     // make our own implementation using a specific class added to the class. Ugh.