Browse Source

Merge pull request #515 from alpinejs/add-x-bind

Add `x-bind`
Caleb Porzio 5 years ago
parent
commit
afdc71c8ca
9 changed files with 527 additions and 168 deletions
  1. 222 77
      dist/alpine-ie11.js
  2. 72 50
      dist/alpine.js
  3. 21 0
      examples/index.html
  4. 3 3
      src/component.js
  5. 10 8
      src/directives/for.js
  6. 2 2
      src/directives/if.js
  7. 2 2
      src/directives/show.js
  8. 54 26
      src/utils.js
  9. 141 0
      test/bind.spec.js

+ 222 - 77
dist/alpine-ie11.js

@@ -2347,6 +2347,10 @@
     }
   }
 
+  function _slicedToArray(arr, i) {
+    return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
+  }
+
   function _toConsumableArray(arr) {
     return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
   }
@@ -2359,14 +2363,52 @@
     }
   }
 
+  function _arrayWithHoles(arr) {
+    if (Array.isArray(arr)) return arr;
+  }
+
   function _iterableToArray(iter) {
     if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
   }
 
+  function _iterableToArrayLimit(arr, i) {
+    if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) {
+      return;
+    }
+
+    var _arr = [];
+    var _n = true;
+    var _d = false;
+    var _e = undefined;
+
+    try {
+      for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
+        _arr.push(_s.value);
+
+        if (i && _arr.length === i) break;
+      }
+    } catch (err) {
+      _d = true;
+      _e = err;
+    } finally {
+      try {
+        if (!_n && _i["return"] != null) _i["return"]();
+      } finally {
+        if (_d) throw _e;
+      }
+    }
+
+    return _arr;
+  }
+
   function _nonIterableSpread() {
     throw new TypeError("Invalid attempt to spread non-iterable instance");
   }
 
+  function _nonIterableRest() {
+    throw new TypeError("Invalid attempt to destructure non-iterable instance");
+  }
+
   var runtime_1 = createCommonjsModule(function (module) {
   /**
    * Copyright (c) 2014-present, Facebook, Inc.
@@ -4483,6 +4525,48 @@
   // https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables
   addToUnscopables(FIND);
 
+  // `FlattenIntoArray` abstract operation
+  // https://tc39.github.io/proposal-flatMap/#sec-FlattenIntoArray
+  var flattenIntoArray = function (target, original, source, sourceLen, start, depth, mapper, thisArg) {
+    var targetIndex = start;
+    var sourceIndex = 0;
+    var mapFn = mapper ? functionBindContext(mapper, thisArg, 3) : false;
+    var element;
+
+    while (sourceIndex < sourceLen) {
+      if (sourceIndex in source) {
+        element = mapFn ? mapFn(source[sourceIndex], sourceIndex, original) : source[sourceIndex];
+
+        if (depth > 0 && isArray(element)) {
+          targetIndex = flattenIntoArray(target, original, element, toLength(element.length), targetIndex, depth - 1) - 1;
+        } else {
+          if (targetIndex >= 0x1FFFFFFFFFFFFF) throw TypeError('Exceed the acceptable array length');
+          target[targetIndex] = element;
+        }
+
+        targetIndex++;
+      }
+      sourceIndex++;
+    }
+    return targetIndex;
+  };
+
+  var flattenIntoArray_1 = flattenIntoArray;
+
+  // `Array.prototype.flatMap` method
+  // https://github.com/tc39/proposal-flatMap
+  _export({ target: 'Array', proto: true }, {
+    flatMap: function flatMap(callbackfn /* , thisArg */) {
+      var O = toObject(this);
+      var sourceLen = toLength(O.length);
+      var A;
+      aFunction$1(callbackfn);
+      A = arraySpeciesCreate(O, 0);
+      A.length = flattenIntoArray_1(A, O, O, sourceLen, 0, 1, callbackfn, arguments.length > 1 ? arguments[1] : undefined);
+      return A;
+    }
+  });
+
   var $indexOf = arrayIncludes.indexOf;
 
 
@@ -4564,6 +4648,12 @@
     }
   });
 
+  // this method was added to unscopables after implementation
+  // in popular engines, so it's moved to a separate module
+
+
+  addToUnscopables('flatMap');
+
   var defineProperty$2 = objectDefineProperty.f;
 
   var FunctionPrototype = Function.prototype;
@@ -4729,6 +4819,16 @@
     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;
 
   // `Object.values` method
