Caleb Porzio 5 years ago
parent
commit
abd7701bf4
11 changed files with 515 additions and 168 deletions
  1. 14 0
      README.md
  2. 153 66
      dist/minimal.js
  3. 1 1
      dist/mix-manifest.json
  4. 19 8
      index.html
  5. 32 1
      package-lock.json
  6. 3 2
      package.json
  7. 79 46
      src/component.js
  8. 5 4
      src/index.js
  9. 97 0
      src/something.js
  10. 42 9
      src/utils.js
  11. 70 31
      test/test.spec.js

+ 14 - 0
README.md

@@ -4,6 +4,18 @@
 `<script src="https://cdn.jsdelivr.net/gh/minimaljs/minimal/dist/minimal.min.js"></script>`
 
 ### Use
+*Dropdown*
+```
+<div x-data="{ hide: true }">
+    <button x-on:click="$data.hide = false">...</button>
+
+    <ul class="hidden" x-bind:class="{ 'hidden': $data.hide }" :click="$data.hide = true">
+        ...
+    </ul>
+</div>
+```
+
+*Modal*
 ```
 <div x-data="{ show: true }">
     <button x-on:click="$data.show = ! $data.show">toggle</button>
@@ -12,6 +24,8 @@
 </div>
 ```
 
+*Tabs*
+
 You can bind expressions to any attribute using `x-bind`, and you can run expressions on any event using `x-on`.
 
 The data is "reactive", when some data updates, only the expressions conscerned with it will.

+ 153 - 66
dist/minimal.js

@@ -107,6 +107,14 @@ return /******/ (function(modules) { // webpackBootstrap
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Component; });
 /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./src/utils.js");
+function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
+
+function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
+
+function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
+
+function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
+
 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
 
 function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