@@ -5379,13 +5479,22 @@
   }
   function saferEval(expression, dataContext) {
     var additionalHelperVariables = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
+
+    if (typeof expression === 'function') {
+      return expression.call(dataContext);
+    }
+
     return new Function(['$data'].concat(_toConsumableArray(Object.keys(additionalHelperVariables))), "var __alpine_result; with($data) { __alpine_result = ".concat(expression, " }; return __alpine_result")).apply(void 0, [dataContext].concat(_toConsumableArray(Object.values(additionalHelperVariables))));
   }
   function saferEvalNoReturn(expression, dataContext) {
     var additionalHelperVariables = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
 
-    // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
+    if (typeof expression === 'function') {
+      expression.call(dataContext);
+    } // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
     // Where "foo" is a function. Also, we'll pass the function the event instance when we call it.
+
+
     if (Object.keys(dataContext).includes(expression)) {
       var methodReference = new Function(['dataContext'].concat(_toConsumableArray(Object.keys(additionalHelperVariables))), "with(dataContext) { return ".concat(expression, " }")).apply(void 0, [dataContext].concat(_toConsumableArray(Object.values(additionalHelperVariables))));
 
@@ -5401,28 +5510,31 @@
     var name = replaceAtAndColonWithStandardSyntax(attr.name);
     return xAttrRE.test(name);
   }
-  function getXAttrs(el, type) {
+  function getXAttrs(el, type, component) {
     var _this2 = this;
 
-    return Array.from(el.attributes).filter(isXAttr).map(function (attr) {
+    return Array.from(el.attributes).filter(isXAttr).map(parseHtmlAttribute).flatMap(function (i) {
       var _this3 = this;
 
       _newArrowCheck(this, _this2);
 
-      var name = replaceAtAndColonWithStandardSyntax(attr.name);
-      var typeMatch = name.match(xAttrRE);
-      var valueMatch = name.match(/:([a-zA-Z\-:]+)/);
-      var modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
-      return {
-        type: typeMatch ? typeMatch[1] : null,
-        value: valueMatch ? valueMatch[1] : null,
-        modifiers: modifiers.map(function (i) {
+      if (i.type === 'bind' && i.value === null) {
+        var directiveBindings = saferEval(i.expression, component.$data);
+        return Object.entries(directiveBindings).map(function (_ref) {
+          var _ref2 = _slicedToArray(_ref, 2),
+              name = _ref2[0],
+              value = _ref2[1];
+
           _newArrowCheck(this, _this3);
 
-          return i.replace('.', '');
-        }.bind(this)),
-        expression: attr.value
-      };
+          return parseHtmlAttribute({
+            name: name,
+            value: value
+          });
+        }.bind(this));
+      } else {
+        return i;
+      }
     }.bind(this)).filter(function (i) {
       _newArrowCheck(this, _this2);
 
@@ -5431,6 +5543,28 @@
       return i.type === type;
     }.bind(this));
   }
+
+  function parseHtmlAttribute(_ref3) {
+    var _this4 = this;
+
+    var name = _ref3.name,
+        value = _ref3.value;
+    var normalizedName = replaceAtAndColonWithStandardSyntax(name);
+    var typeMatch = normalizedName.match(xAttrRE);
+    var valueMatch = normalizedName.match(/:([a-zA-Z\-:]+)/);
+    var modifiers = normalizedName.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
+    return {
+      type: typeMatch ? typeMatch[1] : null,
+      value: valueMatch ? valueMatch[1] : null,
+      modifiers: modifiers.map(function (i) {
+        _newArrowCheck(this, _this4);
+
+        return i.replace('.', '');
+      }.bind(this)),
+      expression: value
+    };
+  }
+
   function isBooleanAttr(attrName) {
     // As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
     // Array roughly ordered by estimated usage
@@ -5446,14 +5580,14 @@
 
     return name;
   }
-  function transitionIn(el, show) {
-    var _this4 = this;
+  function transitionIn(el, show, component) {
+    var _this5 = this;
 
-    var forceSkip = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+    var forceSkip = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
     // We don't want to transition on the initial page load.
     if (forceSkip) return show();
-    var attrs = getXAttrs(el, 'transition');
-    var showAttr = getXAttrs(el, 'show')[0]; // If this is triggered by a x-show.transition.
+    var attrs = getXAttrs(el, 'transition', component);
+    var showAttr = getXAttrs(el, 'show', component)[0]; // If this is triggered by a x-show.transition.
 
     if (showAttr && showAttr.modifiers.includes('transition')) {
       var modifiers = showAttr.modifiers; // If x-show.transition.out, we'll skip the "in" transition.
@@ -5462,13 +5596,13 @@
       var settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out'); // If x-show.transition.in...out... only use "in" related modifiers for this transition.
 
       modifiers = settingBothSidesOfTransition ? modifiers.filter(function (i, index) {
-        _newArrowCheck(this, _this4);
+        _newArrowCheck(this, _this5);
 
         return index < modifiers.indexOf('out');
       }.bind(this)) : modifiers;
       transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter.
     } else if (attrs.filter(function (attr) {
-      _newArrowCheck(this, _this4);
+      _newArrowCheck(this, _this5);
 
       return ['enter', 'enter-start', 'enter-end'].includes(attr.value);
     }.bind(this)).length > 0) {
@@ -5478,26 +5612,26 @@
       show();
     }
   }
-  function transitionOut(el, hide) {
-    var _this5 = this;
+  function transitionOut(el, hide, component) {
+    var _this6 = this;
 
-    var forceSkip = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+    var forceSkip = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
     if (forceSkip) return hide();
-    var attrs = getXAttrs(el, 'transition');
-    var showAttr = getXAttrs(el, 'show')[0];
+    var attrs = getXAttrs(el, 'transition', component);
+    var showAttr = getXAttrs(el, 'show', component)[0];
 
     if (showAttr && showAttr.modifiers.includes('transition')) {
       var modifiers = showAttr.modifiers;
       if (modifiers.includes('in') && !modifiers.includes('out')) return hide();
       var settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out');
       modifiers = settingBothSidesOfTransition ? modifiers.filter(function (i, index) {
-        _newArrowCheck(this, _this5);
+        _newArrowCheck(this, _this6);
 
         return index > modifiers.indexOf('out');
       }.bind(this)) : modifiers;
       transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
     } else if (attrs.filter(function (attr) {
-      _newArrowCheck(this, _this5);
+      _newArrowCheck(this, _this6);
 
       return ['leave', 'leave-start', 'leave-end'].includes(attr.value);
     }.bind(this)).length > 0) {
@@ -5507,7 +5641,7 @@
     }
   }
   function transitionHelperIn(el, modifiers, showCallback) {
-    var _this6 = this;
+    var _this7 = this;
 
     // Default values inspired by: https://material.io/design/motion/speed.html#duration
     var styleValues = {
@@ -5523,11 +5657,11 @@
       }
     };
     transitionHelper(el, modifiers, showCallback, function () {
-      _newArrowCheck(this, _this6);
+      _newArrowCheck(this, _this7);
     }.bind(this), styleValues);
   }
   function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
-    var _this7 = this;
+    var _this8 = this;
 
     // Make the "out" transition .5x slower than the "in". (Visually better)
     // HOWEVER, if they explicitly set a duration for the "out" transition,
@@ -5546,7 +5680,7 @@
       }
     };
     transitionHelper(el, modifiers, function () {
-      _newArrowCheck(this, _this7);
+      _newArrowCheck(this, _this8);
     }.bind(this), hideCallback, styleValues);
   }
 
@@ -5625,83 +5759,83 @@
     transition(el, stages);
   }
   function transitionClassesIn(el, directives, showCallback) {
-    var _this8 = this;
+    var _this9 = this;
 
     var enter = (directives.find(function (i) {
-      _newArrowCheck(this, _this8);
+      _newArrowCheck(this, _this9);
 
       return i.value === 'enter';
     }.bind(this)) || {
       expression: ''
     }).expression.split(' ').filter(function (i) {
-      _newArrowCheck(this, _this8);
+      _newArrowCheck(this, _this9);
 
       return i !== '';
     }.bind(this));
     var enterStart = (directives.find(function (i) {
-      _newArrowCheck(this, _this8);
+      _newArrowCheck(this, _this9);
 
       return i.value === 'enter-start';
     }.bind(this)) || {
       expression: ''
     }).expression.split(' ').filter(function (i) {
-      _newArrowCheck(this, _this8);
+      _newArrowCheck(this, _this9);
 
       return i !== '';
     }.bind(this));
     var enterEnd = (directives.find(function (i) {
-      _newArrowCheck(this, _this8);
+      _newArrowCheck(this, _this9);
 
       return i.value === 'enter-end';
     }.bind(this)) || {
       expression: ''
     }).expression.split(' ').filter(function (i) {
-      _newArrowCheck(this, _this8);
+      _newArrowCheck(this, _this9);
 
       return i !== '';
     }.bind(this));
     transitionClasses(el, enter, enterStart, enterEnd, showCallback, function () {
-      _newArrowCheck(this, _this8);
+      _newArrowCheck(this, _this9);
     }.bind(this));
   }
   function transitionClassesOut(el, directives, hideCallback) {
-    var _this9 = this;
+    var _this10 = this;
 
     var leave = (directives.find(function (i) {
-      _newArrowCheck(this, _this9);
+      _newArrowCheck(this, _this10);
 
       return i.value === 'leave';
     }.bind(this)) || {
       expression: ''
     }).expression.split(' ').filter(function (i) {
-      _newArrowCheck(this, _this9);
+      _newArrowCheck(this, _this10);
 
       return i !== '';
     }.bind(this));
     var leaveStart = (directives.find(function (i) {
-      _newArrowCheck(this, _this9);
+      _newArrowCheck(this, _this10);
 
       return i.value === 'leave-start';
     }.bind(this)) || {
       expression: ''
     }).expression.split(' ').filter(function (i) {
-      _newArrowCheck(this, _this9);
+      _newArrowCheck(this, _this10);
 
       return i !== '';
     }.bind(this));
     var leaveEnd = (directives.find(function (i) {
-      _newArrowCheck(this, _this9);
+      _newArrowCheck(this, _this10);
 
       return i.value === 'leave-end';
     }.bind(this)) || {
       expression: ''
     }).expression.split(' ').filter(function (i) {
-      _newArrowCheck(this, _this9);
+      _newArrowCheck(this, _this10);
 
       return i !== '';
     }.bind(this));
     transitionClasses(el, leave, leaveStart, leaveEnd, function () {
-      _newArrowCheck(this, _this9);
+      _newArrowCheck(this, _this10);
     }.bind(this), hideCallback);
   }
   function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2) {
@@ -5722,12 +5856,12 @@
       },
       end: function end() {
         var _el$classList3,
-            _this10 = this,
+            _this11 = this,
             _el$classList4;
 
         // Don't remove classes that were in the original class attribute.
         (_el$classList3 = el.classList).remove.apply(_el$classList3, _toConsumableArray(classesStart.filter(function (i) {
-          _newArrowCheck(this, _this10);
+          _newArrowCheck(this, _this11);
 
           return !originalClasses.includes(i);
         }.bind(this))));
@@ -5739,17 +5873,17 @@
       },
       cleanup: function cleanup() {
         var _el$classList5,
-            _this11 = this,
+            _this12 = this,
             _el$classList6;
 
         (_el$classList5 = el.classList).remove.apply(_el$classList5, _toConsumableArray(classesDuring.filter(function (i) {
-          _newArrowCheck(this, _this11);
+          _newArrowCheck(this, _this12);
 
           return !originalClasses.includes(i);
         }.bind(this))));
 
         (_el$classList6 = el.classList).remove.apply(_el$classList6, _toConsumableArray(classesEnd.filter(function (i) {
-          _newArrowCheck(this, _this11);
+          _newArrowCheck(this, _this12);
 
           return !originalClasses.includes(i);
         }.bind(this))));
@@ -5758,14 +5892,14 @@
     transition(el, stages);
   }
   function transition(el, stages) {
-    var _this12 = this;
+    var _this13 = this;
 
     stages.start();
     stages.during();
     requestAnimationFrame(function () {
-      var _this13 = this;
+      var _this14 = this;
 
-      _newArrowCheck(this, _this12);
+      _newArrowCheck(this, _this13);
 
       // Note: Safari's transitionDuration property will list out comma separated transition durations
       // for every single transition property. Let's grab the first one and call it a day.
@@ -5777,13 +5911,13 @@
 
       stages.show();
       requestAnimationFrame(function () {
-        var _this14 = this;
+        var _this15 = this;
 
-        _newArrowCheck(this, _this13);
+        _newArrowCheck(this, _this14);
 
         stages.end();
         setTimeout(function () {
-          _newArrowCheck(this, _this14);
+          _newArrowCheck(this, _this15);
 
           stages.hide(); // Adding an "isConnected" check, in case the callback
           // removed the element from the DOM.
@@ -5821,7 +5955,7 @@
 
         transitionIn(nextEl, function () {
           _newArrowCheck(this, _this2);
-        }.bind(this), initialUpdate);
+        }.bind(this), component, initialUpdate);
         nextEl.__x_for = iterationScopeVariables;
         component.initializeElements(nextEl, function () {
           _newArrowCheck(this, _this2);
@@ -5882,7 +6016,7 @@
   function generateKeyForIteration(component, el, index, iterationScopeVariables) {
     var _this3 = this;
 
-    var bindKeyAttribute = getXAttrs(el, 'bind').filter(function (attr) {
+    var bindKeyAttribute = getXAttrs(el, 'bind', component).filter(function (attr) {
       _newArrowCheck(this, _this3);
 
       return attr.value === 'key';
@@ -5901,7 +6035,7 @@
   }
 
   function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, el, iteratorNames, extraVars) {
-    var ifAttribute = getXAttrs(el, 'if')[0];
+    var ifAttribute = getXAttrs(el, 'if', component)[0];
 
     if (ifAttribute && !component.evaluateReturnExpression(el, ifAttribute.expression)) {
       return [];
@@ -5946,7 +6080,7 @@
         _newArrowCheck(this, _this4);
 
         nextElementFromOldLoopImmutable.remove();
-      }.bind(this));
+      }.bind(this), component);
       nextElementFromOldLoop = nextSibling && nextSibling.__x_for_key !== undefined ? nextSibling : false;
     };
 
@@ -5955,6 +6089,21 @@
     }
   }
 
+  var $some = arrayIteration.some;
+
+
+
+  var STRICT_METHOD$4 = arrayMethodIsStrict('some');
+  var USES_TO_LENGTH$9 = arrayMethodUsesToLength('some');
+
+  // `Array.prototype.some` method
+  // https://tc39.github.io/ecma262/#sec-array.prototype.some
+  _export({ target: 'Array', proto: true, forced: !STRICT_METHOD$4 || !USES_TO_LENGTH$9 }, {
+    some: function some(callbackfn /* , thisArg */) {
+      return $some(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined);
+    }
+  });
+
   function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType) {
     var _this = this;
 
@@ -5980,15 +6129,11 @@
           // I'm purposely not using Array.includes here because it's
           // strict, and because of Numeric/String mis-casting, I
           // want the "includes" to be "fuzzy".
-          var valueFound = false;
-          value.forEach(function (val) {
+          el.checked = value.some(function (val) {
             _newArrowCheck(this, _this);
 
-            if (val == el.value) {
-              valueFound = true;
-            }
+            return val == el.value;
           }.bind(this));
-          el.checked = valueFound;
         } else {
           el.checked = !!value;
         } // If we are explicitly binding a string to the :value, set the string,
@@ -6128,7 +6273,7 @@
 
               hide();
             }.bind(this));
-          }.bind(this));
+          }.bind(this), component);
         } else {
           resolve(function () {
             _newArrowCheck(this, _this2);
@@ -6140,7 +6285,7 @@
             _newArrowCheck(this, _this2);
 
             show();
-          }.bind(this));
+          }.bind(this), component);
         } // Resolve immediately, only hold up parent `x-show`s for hidin.
 
 
@@ -6186,7 +6331,7 @@
       el.parentElement.insertBefore(clone, el.nextElementSibling);
       transitionIn(el.nextElementSibling, function () {
         _newArrowCheck(this, _this);
-      }.bind(this), initialUpdate);
+      }.bind(this), component, initialUpdate);
       component.initializeElements(el.nextElementSibling, extraVars);
       el.nextElementSibling.__x_inserted_me = true;
     } else if (!expressionResult && elementHasAlreadyBeenAdded) {
@@ -6194,7 +6339,7 @@
         _newArrowCheck(this, _this);
 
         el.nextElementSibling.remove();
-      }.bind(this), initialUpdate);
+      }.bind(this), component, initialUpdate);
     }
   }
 
@@ -6713,7 +6858,7 @@
       value: function initializeElement(el, extraVars) {
         // To support class attribute merging, we have to know what the element's
         // original class attribute looked like for reference.
-        if (el.hasAttribute('class') && getXAttrs(el).length > 0) {
+        if (el.hasAttribute('class') && getXAttrs(el, undefined, this).length > 0) {
           el.__x_original_classes = el.getAttribute('class').split(' ');
         }
 
@@ -6810,7 +6955,7 @@
       value: function registerListeners(el, extraVars) {
         var _this14 = this;
 
-        getXAttrs(el).forEach(function (_ref) {
+        getXAttrs(el, undefined, this).forEach(function (_ref) {
           var type = _ref.type,
               value = _ref.value,
               modifiers = _ref.modifiers,
@@ -6836,7 +6981,7 @@
 
         var initialUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
         var extraVars = arguments.length > 2 ? arguments[2] : undefined;
-        var attrs = getXAttrs(el);
+        var attrs = getXAttrs(el, undefined, this);
 
         if (el.type !== undefined && el.type === 'radio') {
           // If there's an x-model on a radio input, move it to end of attribute list

+ 72 - 50
dist/alpine.js

@@ -98,11 +98,19 @@
     };
   }
   function saferEval(expression, dataContext, additionalHelperVariables = {}) {
+    if (typeof expression === 'function') {
+      return expression.call(dataContext);
+    }
+
     return new Function(['$data', ...Object.keys(additionalHelperVariables)], `var __alpine_result; with($data) { __alpine_result = ${expression} }; return __alpine_result`)(dataContext, ...Object.values(additionalHelperVariables));
   }
   function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
-    // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
+    if (typeof expression === 'function') {
+      return expression.call(dataContext);
+    } // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
     // Where "foo" is a function. Also, we'll pass the function the event instance when we call it.
+
+
     if (Object.keys(dataContext).includes(expression)) {
       let methodReference = new Function(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { return ${expression} }`)(dataContext, ...Object.values(additionalHelperVariables));
 
@@ -118,24 +126,40 @@
     const name = replaceAtAndColonWithStandardSyntax(attr.name);
     return xAttrRE.test(name);
   }
-  function getXAttrs(el, type) {
-    return Array.from(el.attributes).filter(isXAttr).map(attr => {
-      const name = replaceAtAndColonWithStandardSyntax(attr.name);
-      const typeMatch = name.match(xAttrRE);
-      const valueMatch = name.match(/:([a-zA-Z\-:]+)/);
-      const modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
-      return {
-        type: typeMatch ? typeMatch[1] : null,
-        value: valueMatch ? valueMatch[1] : null,
-        modifiers: modifiers.map(i => i.replace('.', '')),
-        expression: attr.value
-      };
+  function getXAttrs(el, component, type) {
+    return Array.from(el.attributes).filter(isXAttr).map(parseHtmlAttribute).flatMap(i => {
+      if (i.type === 'bind' && i.value === null) {
+        let directiveBindings = saferEval(i.expression, component.$data);
+        return Object.entries(directiveBindings).map(([name, value]) => parseHtmlAttribute({
+          name,
+          value
+        }));
+      } else {
+        return i;
+      }
     }).filter(i => {
       // If no type is passed in for filtering, bypass filter
       if (!type) return true;
       return i.type === type;
     });
   }
+
+  function parseHtmlAttribute({
+    name,
+    value
+  }) {
+    const normalizedName = replaceAtAndColonWithStandardSyntax(name);
+    const typeMatch = normalizedName.match(xAttrRE);
+    const valueMatch = normalizedName.match(/:([a-zA-Z\-:]+)/);
+    const modifiers = normalizedName.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
+    return {
+      type: typeMatch ? typeMatch[1] : null,
+      value: valueMatch ? valueMatch[1] : null,
+      modifiers: modifiers.map(i => i.replace('.', '')),
+      expression: value
+    };
+  }
+
   function isBooleanAttr(attrName) {
     // As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
     // Array roughly ordered by estimated usage
@@ -151,11 +175,11 @@
 
     return name;
   }
-  function transitionIn(el, show, forceSkip = false) {
+  function transitionIn(el, show, component, forceSkip = false) {
     // We don't want to transition on the initial page load.
     if (forceSkip) return show();
-    const attrs = getXAttrs(el, 'transition');
-    const showAttr = getXAttrs(el, 'show')[0]; // If this is triggered by a x-show.transition.
+    const attrs = getXAttrs(el, component, 'transition');
+    const showAttr = getXAttrs(el, component, 'show')[0]; // If this is triggered by a x-show.transition.
 
     if (showAttr && showAttr.modifiers.includes('transition')) {
       let modifiers = showAttr.modifiers; // If x-show.transition.out, we'll skip the "in" transition.
@@ -166,16 +190,16 @@
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers;
       transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter.
     } else if (attrs.filter(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value)).length > 0) {
-      transitionClassesIn(el, attrs, show);
+      transitionClassesIn(el, component, attrs, show);
     } else {
       // If neither, just show that damn thing.
       show();
     }
   }
-  function transitionOut(el, hide, forceSkip = false) {
+  function transitionOut(el, hide, component, forceSkip = false) {
     if (forceSkip) return hide();
-    const attrs = getXAttrs(el, 'transition');
-    const showAttr = getXAttrs(el, 'show')[0];
+    const attrs = getXAttrs(el, component, 'transition');
+    const showAttr = getXAttrs(el, component, 'show')[0];
 
     if (showAttr && showAttr.modifiers.includes('transition')) {
       let modifiers = showAttr.modifiers;
@@ -184,7 +208,7 @@
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers;
       transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
     } else if (attrs.filter(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value)).length > 0) {
-      transitionClassesOut(el, attrs, hide);
+      transitionClassesOut(el, component, attrs, hide);
     } else {
       hide();
     }
@@ -305,19 +329,23 @@
     };
     transition(el, stages);
   }
-  function transitionClassesIn(el, directives, showCallback) {
-    const enter = (directives.find(i => i.value === 'enter') || {
+  function transitionClassesIn(el, component, directives, showCallback) {
+    let ensureStringExpression = expression => {
+      return typeof expression === 'function' ? component.evaluateReturnExpression(el, expression) : expression;
+    };
+
+    const enter = ensureStringExpression((directives.find(i => i.value === 'enter') || {
       expression: ''
-    }).expression.split(' ').filter(i => i !== '');
-    const enterStart = (directives.find(i => i.value === 'enter-start') || {
+    }).expression).split(' ').filter(i => i !== '');
+    const enterStart = ensureStringExpression((directives.find(i => i.value === 'enter-start') || {
       expression: ''
-    }).expression.split(' ').filter(i => i !== '');
-    const enterEnd = (directives.find(i => i.value === 'enter-end') || {
+    }).expression).split(' ').filter(i => i !== '');
+    const enterEnd = ensureStringExpression((directives.find(i => i.value === 'enter-end') || {
       expression: ''
-    }).expression.split(' ').filter(i => i !== '');
+    }).expression).split(' ').filter(i => i !== '');
     transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {});
   }
-  function transitionClassesOut(el, directives, hideCallback) {
+  function transitionClassesOut(el, component, directives, hideCallback) {
     const leave = (directives.find(i => i.value === 'leave') || {
       expression: ''
     }).expression.split(' ').filter(i => i !== '');
@@ -394,7 +422,7 @@
 
   function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
     warnIfNotTemplateTag(templateEl);
-    let iteratorNames = parseForExpression(expression);
+    let iteratorNames = typeof expression === 'function' ? parseForExpression(component.evaluateReturnExpression(templateEl, expression)) : parseForExpression(expression);
     let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars); // As we walk the array, we'll also walk the DOM (updating/creating as we go).
 
     let currentEl = templateEl;
@@ -406,7 +434,7 @@
       if (!nextEl) {
         nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl); // And transition it in if it's not the first page load.
 
-        transitionIn(nextEl, () => {}, initialUpdate);
+        transitionIn(nextEl, () => {}, component, initialUpdate);
         nextEl.__x_for = iterationScopeVariables;
         component.initializeElements(nextEl, () => nextEl.__x_for); // Otherwise update the element we found.
       } else {
@@ -419,7 +447,7 @@
       currentEl = nextEl;
       currentEl.__x_for_key = currentKey;
     });
-    removeAnyLeftOverElementsFromPreviousUpdate(currentEl);
+    removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component);
   } // This was taken from VueJS 2.* core. Thanks Vue!
 
   function parseForExpression(expression) {
@@ -457,7 +485,7 @@
   }
 
   function generateKeyForIteration(component, el, index, iterationScopeVariables) {
-    let bindKeyAttribute = getXAttrs(el, 'bind').filter(attr => attr.value === 'key')[0]; // If the dev hasn't specified a key, just return the index of the iteration.
+    let bindKeyAttribute = getXAttrs(el, component, 'bind').filter(attr => attr.value === 'key')[0]; // If the dev hasn't specified a key, just return the index of the iteration.
 
     if (!bindKeyAttribute) return index;
     return component.evaluateReturnExpression(el, bindKeyAttribute.expression, () => iterationScopeVariables);
@@ -468,7 +496,7 @@
   }
 
   function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, el, iteratorNames, extraVars) {
-    let ifAttribute = getXAttrs(el, 'if')[0];
+    let ifAttribute = getXAttrs(el, component, 'if')[0];
 
     if (ifAttribute && !component.evaluateReturnExpression(el, ifAttribute.expression)) {
       return [];
@@ -501,7 +529,7 @@
     }
   }
 
-  function removeAnyLeftOverElementsFromPreviousUpdate(currentEl) {
+  function removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component) {
     var nextElementFromOldLoop = currentEl.nextElementSibling && currentEl.nextElementSibling.__x_for_key !== undefined ? currentEl.nextElementSibling : false;
 
     while (nextElementFromOldLoop) {
@@ -509,7 +537,7 @@
       let nextSibling = nextElementFromOldLoop.nextElementSibling;
       transitionOut(nextElementFromOldLoop, () => {
         nextElementFromOldLoopImmutable.remove();
-      });
+      }, component);
       nextElementFromOldLoop = nextSibling && nextSibling.__x_for_key !== undefined ? nextSibling : false;
     }
   }
@@ -537,13 +565,7 @@
           // I'm purposely not using Array.includes here because it's
           // strict, and because of Numeric/String mis-casting, I
           // want the "includes" to be "fuzzy".
-          let valueFound = false;
-          value.forEach(val => {
-            if (val == el.value) {
-              valueFound = true;
-            }
-          });
-          el.checked = valueFound;
+          el.checked = value.some(val => val == el.value);
         } else {
           el.checked = !!value;
         } // If we are explicitly binding a string to the :value, set the string,
@@ -642,7 +664,7 @@
             resolve(() => {
               hide();
             });
-          });
+          }, component);
         } else {
           resolve(() => {});
         }
@@ -650,7 +672,7 @@
         if (el.style.display !== '') {
           transitionIn(el, () => {
             show();
-          });
+          }, component);
         } // Resolve immediately, only hold up parent `x-show`s for hidin.
 
 
@@ -686,13 +708,13 @@
     if (expressionResult && !elementHasAlreadyBeenAdded) {
       const clone = document.importNode(el.content, true);
       el.parentElement.insertBefore(clone, el.nextElementSibling);
-      transitionIn(el.nextElementSibling, () => {}, initialUpdate);
+      transitionIn(el.nextElementSibling, () => {}, component, initialUpdate);
       component.initializeElements(el.nextElementSibling, extraVars);
       el.nextElementSibling.__x_inserted_me = true;
     } else if (!expressionResult && elementHasAlreadyBeenAdded) {
       transitionOut(el.nextElementSibling, () => {
         el.nextElementSibling.remove();
-      }, initialUpdate);
+      }, component, initialUpdate);
     }
   }
 
@@ -1400,7 +1422,7 @@
     initializeElement(el, extraVars) {
       // To support class attribute merging, we have to know what the element's
       // original class attribute looked like for reference.
-      if (el.hasAttribute('class') && getXAttrs(el).length > 0) {
+      if (el.hasAttribute('class') && getXAttrs(el, this).length > 0) {
         el.__x_original_classes = el.getAttribute('class').split(' ');
       }
 
@@ -1455,7 +1477,7 @@
     }
 
     registerListeners(el, extraVars) {
-      getXAttrs(el).forEach(({
+      getXAttrs(el, this).forEach(({
         type,
         value,
         modifiers,
@@ -1474,7 +1496,7 @@
     }
 
     resolveBoundAttributes(el, initialUpdate = false, extraVars) {
-      let attrs = getXAttrs(el);
+      let attrs = getXAttrs(el, this);
 
       if (el.type !== undefined && el.type === 'radio') {
         // If there's an x-model on a radio input, move it to end of attribute list

+ 21 - 0
examples/index.html

@@ -426,6 +426,27 @@
                     </td>
                 </tr>
 
+                <tr>
+                    <td>x-bind spread</td>
+                    <td>
+                        <div x-data="modal()">
+                            I should be "hey": <span x-bind="bound"></span>
+                        </div>
+
+                        <script>
+                            function modal() {
+                               return {
+                                   foo: { bar: 'hey' },
+                                   bound: {
+                                       ['x-text']() { return this.foo.bar }
+                                   },
+                               }
+                            }
+                        </script>
+
+                    </td>
+                </tr>
+
                 <tr>
                     <td>Cloak</td>
                     <td>

+ 3 - 3
src/component.js

@@ -162,7 +162,7 @@ export default class Component {
     initializeElement(el, extraVars) {
         // To support class attribute merging, we have to know what the element's
         // original class attribute looked like for reference.
-        if (el.hasAttribute('class') && getXAttrs(el).length > 0) {
+        if (el.hasAttribute('class') && getXAttrs(el, this).length > 0) {
             el.__x_original_classes = el.getAttribute('class').split(' ')
         }
 
@@ -221,7 +221,7 @@ export default class Component {
     }
 
     registerListeners(el, extraVars) {
-        getXAttrs(el).forEach(({ type, value, modifiers, expression }) => {
+        getXAttrs(el, this).forEach(({ type, value, modifiers, expression }) => {
             switch (type) {
                 case 'on':
                     registerListener(this, el, value, modifiers, expression, extraVars)
@@ -237,7 +237,7 @@ export default class Component {
     }
 
     resolveBoundAttributes(el, initialUpdate = false, extraVars) {
-        let attrs = getXAttrs(el)
+        let attrs = getXAttrs(el, this)
         if (el.type !== undefined && el.type === 'radio') {
             // If there's an x-model on a radio input, move it to end of attribute list
             // to ensure that x-bind:value (if present) is processed first.

+ 10 - 8
src/directives/for.js

@@ -1,9 +1,11 @@
-import { transitionIn, transitionOut, getXAttrs } from '../utils'
+import { transitionIn, transitionOut, getXAttrs, saferEval } from '../utils'
 
 export function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
     warnIfNotTemplateTag(templateEl)
 
-    let iteratorNames = parseForExpression(expression)
+    let iteratorNames = typeof expression === 'function'
+        ? parseForExpression(component.evaluateReturnExpression(templateEl, expression))
+        : parseForExpression(expression)
 
     let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars)
 
@@ -19,7 +21,7 @@ export function handleForDirective(component, templateEl, expression, initialUpd
             nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl)
 
             // And transition it in if it's not the first page load.
-            transitionIn(nextEl, () => {}, initialUpdate)
+            transitionIn(nextEl, () => {}, component, initialUpdate)
 
             nextEl.__x_for = iterationScopeVariables
             component.initializeElements(nextEl, () => nextEl.__x_for)
@@ -36,7 +38,7 @@ export function handleForDirective(component, templateEl, expression, initialUpd
         currentEl.__x_for_key = currentKey
     })
 
-    removeAnyLeftOverElementsFromPreviousUpdate(currentEl)
+    removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component)
 }
 
 // This was taken from VueJS 2.* core. Thanks Vue!
@@ -75,7 +77,7 @@ function getIterationScopeVariables(iteratorNames, item, index, items, extraVars
 }
 
 function generateKeyForIteration(component, el, index, iterationScopeVariables) {
-    let bindKeyAttribute = getXAttrs(el, 'bind').filter(attr => attr.value === 'key')[0]
+    let bindKeyAttribute = getXAttrs(el, component, 'bind').filter(attr => attr.value === 'key')[0]
 
     // If the dev hasn't specified a key, just return the index of the iteration.
     if (! bindKeyAttribute) return index
@@ -88,7 +90,7 @@ function warnIfNotTemplateTag(el) {
 }
 
 function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, el, iteratorNames, extraVars) {
-    let ifAttribute = getXAttrs(el, 'if')[0]
+    let ifAttribute = getXAttrs(el, component, 'if')[0]
 
     if (ifAttribute && ! component.evaluateReturnExpression(el, ifAttribute.expression)) {
         return []
@@ -126,7 +128,7 @@ function lookAheadForMatchingKeyedElementAndMoveItIfFound(nextEl, currentKey) {
     }
 }
 
-function removeAnyLeftOverElementsFromPreviousUpdate(currentEl) {
+function removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component) {
     var nextElementFromOldLoop = (currentEl.nextElementSibling && currentEl.nextElementSibling.__x_for_key !== undefined) ? currentEl.nextElementSibling : false
 
     while (nextElementFromOldLoop) {
@@ -134,7 +136,7 @@ function removeAnyLeftOverElementsFromPreviousUpdate(currentEl) {
         let nextSibling = nextElementFromOldLoop.nextElementSibling
         transitionOut(nextElementFromOldLoop, () => {
             nextElementFromOldLoopImmutable.remove()
-        })
+        }, component)
         nextElementFromOldLoop = (nextSibling && nextSibling.__x_for_key !== undefined) ? nextSibling : false
     }
 }

+ 2 - 2
src/directives/if.js

@@ -10,7 +10,7 @@ export function handleIfDirective(component, el, expressionResult, initialUpdate
 
         el.parentElement.insertBefore(clone, el.nextElementSibling)
 
-        transitionIn(el.nextElementSibling, () => {}, initialUpdate)
+        transitionIn(el.nextElementSibling, () => {}, component, initialUpdate)
 
         component.initializeElements(el.nextElementSibling, extraVars)
 
@@ -18,6 +18,6 @@ export function handleIfDirective(component, el, expressionResult, initialUpdate
     } else if (! expressionResult && elementHasAlreadyBeenAdded) {
         transitionOut(el.nextElementSibling, () => {
             el.nextElementSibling.remove()
-        }, initialUpdate)
+        }, component, initialUpdate)
     }
 }

+ 2 - 2
src/directives/show.js

@@ -29,7 +29,7 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
                     resolve(() => {
                         hide()
                     })
-                })
+                }, component)
             } else {
                 resolve(() => {})
             }
@@ -37,7 +37,7 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
             if ( el.style.display !== '' ) {
                 transitionIn(el, () => {
                     show()
-                })
+                }, component)
             }
 
             // Resolve immediately, only hold up parent `x-show`s for hidin.

+ 54 - 26
src/utils.js

@@ -50,12 +50,20 @@ export function debounce(func, wait) {
 }
 
 export function saferEval(expression, dataContext, additionalHelperVariables = {}) {
+    if (typeof expression === 'function') {
+        return expression.call(dataContext)
+    }
+
     return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `var __alpine_result; with($data) { __alpine_result = ${expression} }; return __alpine_result`))(
         dataContext, ...Object.values(additionalHelperVariables)
     )
 }
 
 export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
+    if (typeof expression === 'function') {
+        return expression.call(dataContext)
+    }
+
     // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
     // Where "foo" is a function. Also, we'll pass the function the event instance when we call it.
     if (Object.keys(dataContext).includes(expression)) {
@@ -80,21 +88,17 @@ export function isXAttr(attr) {
     return xAttrRE.test(name)
 }
 
-export function getXAttrs(el, type) {
+export function getXAttrs(el, component, type) {
     return Array.from(el.attributes)
         .filter(isXAttr)
-        .map(attr => {
-            const name = replaceAtAndColonWithStandardSyntax(attr.name)
-
-            const typeMatch = name.match(xAttrRE)
-            const valueMatch = name.match(/:([a-zA-Z\-:]+)/)
-            const modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
-
-            return {
-                type: typeMatch ? typeMatch[1] : null,
-                value: valueMatch ? valueMatch[1] : null,
-                modifiers: modifiers.map(i => i.replace('.', '')),
-                expression: attr.value,
+        .map(parseHtmlAttribute)
+        .flatMap(i => {
+            if (i.type === 'bind' && i.value === null) {
+                let directiveBindings = saferEval(i.expression, component.$data)
+
+                return Object.entries(directiveBindings).map(([name, value]) => parseHtmlAttribute({ name, value }))
+            } else {
+                return i
             }
         })
         .filter(i => {
@@ -105,6 +109,21 @@ export function getXAttrs(el, type) {
         })
 }
 
+function parseHtmlAttribute({ name, value }) {
+    const normalizedName = replaceAtAndColonWithStandardSyntax(name)
+
+    const typeMatch = normalizedName.match(xAttrRE)
+    const valueMatch = normalizedName.match(/:([a-zA-Z\-:]+)/)
+    const modifiers = normalizedName.match(/\.[^.\]]+(?=[^\]]*$)/g) || []
+
+    return {
+        type: typeMatch ? typeMatch[1] : null,
+        value: valueMatch ? valueMatch[1] : null,
+        modifiers: modifiers.map(i => i.replace('.', '')),
+        expression: value,
+    }
+}
+
 export function isBooleanAttr(attrName) {
     // As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
     // Array roughly ordered by estimated usage
@@ -129,12 +148,12 @@ export function replaceAtAndColonWithStandardSyntax(name) {
     return name
 }
 
-export function transitionIn(el, show, forceSkip = false) {
+export function transitionIn(el, show, component, forceSkip = false) {
     // We don't want to transition on the initial page load.
     if (forceSkip) return show()
 
-    const attrs = getXAttrs(el, 'transition')
-    const showAttr = getXAttrs(el, 'show')[0]
+    const attrs = getXAttrs(el, component, 'transition')
+    const showAttr = getXAttrs(el, component, 'show')[0]
 
     // If this is triggered by a x-show.transition.
     if (showAttr && showAttr.modifiers.includes('transition')) {
@@ -152,18 +171,18 @@ export function transitionIn(el, show, forceSkip = false) {
         transitionHelperIn(el, modifiers, show)
     // Otherwise, we can assume x-transition:enter.
     } else if (attrs.filter(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value)).length > 0) {
-        transitionClassesIn(el, attrs, show)
+        transitionClassesIn(el, component, attrs, show)
     } else {
     // If neither, just show that damn thing.
         show()
     }
 }
 
-export function transitionOut(el, hide, forceSkip = false) {
+export function transitionOut(el, hide, component, forceSkip = false) {
     if (forceSkip) return hide()
 
-    const attrs = getXAttrs(el, 'transition')
-    const showAttr = getXAttrs(el, 'show')[0]
+    const attrs = getXAttrs(el, component, 'transition')
+    const showAttr = getXAttrs(el, component, 'show')[0]
 
     if (showAttr && showAttr.modifiers.includes('transition')) {
         let modifiers = showAttr.modifiers
@@ -177,7 +196,7 @@ export function transitionOut(el, hide, forceSkip = false) {
 
         transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide)
     } else if (attrs.filter(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value)).length > 0) {
-        transitionClassesOut(el, attrs, hide)
+        transitionClassesOut(el, component, attrs, hide)
     } else {
         hide()
     }
@@ -305,15 +324,24 @@ export function transitionHelper(el, modifiers, hook1, hook2, styleValues) {
     transition(el, stages)
 }
 
-export function transitionClassesIn(el, directives, showCallback) {
-    const enter = (directives.find(i => i.value === 'enter') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const enterStart = (directives.find(i => i.value === 'enter-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const enterEnd = (directives.find(i => i.value === 'enter-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+export function transitionClassesIn(el, component, directives, showCallback) {
+    let ensureStringExpression = (expression) => {
+        return typeof expression === 'function'
+            ? component.evaluateReturnExpression(el, expression)
+            : expression
+    }
+
+    const enter = ensureStringExpression((directives.find(i => i.value === 'enter') || { expression: '' }).expression)
+        .split(' ').filter(i => i !== '')
+    const enterStart = ensureStringExpression((directives.find(i => i.value === 'enter-start') || { expression: '' }).expression)
+        .split(' ').filter(i => i !== '')
+    const enterEnd = ensureStringExpression((directives.find(i => i.value === 'enter-end') || { expression: '' }).expression)
+        .split(' ').filter(i => i !== '')
 
     transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {})
 }
 
-export function transitionClassesOut(el, directives, hideCallback) {
+export function transitionClassesOut(el, component, directives, hideCallback) {
     const leave = (directives.find(i => i.value === 'leave') || { expression: '' }).expression.split(' ').filter(i => i !== '')
     const leaveStart = (directives.find(i => i.value === 'leave-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
     const leaveEnd = (directives.find(i => i.value === 'leave-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')

+ 141 - 0
test/bind.spec.js

@@ -443,3 +443,144 @@ test('extra whitespace in class binding string syntax is ignored', async () => {
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
     expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
 })
+
+test('can bind an object of directives', async () => {
+    window.modal = () => {
+        return {
+            show: false,
+            trigger: {
+                ['x-on:click']() { this.show = ! this.show }
+            },
+            dialogue: {
+                ['x-show']() { return this.show }
+            },
+        }
+    }
+
+    document.body.innerHTML = `
+        <div x-data="window.modal()">
+            <button x-bind="trigger">Toggle</button>
+
+            <span x-bind="dialogue">Modal Body</span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual(null) })
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;') })
+})
+
+test('x-bind object spread syntax supports x-for', async () => {
+    window.todos = () => {
+        return {
+            todos: ['one', 'two', 'three'],
+            outputForExpression: {
+                ['x-for']() { return 'todo in todos' }
+            },
+        }
+    }
+
+    document.body.innerHTML = `
+        <div x-data="window.todos()">
+            <ul>
+                <template x-bind="outputForExpression">
+                    <li x-text="todo"></li>
+                </template>
+            </ul>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelectorAll('li')[0].innerText).toEqual('one')
+    expect(document.querySelectorAll('li')[1].innerText).toEqual('two')
+    expect(document.querySelectorAll('li')[2].innerText).toEqual('three')
+})
+
+test('x-bind object spread syntax supports x-transition', async () => {
+    // Hijack "requestAnimationFrame" for finer-tuned control in this test.
+    var frameStack = []
+
+    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
+        frameStack.push(callback)
+    });
+
+    // Hijack "getComputeStyle" because js-dom is weird with it.
+    // (hardcoding 10ms transition time for later assertions)
+    jest.spyOn(window, 'getComputedStyle').mockImplementation(el => {
+        return { transitionDuration: '.01s' }
+    });
+
+    window.transitions = () => {
+        return {
+            show: false,
+            outputClickExpression: {
+                ['x-on:click']() { this.show = ! this.show },
+            },
+            outputTransitionExpression: {
+                ['x-show']() { return this.show },
+                ['x-transition:enter']() { return 'enter' },
+                ['x-transition:enter-start']() { return 'enter-start' },
+                ['x-transition:enter-end']() { return 'enter-end' },
+            },
+        }
+    }
+
+    document.body.innerHTML = `
+        <div x-data="transitions()">
+            <button x-bind="outputClickExpression"></button>
+
+            <span x-bind="outputTransitionExpression"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;') })
+
+    document.querySelector('button').click()
+
+    // Wait out the intial Alpine refresh debounce.
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 5)
+    )
+
+    expect(document.querySelector('span').classList.contains('enter')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-start')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-end')).toEqual(false)
+    expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
+
+    frameStack.pop()()
+
+    expect(document.querySelector('span').classList.contains('enter')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-start')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-end')).toEqual(false)
+    expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+
+    frameStack.pop()()
+
+    expect(document.querySelector('span').classList.contains('enter')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-start')).toEqual(false)
+    expect(document.querySelector('span').classList.contains('enter-end')).toEqual(true)
+    expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            expect(document.querySelector('span').classList.contains('enter')).toEqual(false)
+            expect(document.querySelector('span').classList.contains('enter-start')).toEqual(false)
+            expect(document.querySelector('span').classList.contains('enter-end')).toEqual(false)
+            expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+            resolve();
+        }, 10)
+    )
+})