@@ -122,11 +130,10 @@ function () {
     _classCallCheck(this, Component);
 
     this.el = el;
-    this.data = Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEval"])(this.el.getAttribute('x-data'));
+    this.data = Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEval"])(this.el.getAttribute('x-data'), {});
+    this.concernedData = [];
     this.registerListeners();
-    this.updateBoundAttributes(function () {
-      return true;
-    });
+    this.updateAllBoundAttributes();
   }
 
   _createClass(Component, [{
@@ -134,39 +141,62 @@ function () {
     value: function registerListeners() {
       var _this = this;
 
-      // Do a sweep through the component, find out what events children are
-      // listening for so we can do "event delegation" on the root.
-      // The reason for using event delegation is so that new
-      // DOM element listeners can potentially be added
-      // and they will be detected.
-      this.eventsThisComponentIsListeningFor().forEach(function (eventName) {
-        _this.el.addEventListener(eventName, function (e) {
-          if (e.target.hasAttribute("x-on:".concat(eventName))) {
-            var mutatedDataItems = []; // Detect if the listener action mutated some data,
-            // this way we can selectively update bindings.
-
-            var proxiedData = new Proxy(_this.data, {
-              set: function set(obj, property, value) {
-                var setWasSuccessful = Reflect.set(obj, property, value);
-                mutatedDataItems.push(property);
-                return setWasSuccessful;
-              }
-            });
-            var expression = e.target.getAttribute("x-on:".concat(eventName));
-            Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEval"])(expression, {
-              '$data': proxiedData,
-              '$event': e
+      Object(_utils__WEBPACK_IMPORTED_MODULE_0__["walk"])(this.el, function (el) {
+        Object(_utils__WEBPACK_IMPORTED_MODULE_0__["getXAttrs"])(el, 'on').forEach(function (_ref) {
+          var type = _ref.type,
+              value = _ref.value,
+              modifiers = _ref.modifiers,
+              expression = _ref.expression;
+
+          if (modifiers.includes('away')) {
+            // Listen for this event at the root level.
+            document.addEventListener(value, function (e) {
+              var _this$concernedData;
+
+              // Don't do anything if the click came form the element or within it.
+              if (el.contains(e.target)) return; // Don't do anything if this element isn't currently visible.
+
+              if (el.offsetWidth < 1 && el.offsetHeight < 1) return; // Now that we are sure the element is visible, AND the click
+              // is from outside it, let's run the expression.
+
+              (_this$concernedData = _this.concernedData).push.apply(_this$concernedData, _toConsumableArray(_this.evaluateExpressionWithEvent(expression, e)));
+
+              _this.concernedData = _this.concernedData.filter(_utils__WEBPACK_IMPORTED_MODULE_0__["onlyUnique"]);
+
+              _this.updateBoundAttributes();
             });
+          } else {
+            el.addEventListener(value, function (e) {
+              var _this$concernedData2;
+
+              (_this$concernedData2 = _this.concernedData).push.apply(_this$concernedData2, _toConsumableArray(_this.evaluateExpressionWithEvent(expression, e)));
+
+              _this.concernedData = _this.concernedData.filter(_utils__WEBPACK_IMPORTED_MODULE_0__["onlyUnique"]);
 
-            _this.updateBoundAttributes(function (isConscernedWith) {
-              return mutatedDataItems.filter(function (i) {
-                return isConscernedWith.includes(i);
-              }).length > 0;
+              _this.updateBoundAttributes();
             });
           }
         });
       });
     }
+  }, {
+    key: "evaluateExpressionWithEvent",
+    value: function evaluateExpressionWithEvent(expression, event) {
+      var mutatedDataItems = []; // Detect if the listener action mutated some data,
+      // this way we can selectively update bindings.
+
+      var proxiedData = new Proxy(this.data, {
+        set: function set(obj, property, value) {
+          var setWasSuccessful = Reflect.set(obj, property, value);
+          mutatedDataItems.push(property);
+          return setWasSuccessful;
+        }
+      });
+      Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEvalNoReturn"])(expression, proxiedData, {
+        '$event': event
+      });
+      return mutatedDataItems;
+    }
   }, {
     key: "eventsThisComponentIsListeningFor",
     value: function eventsThisComponentIsListeningFor() {
@@ -184,30 +214,53 @@ function () {
     }
   }, {
     key: "updateBoundAttributes",
-    value: function updateBoundAttributes(ifConcernedWith) {
+    value: function updateBoundAttributes() {
+      var self = this;
+      Object(_utils__WEBPACK_IMPORTED_MODULE_0__["debounce"])(_utils__WEBPACK_IMPORTED_MODULE_0__["walk"], 5)(this.el, function (el) {
+        Object(_utils__WEBPACK_IMPORTED_MODULE_0__["getXAttrs"])(el, 'bind').forEach(function (_ref2) {
+          var type = _ref2.type,
+              value = _ref2.value,
+              modifiers = _ref2.modifiers,
+              expression = _ref2.expression;
+          var isConscernedWith = [];
+          var proxiedData = new Proxy(self.data, {
+            get: function get(object, prop) {
+              isConscernedWith.push(prop);
+              return object[prop];
+            }
+          });
+          var result = Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEval"])(expression, proxiedData);
+
+          if (self.concernedData.filter(function (i) {
+            return isConscernedWith.includes(i);
+          }).length > 0) {
+            self.updateBoundAttributeValue(el, value, result);
+          }
+        });
+      });
+    }
+  }, {
+    key: "updateAllBoundAttributes",
+    value: function updateAllBoundAttributes() {
       var _this2 = this;
 
       Object(_utils__WEBPACK_IMPORTED_MODULE_0__["walk"])(this.el, function (el) {
-        if (Object(_utils__WEBPACK_IMPORTED_MODULE_0__["hasXAttr"])(el, 'bind')) {
-          Object(_utils__WEBPACK_IMPORTED_MODULE_0__["getXAttrs"])(el, 'bind').forEach(function (attr) {
-            var boundAttribute = attr.name.replace(/x-bind:/, '');
-            var expression = attr.value;
-            var isConscernedWith = [];
-            var proxiedData = new Proxy(_this2.data, {
-              get: function get(object, prop) {
-                isConscernedWith.push(prop);
-                return object[prop];
-              }
-            });
-            var result = Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEval"])(expression, {
-              "$data": proxiedData
-            });
-
-            if (ifConcernedWith(isConscernedWith)) {
-              _this2.updateBoundAttributeValue(el, boundAttribute, result);
+        Object(_utils__WEBPACK_IMPORTED_MODULE_0__["getXAttrs"])(el, 'bind').forEach(function (_ref3) {
+          var type = _ref3.type,
+              value = _ref3.value,
+              modifiers = _ref3.modifiers,
+              expression = _ref3.expression;
+          var isConscernedWith = [];
+          var proxiedData = new Proxy(_this2.data, {
+            get: function get(object, prop) {
+              isConscernedWith.push(prop);
+              return object[prop];
             }
           });
-        }
+          var result = Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEval"])(expression, proxiedData);
+
+          _this2.updateBoundAttributeValue(el, value, result);
+        });
       });
     }
   }, {
@@ -252,8 +305,9 @@ function () {
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony import */ var _component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./component */ "./src/component.js");
+/* @flow */
 
-var rename = {
+var minimal = {
   start: function start() {
     var rootEls = document.querySelectorAll('[x-data]');
     rootEls.forEach(function (rootEl) {
@@ -262,11 +316,11 @@ var rename = {
   }
 };
 
-if (!window.rename) {
-  window.rename = rename;
+if (!window.minimal) {
+  window.minimal = minimal;
 }
 
-/* harmony default export */ __webpack_exports__["default"] = (rename);
+/* harmony default export */ __webpack_exports__["default"] = (minimal);
 
 /***/ }),
 
@@ -274,15 +328,17 @@ if (!window.rename) {
 /*!**********************!*\
   !*** ./src/utils.js ***!
   \**********************/
-/*! exports provided: walk, onlyUnique, saferEval, hasXAttr, getXAttrs */
+/*! exports provided: walk, 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__, "walk", function() { return walk; });
+/* 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__, "saferEval", function() { return saferEval; });
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "hasXAttr", function() { return hasXAttr; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "saferEvalNoReturn", function() { return saferEvalNoReturn; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isXAttr", function() { return isXAttr; });
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getXAttrs", function() { return getXAttrs; });
 function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
 
@@ -301,23 +357,54 @@ function walk(el, callback) {
     node = node.nextElementSibling;
   }
 }
+function debounce(func, wait, immediate) {
+  var timeout;
+  return function () {
+    var context = this,
+        args = arguments;
+
+    var later = function later() {
+      timeout = null;
+      if (!immediate) func.apply(context, args);
+    };
+
+    var callNow = immediate && !timeout;
+    clearTimeout(timeout);
+    timeout = setTimeout(later, wait);
+    if (callNow) func.apply(context, args);
+  };
+}
+;
 function onlyUnique(value, index, self) {
   return self.indexOf(value) === index;
 }
-function saferEval(expression) {
-  var scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
-  return new Function(Object.keys(scope), "\"use strict\"; return ".concat(expression)).apply(void 0, _toConsumableArray(Object.values(scope)));
+function saferEval(expression, dataContext) {
+  var additionalHelperVariables = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+  return new Function(['$data'].concat(_toConsumableArray(Object.keys(additionalHelperVariables))), "var result; with($data) { result = ".concat(expression, " }; return result")).apply(void 0, [dataContext].concat(_toConsumableArray(Object.values(additionalHelperVariables))));
 }
-function hasXAttr(el, name) {
-  return !!Array.from(el.attributes).map(function (i) {
-    return i.name;
-  }).filter(function (i) {
-    return i.search(new RegExp("^x-".concat(name))) > -1;
-  }).length;
+function saferEvalNoReturn(expression, dataContext) {
+  var additionalHelperVariables = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+  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):/;
+  return xAttrRE.test(attr.name);
 }
 function getXAttrs(el, name) {
-  return Array.from(el.attributes).filter(function (i) {
-    return i.name.search(new RegExp("^x-".concat(name))) > -1;
+  return Array.from(el.attributes).filter(isXAttr).map(function (attr) {
+    var typeMatch = attr.name.match(/x-(on|bind|data):/);
+    var valueMatch = attr.name.match(/:([a-zA-Z\-]+)/);
+    var modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
+    return {
+      type: typeMatch ? typeMatch[1] : null,
+      value: valueMatch ? valueMatch[1] : null,
+      modifiers: modifiers.map(function (i) {
+        return i.replace('.', '');
+      }),
+      expression: attr.value
+    };
+  }).filter(function (i) {
+    return i.type === name;
   });
 }
 

+ 1 - 1
dist/mix-manifest.json

@@ -1,6 +1,6 @@
 {
     "/rename-me.js": "/rename-me.js?id=bbf39d4a044a5ded45f2",
     "/rename-me.min.js": "/rename-me.min.js?id=727c8cae5cc9ff82a386",
-    "/minimal.js": "/minimal.js?id=b1fad07629485d0e82a8",
+    "/minimal.js": "/minimal.js?id=9d75625d65a35b1f6a1b",
     "/minimal.min.js": "/minimal.min.js?id=5456a8b662edbed0ae52"
 }

+ 19 - 8
index.html

@@ -1,21 +1,32 @@
 <html>
     <head>
         <style>
+            .hidden { display: none; }
         </style>
     </head>
     <body>
-        <div x-data='{ "isOpen": false, "count": 1 }'>
-            <button x-on:click="$data.isOpen = ! $data.isOpen">toggle</button>
-            <button x-on:click="$data.count++">inc</button>
+        <div id="outer">
+            outer
+            <div x-data="{ isOpen: true }">
+                <button x-on:click="$data.isOpen = true"></button>
 
-            <input type="checkbox" x-bind:checked="$data.count % 2">
-            <input type="text" x-on:input="$data.count = $event.target.value">
-            <input type="text" x-bind:value="$data.count">
+                <ul x-bind:value="$data.isOpen" x-on:click.away="$data.isOpen = false">
+                    <li>...</li>
+                </ul>
+            </div>
         </div>
 
-        <script src="/dist/rename-me.js"></script>
+        <div x-data='{ isOpen: false, name: ""}'>
+            <button x-on:click="isOpen = ! isOpen">toggle</button>
+
+            <div x-bind:class="{ 'hidden' : ! isOpen }" x-on:click.away="isOpen = false">
+                <p>hey, I'm a dropdown<p>
+            </div>
+        </div>
+
+        <script src="/dist/minimal.js"></script>
         <script>
-            window.rename.start()
+            window.minimal.start('hey')
         </script>
     </body>
 </html>

+ 32 - 1
package-lock.json

@@ -1,8 +1,33 @@
 {
-  "name": "rename-me",
+  "name": "minimal",
   "requires": true,
   "lockfileVersion": 1,
   "dependencies": {
+    "@babel/cli": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.7.4.tgz",
+      "integrity": "sha512-O7mmzaWdm+VabWQmxuM8hqNrWGGihN83KfhPUzp2lAW4kzIMwBxujXkZbD4fMwKMYY9FXTbDvXsJqU+5XHXi4A==",
+      "dev": true,
+      "requires": {
+        "chokidar": "^2.1.8",
+        "commander": "^4.0.1",
+        "convert-source-map": "^1.1.0",
+        "fs-readdir-recursive": "^1.1.0",
+        "glob": "^7.0.0",
+        "lodash": "^4.17.13",
+        "make-dir": "^2.1.0",
+        "slash": "^2.0.0",
+        "source-map": "^0.5.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz",
+          "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==",
+          "dev": true
+        }
+      }
+    },
     "@babel/code-frame": {
       "version": "7.5.5",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
@@ -4443,6 +4468,12 @@
         "universalify": "^0.1.0"
       }
     },
+    "fs-readdir-recursive": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+      "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==",
+      "dev": true
+    },
     "fs-write-stream-atomic": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",

+ 3 - 2
package.json

@@ -1,6 +1,6 @@
 {
-  "main": "dist/rename-me.js",
-  "name": "rename-me",
+  "main": "dist/minimal.js",
+  "name": "minimal",
   "scripts": {
     "test": "jest",
     "test:debug": "node --inspect node_modules/.bin/jest --runInBand",
@@ -12,6 +12,7 @@
   "author": "Caleb Porzio",
   "license": "MIT",
   "devDependencies": {
+    "@babel/cli": "^7.7.4",
     "@babel/core": "^7.5.5",
     "@babel/preset-env": "^7.5.5",
     "babel-jest": "^24.8.0",

+ 79 - 46
src/component.js

@@ -1,56 +1,72 @@
-import { walk, onlyUnique, saferEval, hasXAttr, getXAttrs } from './utils'
+import { walk, onlyUnique, saferEval, saferEvalNoReturn, getXAttrs, debounce } from './utils'
 
 export default class Component {
     constructor(el) {
         this.el = el
 
-        this.data = saferEval(this.el.getAttribute('x-data'))
+        this.data = saferEval(this.el.getAttribute('x-data'), {})
+
+        this.concernedData = []
 
         this.registerListeners()
 
-        this.updateBoundAttributes(() => { return true })
+        this.updateAllBoundAttributes()
     }
 
     registerListeners() {
-        // Do a sweep through the component, find out what events children are
-        // listening for so we can do "event delegation" on the root.
-        // The reason for using event delegation is so that new
-        // DOM element listeners can potentially be added
-        // and they will be detected.
-        this.eventsThisComponentIsListeningFor().forEach(eventName => {
-            this.el.addEventListener(eventName, e => {
-                if (e.target.hasAttribute(`x-on:${eventName}`)) {
-                    var mutatedDataItems = []
-
-                    // Detect if the listener action mutated some data,
-                    // this way we can selectively update bindings.
-                    const proxiedData = new Proxy(this.data, {
-                        set(obj, property, value) {
-                            const setWasSuccessful = Reflect.set(obj, property, value)
-
-                            mutatedDataItems.push(property)
-
-                            return setWasSuccessful
-                        }
-                    })
-
-                    const expression = e.target.getAttribute(`x-on:${eventName}`)
-
-                    saferEval(expression, {
-                        '$data': proxiedData,
-                        '$event': e,
+        walk(this.el, el => {
+            getXAttrs(el, 'on').forEach(({ type, value, modifiers, expression }) => {
+                if (modifiers.includes('away')) {
+                    // Listen for this event at the root level.
+                    document.addEventListener(value, e => {
+                        // Don't do anything if the click came form the element or within it.
+                        if (el.contains(e.target)) return
+
+                        // Don't do anything if this element isn't currently visible.
+                        if (el.offsetWidth < 1 && el.offsetHeight < 1) return
+
+                        // Now that we are sure the element is visible, AND the click
+                        // is from outside it, let's run the expression.
+                        this.concernedData.push(...this.evaluateExpressionWithEvent(expression, e))
+                        this.concernedData = this.concernedData.filter(onlyUnique)
+
+                        this.updateBoundAttributes()
                     })
+                } else {
+                    el.addEventListener(value, e => {
+                        this.concernedData.push(...this.evaluateExpressionWithEvent(expression, e))
+                        this.concernedData = this.concernedData.filter(onlyUnique)
 
-                    this.updateBoundAttributes(isConscernedWith => {
-                        return mutatedDataItems.filter(i => isConscernedWith.includes(i)).length > 0;
+                        this.updateBoundAttributes()
                     })
                 }
             })
         })
     }
 
-    eventsThisComponentIsListeningFor()
-    {
+    evaluateExpressionWithEvent(expression, event) {
+        var mutatedDataItems = []
+
+        // Detect if the listener action mutated some data,
+        // this way we can selectively update bindings.
+        const proxiedData = new Proxy(this.data, {
+            set(obj, property, value) {
+                const setWasSuccessful = Reflect.set(obj, property, value)
+
+                mutatedDataItems.push(property)
+
+                return setWasSuccessful
+            }
+        })
+
+        saferEvalNoReturn(expression, proxiedData, {
+            '$event': event,
+        })
+
+        return mutatedDataItems;
+    }
+
+    eventsThisComponentIsListeningFor() {
         var eventsToListenFor = []
 
         walk(this.el, el => {
@@ -65,12 +81,32 @@ export default class Component {
         return eventsToListenFor.filter(onlyUnique)
     }
 
-    updateBoundAttributes(ifConcernedWith) {
-        walk(this.el, el => {
-            if (hasXAttr(el, 'bind')) {
-                getXAttrs(el, 'bind').forEach(attr => {
-                    const boundAttribute = attr.name.replace(/x-bind:/, '')
-                    const expression = attr.value
+    updateBoundAttributes() {
+        var self = this
+        debounce(walk, 5)(this.el, function (el) {
+            getXAttrs(el, 'bind').forEach(({ type, value, modifiers, expression }) => {
+                var isConscernedWith = []
+
+                const proxiedData = new Proxy(self.data, {
+                    get(object, prop) {
+                        isConscernedWith.push(prop)
+
+                        return object[prop]
+                    }
+                })
+
+                const result = saferEval(expression, proxiedData)
+
+                if (self.concernedData.filter(i => isConscernedWith.includes(i)).length > 0) {
+                    self.updateBoundAttributeValue(el, value, result)
+                }
+            })
+        })
+    }
+
+    updateAllBoundAttributes() {
+            walk(this.el, el => {
+                getXAttrs(el, 'bind').forEach(({ type, value, modifiers, expression }) => {
                     var isConscernedWith = []
 
                     const proxiedData = new Proxy(this.data, {
@@ -81,14 +117,11 @@ export default class Component {
                         }
                     })
 
-                    const result = saferEval(expression, {"$data":  proxiedData})
+                    const result = saferEval(expression, proxiedData)
 
-                    if (ifConcernedWith(isConscernedWith)) {
-                        this.updateBoundAttributeValue(el, boundAttribute, result)
-                    }
+                    this.updateBoundAttributeValue(el, value, result)
                 })
-            }
-        })
+            })
     }
 
     updateBoundAttributeValue(el, attrName, value) {

+ 5 - 4
src/index.js

@@ -1,6 +1,7 @@
+/* @flow */
 import Component from './component'
 
-var rename = {
+const minimal = {
     start: function () {
         const rootEls = document.querySelectorAll('[x-data]');
 
@@ -10,8 +11,8 @@ var rename = {
     }
 }
 
-if (! window.rename) {
-    window.rename = rename
+if (! window.minimal) {
+    window.minimal = minimal
 }
 
-export default rename
+export default minimal

+ 97 - 0
src/something.js

@@ -0,0 +1,97 @@
+/* @flow */
+
+const validDivisionCharRE = /[\w).+\-_$\]]/
+
+export function parseFilters (exp: string): string {
+  let inSingle = false
+  let inDouble = false
+  let inTemplateString = false
+  let inRegex = false
+  let curly = 0
+  let square = 0
+  let paren = 0
+  let lastFilterIndex = 0
+  let c, prev, i, expression, filters
+
+  for (i = 0; i < exp.length; i++) {
+    prev = c
+    c = exp.charCodeAt(i)
+    if (inSingle) {
+      if (c === 0x27 && prev !== 0x5C) inSingle = false
+    } else if (inDouble) {
+      if (c === 0x22 && prev !== 0x5C) inDouble = false
+    } else if (inTemplateString) {
+      if (c === 0x60 && prev !== 0x5C) inTemplateString = false
+    } else if (inRegex) {
+      if (c === 0x2f && prev !== 0x5C) inRegex = false
+    } else if (
+      c === 0x7C && // pipe
+      exp.charCodeAt(i + 1) !== 0x7C &&
+      exp.charCodeAt(i - 1) !== 0x7C &&
+      !curly && !square && !paren
+    ) {
+      if (expression === undefined) {
+        // first filter, end of expression
+        lastFilterIndex = i + 1
+        expression = exp.slice(0, i).trim()
+      } else {
+        pushFilter()
+      }
+    } else {
+      switch (c) {
+        case 0x22: inDouble = true; break         // "
+        case 0x27: inSingle = true; break         // '
+        case 0x60: inTemplateString = true; break // `
+        case 0x28: paren++; break                 // (
+        case 0x29: paren--; break                 // )
+        case 0x5B: square++; break                // [
+        case 0x5D: square--; break                // ]
+        case 0x7B: curly++; break                 // {
+        case 0x7D: curly--; break                 // }
+      }
+      if (c === 0x2f) { // /
+        let j = i - 1
+        let p
+        // find first non-whitespace prev char
+        for (; j >= 0; j--) {
+          p = exp.charAt(j)
+          if (p !== ' ') break
+        }
+        if (!p || !validDivisionCharRE.test(p)) {
+          inRegex = true
+        }
+      }
+    }
+  }
+
+  if (expression === undefined) {
+    expression = exp.slice(0, i).trim()
+  } else if (lastFilterIndex !== 0) {
+    pushFilter()
+  }
+
+  function pushFilter () {
+    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
+    lastFilterIndex = i + 1
+  }
+
+  if (filters) {
+    for (i = 0; i < filters.length; i++) {
+      expression = wrapFilter(expression, filters[i])
+    }
+  }
+
+  return expression
+}
+
+function wrapFilter (exp: string, filter: string): string {
+  const i = filter.indexOf('(')
+  if (i < 0) {
+    // _f: resolveFilter
+    return `_f("${filter}")(${exp})`
+  } else {
+    const name = filter.slice(0, i)
+    const args = filter.slice(i + 1)
+    return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
+  }
+}

+ 42 - 9
src/utils.js

@@ -10,24 +10,57 @@ export function walk(el, callback) {
     }
 }
 
+export function debounce(func, wait, immediate) {
+    var timeout;
+    return function () {
+        var context = this, args = arguments;
+        var later = function () {
+            timeout = null;
+            if (!immediate) func.apply(context, args);
+        };
+        var callNow = immediate && !timeout;
+        clearTimeout(timeout);
+        timeout = setTimeout(later, wait);
+        if (callNow) func.apply(context, args);
+    };
+};
+
 export function onlyUnique(value, index, self) {
     return self.indexOf(value) === index;
 }
 
-export function saferEval(expression, scope = {}) {
-    return (new Function(Object.keys(scope), `"use strict"; return ${expression}`))(
-        ...Object.values(scope)
+export function saferEval(expression, dataContext, additionalHelperVariables = {}) {
+    return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `var result; with($data) { result = ${expression} }; return result`))(
+        dataContext, ...Object.values(additionalHelperVariables)
+    )
+}
+
+export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
+    return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `with($data) { ${expression} }`))(
+        dataContext, ...Object.values(additionalHelperVariables)
     )
 }
 
-export function hasXAttr(el, name) {
-    return !! Array.from(el.attributes)
-        .map(i => i.name)
-        .filter(i => i.search(new RegExp(`^x-${name}`)) > -1)
-        .length
+export function isXAttr(attr) {
+    const xAttrRE = /x-(on|bind|data):/
+
+    return xAttrRE.test(attr.name)
 }
 
 export function getXAttrs(el, name) {
     return Array.from(el.attributes)
-        .filter(i => i.name.search(new RegExp(`^x-${name}`)) > -1)
+        .filter(isXAttr)
+        .map(attr => {
+            const typeMatch = attr.name.match(/x-(on|bind|data):/)
+            const valueMatch = attr.name.match(/:([a-zA-Z\-]+)/)
+            const modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
+
+            return {
+                type: typeMatch ? typeMatch[1] : null,
+                value: valueMatch ? valueMatch[1] : null,
+                modifiers: modifiers.map(i => i.replace('.', '')),
+                expression: attr.value,
+            }
+        })
+        .filter(i => i.type === name)
 }

+ 70 - 31
test/test.spec.js

@@ -1,13 +1,14 @@
-import rename from 'rename-me'
+import minimal from 'minimal'
+import { wait } from 'dom-testing-library'
 
 test('attribute bindings are set on initialize', async () => {
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">
-            <span x-bind:foo="$data.foo"></span>
+            <span x-bind:foo="foo"></span>
         </div>
     `
 
-    rename.start()
+    minimal.start()
 
     expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
 })
@@ -15,11 +16,11 @@ test('attribute bindings are set on initialize', async () => {
 test('class attribute bindings are removed by object syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{ isOn: false }">
-            <span class="foo" x-bind:class="{ 'foo': $data.isOn }"></span>
+            <span class="foo" x-bind:class="{ 'foo': isOn }"></span>
         </div>
     `
 
-    rename.start()
+    minimal.start()
 
     expect(document.querySelector('span').classList.contains('foo')).toBeFalsy
 })
@@ -27,11 +28,11 @@ test('class attribute bindings are removed by object syntax', async () => {
 test('class attribute bindings are added by object syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{ isOn: true }">
-            <span x-bind:class="{ 'foo': $data.isOn }"></span>
+            <span x-bind:class="{ 'foo': isOn }"></span>
         </div>
     `
 
-    rename.start()
+    minimal.start()
 
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy
 })
@@ -39,14 +40,14 @@ test('class attribute bindings are added by object syntax', async () => {
 test('boolean attributes set to false are removed from element', async () => {
     document.body.innerHTML = `
         <div x-data="{ isSet: false }">
-            <input x-bind:disabled="$data.isSet"></input>
-            <input x-bind:checked="$data.isSet"></input>
-            <input x-bind:required="$data.isSet"></input>
-            <input x-bind:readonly="$data.isSet"></input>
+            <input x-bind:disabled="isSet"></input>
+            <input x-bind:checked="isSet"></input>
+            <input x-bind:required="isSet"></input>
+            <input x-bind:readonly="isSet"></input>
         </div>
     `
 
-    rename.start()
+    minimal.start()
 
     expect(document.querySelectorAll('input')[0].disabled).toBeFalsy()
     expect(document.querySelectorAll('input')[1].checked).toBeFalsy()
@@ -57,14 +58,14 @@ test('boolean attributes set to false are removed from element', async () => {
 test('boolean attributes set to true are added to element', async () => {
     document.body.innerHTML = `
         <div x-data="{ isSet: true }">
-            <input x-bind:disabled="$data.isSet"></input>
-            <input x-bind:checked="$data.isSet"></input>
-            <input x-bind:required="$data.isSet"></input>
-            <input x-bind:readonly="$data.isSet"></input>
+            <input x-bind:disabled="isSet"></input>
+            <input x-bind:checked="isSet"></input>
+            <input x-bind:required="isSet"></input>
+            <input x-bind:readonly="isSet"></input>
         </div>
     `
 
-    rename.start()
+    minimal.start()
 
     expect(document.querySelectorAll('input')[0].disabled).toBeTruthy()
     expect(document.querySelectorAll('input')[1].checked).toBeTruthy()
@@ -75,44 +76,82 @@ test('boolean attributes set to true are added to element', async () => {
 test('data modified in event listener updates effected attribute bindings', async () => {
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">
-            <button x-on:click="$data.foo = 'baz'"></button>
+            <button x-on:click="foo = 'baz'"></button>
 
-            <span x-bind:foo="$data.foo"></span>
+            <span x-bind:foo="foo"></span>
         </div>
     `
 
-    rename.start()
+    minimal.start()
 
     expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
 
     document.querySelector('button').click()
 
-    expect(document.querySelector('span').getAttribute('foo')).toEqual('baz')
+    await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
 })
 
 test('data modified in event listener doesnt update uneffected attribute bindings', async () => {
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar', count: 0 }">
-            <button x-on:click="$data.foo = 'baz'"></button>
-            <button x-on:click="$data.count++"></button>
+            <button x-on:click="foo = 'baz'"></button>
+            <button x-on:click="count++"></button>
 
-            <span x-bind:value="$data.foo"></span>
-            <span x-bind:value="$data.count++"></span>
+            <span x-bind:value="foo"></span>
+            <span x-bind:value="count++"></span>
         </div>
     `
 
-    rename.start()
+    minimal.start()
 
     expect(document.querySelectorAll('span')[0].getAttribute('value')).toEqual('bar')
     expect(document.querySelectorAll('span')[1].getAttribute('value')).toEqual('0')
 
     document.querySelectorAll('button')[0].click()
 
-    expect(document.querySelectorAll('span')[0].getAttribute('value')).toEqual('baz')
-    expect(document.querySelectorAll('span')[1].getAttribute('value')).toEqual('0')
+    await wait(async () => {
+        expect(document.querySelectorAll('span')[0].getAttribute('value')).toEqual('baz')
+        expect(document.querySelectorAll('span')[1].getAttribute('value')).toEqual('0')
+
+        document.querySelectorAll('button')[1].click()
+
+        await wait(() => {
+            expect(document.querySelectorAll('span')[0].getAttribute('value')).toEqual('baz')
+            expect(document.querySelectorAll('span')[1].getAttribute('value')).toEqual('3')
+        })
+    })
+})
+
+test('click away', async () => {
+    document.body.innerHTML = `
+        <div id="outer">
+            <div x-data="{ isOpen: true }">
+                <button x-on:click="isOpen = true"></button>
+
+                <ul x-bind:value="isOpen" x-on:click.away="isOpen = false">
+                    <li>...</li>
+                </ul>
+            </div>
+        </div>
+    `
+
+    minimal.start()
 
-    document.querySelectorAll('button')[1].click()
+    expect(document.querySelector('ul').getAttribute('value')).toEqual('true')
+
+    document.querySelector('li').click()
+
+    await wait(() => { expect(document.querySelector('ul').getAttribute('value')).toEqual('true') })
+
+    document.querySelector('ul').click()
+
+    await wait(() => { expect(document.querySelector('ul').getAttribute('value')).toEqual('true') })
+
+    document.querySelector('#outer').click()
+
+    await wait(() => { expect(document.querySelector('ul').getAttribute('value')).toEqual('false') })
+
+    document.querySelector('button').click()
 
-    expect(document.querySelectorAll('span')[0].getAttribute('value')).toEqual('baz')
-    expect(document.querySelectorAll('span')[1].getAttribute('value')).toEqual('3')
+    await wait(() => { expect(document.querySelector('ul').getAttribute('value')).toEqual('true') })
 })