瀏覽代碼

Merge pull request #2 from alpinejs/master

Update
Ryan Chandler 5 年之前
父節點
當前提交
19ce3c3671
共有 18 個文件被更改,包括 1916 次插入539 次删除
  1. 11 1
      README.md
  2. 576 153
      dist/alpine-ie11.js
  3. 116 40
      dist/alpine.js
  4. 460 267
      package-lock.json
  5. 4 4
      package.json
  6. 7 3
      src/component.js
  7. 4 2
      src/directives/bind.js
  8. 1 1
      src/directives/if.js
  9. 6 1
      src/directives/on.js
  10. 6 4
      src/directives/show.js
  11. 6 0
      src/index.js
  12. 94 29
      src/utils.js
  13. 12 0
      test/bind.spec.js
  14. 392 21
      test/model.spec.js
  15. 21 0
      test/on.spec.js
  16. 47 0
      test/show.spec.js
  17. 28 0
      test/spread.spec.js
  18. 125 13
      test/transition.spec.js

+ 11 - 1
README.md

@@ -290,6 +290,11 @@ This will add or remove the `disabled` attribute when `myVar` is true or false r
 
 
 Boolean attributes are supported as per the [HTML specification](https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute), for example `disabled`, `readonly`, `required`, `checked`, `hidden`, `selected`, `open`, etc.
 Boolean attributes are supported as per the [HTML specification](https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute), for example `disabled`, `readonly`, `required`, `checked`, `hidden`, `selected`, `open`, etc.
 
 
+**`.camel` modifier**
+**Example:** `<svg x-bind:view-box.camel="viewBox">`
+
+The `camel` modifier will bind to the camel case equivalent of the attribute name. In the example above, the value of `viewBox` will be bound the `viewBox` attribute as opposed to the `view-box` attribute.
+
 ---
 ---
 
 
 ### `x-on`
 ### `x-on`
@@ -374,6 +379,11 @@ If you wish to customize this, you can specifiy a custom wait time like so:
 <input x-on:input.debounce.750ms="fetchSomething()">
 <input x-on:input.debounce.750ms="fetchSomething()">
 ```
 ```
 
 
+**`.camel` modifier**
+**Example:** `<input x-on:event-name.camel="doSomething()">`
+
+The `camel` modifier will attach an event listener for the camel case equivalent event name. In the example above, the expression will be evaluated when the `eventName` event is fired on the element.
+
 ---
 ---
 
 
 ### `x-model`
 ### `x-model`
@@ -562,7 +572,7 @@ These behave exactly like VueJs's transition directives, except they have differ
 </script>
 </script>
 ```
 ```
 
 
-`x-spread` allows you to extract an elements Alpine bindings into a reusable object.
+`x-spread` allows you to extract an element's Alpine bindings into a reusable object.
 
 
 The object keys are the directives (Can be any directive including modifiers), and the values are callbacks to be evaluated by Alpine.
 The object keys are the directives (Can be any directive including modifiers), and the values are callbacks to be evaluated by Alpine.
 
 

文件差異過大導致無法顯示
+ 576 - 153
dist/alpine-ie11.js


+ 116 - 40
dist/alpine.js

@@ -80,6 +80,9 @@
   function kebabCase(subject) {
   function kebabCase(subject) {
     return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase();
     return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase();
   }
   }
+  function camelCase(subject) {
+    return subject.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (match, char) => char.toUpperCase());
+  }
   function walk(el, callback) {
   function walk(el, callback) {
     if (callback(el) === false) return;
     if (callback(el) === false) return;
     let node = el.firstElementChild;
     let node = el.firstElementChild;
@@ -113,7 +116,7 @@
   }
   }
   function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
   function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
     if (typeof expression === 'function') {
     if (typeof expression === 'function') {
-      return expression.call(dataContext);
+      return expression.call(dataContext, additionalHelperVariables['$event']);
     } // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
     } // 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.
     // Where "foo" is a function. Also, we'll pass the function the event instance when we call it.
 
 
@@ -147,10 +150,16 @@
       })));
       })));
     }
     }
 
 
-    return directives.filter(i => {
-      // If no type is passed in for filtering, bypass filter
-      if (!type) return true;
-      return i.type === type;
+    if (type) return directives.filter(i => i.type === type);
+    return sortDirectives(directives);
+  }
+
+  function sortDirectives(directives) {
+    let directiveOrder = ['bind', 'model', 'show', 'catch-all'];
+    return directives.sort((a, b) => {
+      let typeA = directiveOrder.indexOf(a.type) === -1 ? 'catch-all' : a.type;
+      let typeB = directiveOrder.indexOf(b.type) === -1 ? 'catch-all' : b.type;
+      return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB);
     });
     });
   }
   }
 
 
@@ -188,8 +197,18 @@
   function convertClassStringToArray(classList, filterFn = Boolean) {
   function convertClassStringToArray(classList, filterFn = Boolean) {
     return classList.split(' ').filter(filterFn);
     return classList.split(' ').filter(filterFn);
   }
   }
+  const TRANSITION_TYPE_IN = 'in';
+  const TRANSITION_TYPE_OUT = 'out';
   function transitionIn(el, show, component, forceSkip = false) {
   function transitionIn(el, show, component, forceSkip = false) {
+    // We don't want to transition on the initial page load.
     if (forceSkip) return show();
     if (forceSkip) return show();
+
+    if (el.__x_transition && el.__x_transition.type === TRANSITION_TYPE_IN) {
+      // there is already a similar transition going on, this was probably triggered by
+      // a change in a different property, let's just leave the previous one doing its job
+      return;
+    }
+
     const attrs = getXAttrs(el, component, 'transition');
     const attrs = getXAttrs(el, component, 'transition');
     const showAttr = getXAttrs(el, component, 'show')[0]; // If this is triggered by a x-show.transition.
     const showAttr = getXAttrs(el, component, 'show')[0]; // If this is triggered by a x-show.transition.
 
 
@@ -201,7 +220,7 @@
 
 
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers;
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers;
       transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter.
       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) {
+    } else if (attrs.some(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value))) {
       transitionClassesIn(el, component, attrs, show);
       transitionClassesIn(el, component, attrs, show);
     } else {
     } else {
       // If neither, just show that damn thing.
       // If neither, just show that damn thing.
@@ -211,6 +230,13 @@
   function transitionOut(el, hide, component, forceSkip = false) {
   function transitionOut(el, hide, component, forceSkip = false) {
     // We don't want to transition on the initial page load.
     // We don't want to transition on the initial page load.
     if (forceSkip) return hide();
     if (forceSkip) return hide();
+
+    if (el.__x_transition && el.__x_transition.type === TRANSITION_TYPE_OUT) {
+      // there is already a similar transition going on, this was probably triggered by
+      // a change in a different property, let's just leave the previous one doing its job
+      return;
+    }
+
     const attrs = getXAttrs(el, component, 'transition');
     const attrs = getXAttrs(el, component, 'transition');
     const showAttr = getXAttrs(el, component, 'show')[0];
     const showAttr = getXAttrs(el, component, 'show')[0];
 
 
@@ -220,7 +246,7 @@
       const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out');
       const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out');
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers;
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers;
       transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
       transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
-    } else if (attrs.filter(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value)).length > 0) {
+    } else if (attrs.some(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value))) {
       transitionClassesOut(el, component, attrs, hide);
       transitionClassesOut(el, component, attrs, hide);
     } else {
     } else {
       hide();
       hide();
@@ -240,7 +266,7 @@
         scale: 100
         scale: 100
       }
       }
     };
     };
-    transitionHelper(el, modifiers, showCallback, () => {}, styleValues);
+    transitionHelper(el, modifiers, showCallback, () => {}, styleValues, TRANSITION_TYPE_IN);
   }
   }
   function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
   function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
     // Make the "out" transition .5x slower than the "in". (Visually better)
     // Make the "out" transition .5x slower than the "in". (Visually better)
@@ -259,7 +285,7 @@
         scale: modifierValue(modifiers, 'scale', 95)
         scale: modifierValue(modifiers, 'scale', 95)
       }
       }
     };
     };
-    transitionHelper(el, modifiers, () => {}, hideCallback, styleValues);
+    transitionHelper(el, modifiers, () => {}, hideCallback, styleValues, TRANSITION_TYPE_OUT);
   }
   }
 
 
   function modifierValue(modifiers, key, fallback) {
   function modifierValue(modifiers, key, fallback) {
@@ -292,8 +318,14 @@
     return rawValue;
     return rawValue;
   }
   }
 
 
-  function transitionHelper(el, modifiers, hook1, hook2, styleValues) {
-    // If the user set these style values, we'll put them back when we're done with them.
+  function transitionHelper(el, modifiers, hook1, hook2, styleValues, type) {
+    // clear the previous transition if exists to avoid caching the wrong styles
+    if (el.__x_transition) {
+      cancelAnimationFrame(el.__x_transition.nextFrame);
+      el.__x_transition.callback && el.__x_transition.callback();
+    } // If the user set these style values, we'll put them back when we're done with them.
+
+
     const opacityCache = el.style.opacity;
     const opacityCache = el.style.opacity;
     const transformCache = el.style.transform;
     const transformCache = el.style.transform;
     const transformOriginCache = el.style.transformOrigin; // If no modifiers are present: x-show.transition, we'll default to both opacity and scale.
     const transformOriginCache = el.style.transformOrigin; // If no modifiers are present: x-show.transition, we'll default to both opacity and scale.
@@ -340,7 +372,7 @@
       }
       }
 
 
     };
     };
-    transition(el, stages);
+    transition(el, stages, type);
   }
   }
   function transitionClassesIn(el, component, directives, showCallback) {
   function transitionClassesIn(el, component, directives, showCallback) {
     let ensureStringExpression = expression => {
     let ensureStringExpression = expression => {
@@ -356,7 +388,7 @@
     const enterEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-end') || {
     const enterEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-end') || {
       expression: ''
       expression: ''
     }).expression));
     }).expression));
-    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {});
+    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}, TRANSITION_TYPE_IN);
   }
   }
   function transitionClassesOut(el, component, directives, hideCallback) {
   function transitionClassesOut(el, component, directives, hideCallback) {
     const leave = convertClassStringToArray((directives.find(i => i.value === 'leave') || {
     const leave = convertClassStringToArray((directives.find(i => i.value === 'leave') || {
@@ -368,9 +400,15 @@
     const leaveEnd = convertClassStringToArray((directives.find(i => i.value === 'leave-end') || {
     const leaveEnd = convertClassStringToArray((directives.find(i => i.value === 'leave-end') || {
       expression: ''
       expression: ''
     }).expression);
     }).expression);
-    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback);
+    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback, TRANSITION_TYPE_OUT);
   }
   }
-  function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2) {
+  function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type) {
+    // clear the previous transition if exists to avoid caching the wrong classes
+    if (el.__x_transition) {
+      cancelAnimationFrame(el.__x_transition.nextFrame);
+      el.__x_transition.callback && el.__x_transition.callback();
+    }
+
     const originalClasses = el.__x_original_classes || [];
     const originalClasses = el.__x_original_classes || [];
     const stages = {
     const stages = {
       start() {
       start() {
@@ -401,12 +439,31 @@
       }
       }
 
 
     };
     };
-    transition(el, stages);
-  }
-  function transition(el, stages) {
+    transition(el, stages, type);
+  }
+  function transition(el, stages, type) {
+    el.__x_transition = {
+      // Set transition type so we can avoid clearing transition if the direction is the same
+      type: type,
+      // create a callback for the last stages of the transition so we can call it
+      // from different point and early terminate it. Once will ensure that function
+      // is only called one time.
+      callback: once(() => {
+        stages.hide(); // Adding an "isConnected" check, in case the callback
+        // removed the element from the DOM.
+
+        if (el.isConnected) {
+          stages.cleanup();
+        }
+
+        delete el.__x_transition;
+      }),
+      // This store the next animation frame so we can cancel it
+      nextFrame: null
+    };
     stages.start();
     stages.start();
     stages.during();
     stages.during();
-    requestAnimationFrame(() => {
+    el.__x_transition.nextFrame = requestAnimationFrame(() => {
       // Note: Safari's transitionDuration property will list out comma separated transition durations
       // 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.
       // for every single transition property. Let's grab the first one and call it a day.
       let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000;
       let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000;
@@ -416,22 +473,25 @@
       }
       }
 
 
       stages.show();
       stages.show();
-      requestAnimationFrame(() => {
-        stages.end(); // Assign current transition to el in case we need to force it.
-
-        setTimeout(() => {
-          stages.hide(); // Adding an "isConnected" check, in case the callback
-          // removed the element from the DOM.
-
-          if (el.isConnected) {
-            stages.cleanup();
-          }
-        }, duration);
+      el.__x_transition.nextFrame = requestAnimationFrame(() => {
+        stages.end();
+        setTimeout(el.__x_transition.callback, duration);
       });
       });
     });
     });
   }
   }
   function isNumeric(subject) {
   function isNumeric(subject) {
     return !isNaN(subject);
     return !isNaN(subject);
+  } // Thanks @vuejs
+  // https://github.com/vuejs/vue/blob/4de4649d9637262a9b007720b59f80ac72a5620c/src/shared/util.js
+
+  function once(callback) {
+    let called = false;
+    return function () {
+      if (!called) {
+        called = true;
+        callback.apply(this, arguments);
+      }
+    };
   }
   }
 
 
   function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
   function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
@@ -551,7 +611,7 @@
     }
     }
   }
   }
 
 
-  function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType) {
+  function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType, modifiers) {
     var value = component.evaluateReturnExpression(el, expression, extraVars);
     var value = component.evaluateReturnExpression(el, expression, extraVars);
 
 
     if (attrName === 'value') {
     if (attrName === 'value') {
@@ -612,7 +672,8 @@
         el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '));
         el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '));
       }
       }
     } else {
     } else {
-      // If an attribute's bound value is null, undefined or false, remove the attribute
+      attrName = modifiers.includes('camel') ? camelCase(attrName) : attrName; // If an attribute's bound value is null, undefined or false, remove the attribute
+
       if ([null, undefined, false].includes(value)) {
       if ([null, undefined, false].includes(value)) {
         el.removeAttribute(attrName);
         el.removeAttribute(attrName);
       } else {
       } else {
@@ -674,9 +735,12 @@
 
 
     const handle = resolve => {
     const handle = resolve => {
       if (value) {
       if (value) {
-        transitionIn(el, () => {
-          show();
-        }, component);
+        if (el.style.display === 'none' || el.__x_transition) {
+          transitionIn(el, () => {
+            show();
+          }, component);
+        }
+
         resolve(() => {});
         resolve(() => {});
       } else {
       } else {
         if (el.style.display !== 'none') {
         if (el.style.display !== 'none') {
@@ -715,7 +779,7 @@
     warnIfMalformedTemplate(el, 'x-if');
     warnIfMalformedTemplate(el, 'x-if');
     const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true;
     const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true;
 
 
-    if (expressionResult && !elementHasAlreadyBeenAdded) {
+    if (expressionResult && (!elementHasAlreadyBeenAdded || el.__x_transition)) {
       const clone = document.importNode(el.content, true);
       const clone = document.importNode(el.content, true);
       el.parentElement.insertBefore(clone, el.nextElementSibling);
       el.parentElement.insertBefore(clone, el.nextElementSibling);
       transitionIn(el.nextElementSibling, () => {}, component, initialUpdate);
       transitionIn(el.nextElementSibling, () => {}, component, initialUpdate);
@@ -733,6 +797,10 @@
       passive: modifiers.includes('passive')
       passive: modifiers.includes('passive')
     };
     };
 
 
+    if (modifiers.includes('camel')) {
+      event = camelCase(event);
+    }
+
     if (modifiers.includes('away')) {
     if (modifiers.includes('away')) {
       let handler = e => {
       let handler = e => {
         // Don't do anything if the click came from the element or within it.
         // Don't do anything if the click came from the element or within it.
@@ -1375,6 +1443,10 @@
         // Alpine's got it's grubby little paws all over everything.
         // Alpine's got it's grubby little paws all over everything.
         initReturnedCallback.call(this.$data);
         initReturnedCallback.call(this.$data);
       }
       }
+
+      componentForClone || setTimeout(() => {
+        Alpine.onComponentInitializeds.forEach(callback => callback(this));
+      }, 0);
     }
     }
 
 
     getUnobservedData() {
     getUnobservedData() {
@@ -1548,13 +1620,13 @@
       }) => {
       }) => {
         switch (type) {
         switch (type) {
           case 'model':
           case 'model':
-            handleAttributeBindingDirective(this, el, 'value', expression, extraVars, type);
+            handleAttributeBindingDirective(this, el, 'value', expression, extraVars, type, modifiers);
             break;
             break;
 
 
           case 'bind':
           case 'bind':
             // The :key binding on an x-for is special, ignore it.
             // The :key binding on an x-for is special, ignore it.
             if (el.tagName.toLowerCase() === 'template' && value === 'key') return;
             if (el.tagName.toLowerCase() === 'template' && value === 'key') return;
-            handleAttributeBindingDirective(this, el, value, expression, extraVars, type);
+            handleAttributeBindingDirective(this, el, value, expression, extraVars, type, modifiers);
             break;
             break;
 
 
           case 'text':
           case 'text':
@@ -1574,7 +1646,7 @@
           case 'if':
           case 'if':
             // If this element also has x-for on it, don't process x-if.
             // If this element also has x-for on it, don't process x-if.
             // We will let the "x-for" directive handle the "if"ing.
             // We will let the "x-for" directive handle the "if"ing.
-            if (attrs.filter(i => i.type === 'for').length > 0) return;
+            if (attrs.some(i => i.type === 'for')) return;
             var output = this.evaluateReturnExpression(el, expression, extraVars);
             var output = this.evaluateReturnExpression(el, expression, extraVars);
             handleIfDirective(this, el, output, initialUpdate, extraVars);
             handleIfDirective(this, el, output, initialUpdate, extraVars);
             break;
             break;
@@ -1680,9 +1752,10 @@
   }
   }
 
 
   const Alpine = {
   const Alpine = {
-    version: "2.4.1",
+    version: "2.5.0",
     pauseMutationObserver: false,
     pauseMutationObserver: false,
     magicProperties: {},
     magicProperties: {},
+    onComponentInitializeds: [],
     start: async function start() {
     start: async function start() {
       if (!isTesting()) {
       if (!isTesting()) {
         await domReady();
         await domReady();
@@ -1761,6 +1834,9 @@
     },
     },
     addMagicProperty: function addMagicProperty(name, callback) {
     addMagicProperty: function addMagicProperty(name, callback) {
       this.magicProperties[name] = callback;
       this.magicProperties[name] = callback;
+    },
+    onComponentInitialized: function onComponentInitialized(callback) {
+      this.onComponentInitializeds.push(callback);
     }
     }
   };
   };
 
 

文件差異過大導致無法顯示
+ 460 - 267
package-lock.json


+ 4 - 4
package.json

@@ -1,7 +1,7 @@
 {
 {
     "main": "dist/alpine.js",
     "main": "dist/alpine.js",
     "name": "alpinejs",
     "name": "alpinejs",
-    "version": "2.4.1",
+    "version": "2.5.0",
     "repository": {
     "repository": {
         "type": "git",
         "type": "git",
         "url": "git://github.com/alpinejs/alpine.git"
         "url": "git://github.com/alpinejs/alpine.git"
@@ -15,8 +15,8 @@
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "devDependencies": {
     "devDependencies": {
-        "@babel/core": "^7.10.2",
-        "@babel/preset-env": "^7.10.2",
+        "@babel/core": "^7.10.5",
+        "@babel/preset-env": "^7.10.4",
         "@rollup/plugin-commonjs": "^11.1.0",
         "@rollup/plugin-commonjs": "^11.1.0",
         "@rollup/plugin-multi-entry": "^3.0.1",
         "@rollup/plugin-multi-entry": "^3.0.1",
         "@rollup/plugin-replace": "^2.3.3",
         "@rollup/plugin-replace": "^2.3.3",
@@ -33,7 +33,7 @@
         "jest": "^25.5.4",
         "jest": "^25.5.4",
         "jsdom-simulant": "^1.1.2",
         "jsdom-simulant": "^1.1.2",
         "observable-membrane": "^0.26.1",
         "observable-membrane": "^0.26.1",
-        "proxy-polyfill": "^0.3.1",
+        "proxy-polyfill": "^0.3.2",
         "rollup": "^1.32.1",
         "rollup": "^1.32.1",
         "rollup-plugin-babel": "^4.4.0",
         "rollup-plugin-babel": "^4.4.0",
         "rollup-plugin-filesize": "^6.2.1",
         "rollup-plugin-filesize": "^6.2.1",

+ 7 - 3
src/component.js

@@ -86,6 +86,10 @@ export default class Component {
             // Alpine's got it's grubby little paws all over everything.
             // Alpine's got it's grubby little paws all over everything.
             initReturnedCallback.call(this.$data)
             initReturnedCallback.call(this.$data)
         }
         }
+
+        componentForClone || setTimeout(() => {
+            Alpine.onComponentInitializeds.forEach(callback => callback(this))
+        }, 0)
     }
     }
 
 
     getUnobservedData() {
     getUnobservedData() {
@@ -264,14 +268,14 @@ export default class Component {
         attrs.forEach(({ type, value, modifiers, expression }) => {
         attrs.forEach(({ type, value, modifiers, expression }) => {
             switch (type) {
             switch (type) {
                 case 'model':
                 case 'model':
-                    handleAttributeBindingDirective(this, el, 'value', expression, extraVars, type)
+                    handleAttributeBindingDirective(this, el, 'value', expression, extraVars, type, modifiers)
                     break;
                     break;
 
 
                 case 'bind':
                 case 'bind':
                     // The :key binding on an x-for is special, ignore it.
                     // The :key binding on an x-for is special, ignore it.
                     if (el.tagName.toLowerCase() === 'template' && value === 'key') return
                     if (el.tagName.toLowerCase() === 'template' && value === 'key') return
 
 
-                    handleAttributeBindingDirective(this, el, value, expression, extraVars, type)
+                    handleAttributeBindingDirective(this, el, value, expression, extraVars, type, modifiers)
                     break;
                     break;
 
 
                 case 'text':
                 case 'text':
@@ -293,7 +297,7 @@ export default class Component {
                 case 'if':
                 case 'if':
                     // If this element also has x-for on it, don't process x-if.
                     // If this element also has x-for on it, don't process x-if.
                     // We will let the "x-for" directive handle the "if"ing.
                     // We will let the "x-for" directive handle the "if"ing.
-                    if (attrs.filter(i => i.type === 'for').length > 0) return
+                    if (attrs.some(i => i.type === 'for')) return
 
 
                     var output = this.evaluateReturnExpression(el, expression, extraVars)
                     var output = this.evaluateReturnExpression(el, expression, extraVars)
 
 

+ 4 - 2
src/directives/bind.js

@@ -1,6 +1,6 @@
-import { arrayUnique, isBooleanAttr, convertClassStringToArray } from '../utils'
+import { arrayUnique, isBooleanAttr, convertClassStringToArray, camelCase } from '../utils'
 
 
-export function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType) {
+export function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType, modifiers) {
     var value = component.evaluateReturnExpression(el, expression, extraVars)
     var value = component.evaluateReturnExpression(el, expression, extraVars)
 
 
     if (attrName === 'value') {
     if (attrName === 'value') {
@@ -63,6 +63,8 @@ export function handleAttributeBindingDirective(component, el, attrName, express
             el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '))
             el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '))
         }
         }
     } else {
     } else {
+        attrName = modifiers.includes('camel') ? camelCase(attrName) : attrName
+
         // If an attribute's bound value is null, undefined or false, remove the attribute
         // If an attribute's bound value is null, undefined or false, remove the attribute
         if ([null, undefined, false].includes(value)) {
         if ([null, undefined, false].includes(value)) {
             el.removeAttribute(attrName)
             el.removeAttribute(attrName)

+ 1 - 1
src/directives/if.js

@@ -5,7 +5,7 @@ export function handleIfDirective(component, el, expressionResult, initialUpdate
 
 
     const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true
     const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true
 
 
-    if (expressionResult && ! elementHasAlreadyBeenAdded) {
+    if (expressionResult && (! elementHasAlreadyBeenAdded || el.__x_transition)) {
         const clone = document.importNode(el.content, true);
         const clone = document.importNode(el.content, true);
 
 
         el.parentElement.insertBefore(clone, el.nextElementSibling)
         el.parentElement.insertBefore(clone, el.nextElementSibling)

+ 6 - 1
src/directives/on.js

@@ -1,9 +1,14 @@
-import { kebabCase, debounce, isNumeric } from '../utils'
+import { kebabCase, camelCase, debounce, isNumeric } from '../utils'
 
 
 export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
 export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
     const options = {
     const options = {
         passive: modifiers.includes('passive'),
         passive: modifiers.includes('passive'),
     };
     };
+
+    if (modifiers.includes('camel')) {
+        event = camelCase(event);
+    }
+
     if (modifiers.includes('away')) {
     if (modifiers.includes('away')) {
         let handler = e => {
         let handler = e => {
             // Don't do anything if the click came from the element or within it.
             // Don't do anything if the click came from the element or within it.

+ 6 - 4
src/directives/show.js

@@ -24,12 +24,14 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
 
 
     const handle = (resolve) => {
     const handle = (resolve) => {
         if (value) {
         if (value) {
-            transitionIn(el,() => {
-                show()
-            }, component)
+            if(el.style.display === 'none' || el.__x_transition) {
+                transitionIn(el, () => {
+                    show()
+                }, component)
+            }
             resolve(() => {})
             resolve(() => {})
         } else {
         } else {
-            if (el.style.display !== 'none' ) {
+            if (el.style.display !== 'none') {
                 transitionOut(el, () => {
                 transitionOut(el, () => {
                     resolve(() => {
                     resolve(() => {
                         hide()
                         hide()

+ 6 - 0
src/index.js

@@ -8,6 +8,8 @@ const Alpine = {
 
 
     magicProperties: {},
     magicProperties: {},
 
 
+    onComponentInitializeds: [],
+
     start: async function () {
     start: async function () {
         if (! isTesting()) {
         if (! isTesting()) {
             await domReady()
             await domReady()
@@ -103,6 +105,10 @@ const Alpine = {
 
 
     addMagicProperty: function (name, callback) {
     addMagicProperty: function (name, callback) {
         this.magicProperties[name] = callback
         this.magicProperties[name] = callback
+    },
+
+    onComponentInitialized: function (callback) {
+        this.onComponentInitializeds.push(callback)
     }
     }
 }
 }
 
 

+ 94 - 29
src/utils.js

@@ -32,6 +32,10 @@ export function kebabCase(subject) {
     return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
     return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
 }
 }
 
 
+export function camelCase(subject) {
+    return subject.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (match, char) => char.toUpperCase())
+}
+
 export function walk(el, callback) {
 export function walk(el, callback) {
     if (callback(el) === false) return
     if (callback(el) === false) return
 
 
@@ -69,7 +73,7 @@ export function saferEval(expression, dataContext, additionalHelperVariables = {
 
 
 export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
 export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
     if (typeof expression === 'function') {
     if (typeof expression === 'function') {
-        return expression.call(dataContext)
+        return expression.call(dataContext, additionalHelperVariables['$event'])
     }
     }
 
 
     // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
     // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
@@ -110,11 +114,19 @@ export function getXAttrs(el, component, type) {
         directives = directives.concat(Object.entries(spreadObject).map(([name, value]) => parseHtmlAttribute({ name, value })))
         directives = directives.concat(Object.entries(spreadObject).map(([name, value]) => parseHtmlAttribute({ name, value })))
     }
     }
 
 
-    return directives.filter(i => {
-        // If no type is passed in for filtering, bypass filter
-        if (! type) return true
+    if (type) return directives.filter(i => i.type === type)
+
+    return sortDirectives(directives)
+}
+
+function sortDirectives(directives) {
+    let directiveOrder = ['bind', 'model', 'show', 'catch-all']
+
+    return directives.sort((a, b) => {
+        let typeA = directiveOrder.indexOf(a.type) === -1 ? 'catch-all' : a.type
+        let typeB = directiveOrder.indexOf(b.type) === -1 ? 'catch-all' : b.type
 
 
-        return i.type === type
+        return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB)
     })
     })
 }
 }
 
 
@@ -161,9 +173,19 @@ export function convertClassStringToArray(classList, filterFn = Boolean) {
     return classList.split(' ').filter(filterFn)
     return classList.split(' ').filter(filterFn)
 }
 }
 
 
+const TRANSITION_TYPE_IN = 'in'
+const TRANSITION_TYPE_OUT = 'out'
+
 export function transitionIn(el, show, component, 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()
     if (forceSkip) return show()
 
 
+    if (el.__x_transition && el.__x_transition.type === TRANSITION_TYPE_IN) {
+        // there is already a similar transition going on, this was probably triggered by
+        // a change in a different property, let's just leave the previous one doing its job
+        return
+    }
+
     const attrs = getXAttrs(el, component, 'transition')
     const attrs = getXAttrs(el, component, 'transition')
     const showAttr = getXAttrs(el, component, 'show')[0]
     const showAttr = getXAttrs(el, component, 'show')[0]
 
 
@@ -182,7 +204,7 @@ export function transitionIn(el, show, component, forceSkip = false) {
 
 
         transitionHelperIn(el, modifiers, show)
         transitionHelperIn(el, modifiers, show)
     // Otherwise, we can assume x-transition:enter.
     // Otherwise, we can assume x-transition:enter.
-    } else if (attrs.filter(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value)).length > 0) {
+    } else if (attrs.some(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value))) {
         transitionClassesIn(el, component, attrs, show)
         transitionClassesIn(el, component, attrs, show)
     } else {
     } else {
     // If neither, just show that damn thing.
     // If neither, just show that damn thing.
@@ -191,9 +213,15 @@ export function transitionIn(el, show, component, forceSkip = false) {
 }
 }
 
 
 export function transitionOut(el, hide, component, forceSkip = false) {
 export function transitionOut(el, hide, component, forceSkip = false) {
-     // We don't want to transition on the initial page load.
+    // We don't want to transition on the initial page load.
     if (forceSkip) return hide()
     if (forceSkip) return hide()
 
 
+    if (el.__x_transition && el.__x_transition.type === TRANSITION_TYPE_OUT) {
+        // there is already a similar transition going on, this was probably triggered by
+        // a change in a different property, let's just leave the previous one doing its job
+        return
+    }
+
     const attrs = getXAttrs(el, component, 'transition')
     const attrs = getXAttrs(el, component, 'transition')
     const showAttr = getXAttrs(el, component, 'show')[0]
     const showAttr = getXAttrs(el, component, 'show')[0]
 
 
@@ -208,7 +236,7 @@ export function transitionOut(el, hide, component, forceSkip = false) {
             ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers
             ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers
 
 
         transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide)
         transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide)
-    } else if (attrs.filter(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value)).length > 0) {
+    } else if (attrs.some(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value))) {
         transitionClassesOut(el, component, attrs, hide)
         transitionClassesOut(el, component, attrs, hide)
     } else {
     } else {
         hide()
         hide()
@@ -230,7 +258,7 @@ export function transitionHelperIn(el, modifiers, showCallback) {
         },
         },
     }
     }
 
 
-    transitionHelper(el, modifiers, showCallback, () => {}, styleValues)
+    transitionHelper(el, modifiers, showCallback, () => {}, styleValues, TRANSITION_TYPE_IN)
 }
 }
 
 
 export function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
 export function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
@@ -254,7 +282,7 @@ export function transitionHelperOut(el, modifiers, settingBothSidesOfTransition,
         },
         },
     }
     }
 
 
-    transitionHelper(el, modifiers, () => {}, hideCallback, styleValues)
+    transitionHelper(el, modifiers, () => {}, hideCallback, styleValues, TRANSITION_TYPE_OUT)
 }
 }
 
 
 function modifierValue(modifiers, key, fallback) {
 function modifierValue(modifiers, key, fallback) {
@@ -289,7 +317,13 @@ function modifierValue(modifiers, key, fallback) {
     return rawValue
     return rawValue
 }
 }
 
 
-export function transitionHelper(el, modifiers, hook1, hook2, styleValues) {
+export function transitionHelper(el, modifiers, hook1, hook2, styleValues, type) {
+    // clear the previous transition if exists to avoid caching the wrong styles
+    if (el.__x_transition) {
+        cancelAnimationFrame(el.__x_transition.nextFrame)
+        el.__x_transition.callback && el.__x_transition.callback()
+    }
+
     // If the user set these style values, we'll put them back when we're done with them.
     // If the user set these style values, we'll put them back when we're done with them.
     const opacityCache = el.style.opacity
     const opacityCache = el.style.opacity
     const transformCache = el.style.transform
     const transformCache = el.style.transform
@@ -334,7 +368,7 @@ export function transitionHelper(el, modifiers, hook1, hook2, styleValues) {
         },
         },
     }
     }
 
 
-    transition(el, stages)
+    transition(el, stages, type)
 }
 }
 
 
 export function transitionClassesIn(el, component, directives, showCallback) {
 export function transitionClassesIn(el, component, directives, showCallback) {
@@ -348,7 +382,7 @@ export function transitionClassesIn(el, component, directives, showCallback) {
     const enterStart = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-start') || { expression: '' }).expression))
     const enterStart = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-start') || { expression: '' }).expression))
     const enterEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-end') || { expression: '' }).expression))
     const enterEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-end') || { expression: '' }).expression))
 
 
-    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {})
+    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}, TRANSITION_TYPE_IN)
 }
 }
 
 
 export function transitionClassesOut(el, component, directives, hideCallback) {
 export function transitionClassesOut(el, component, directives, hideCallback) {
@@ -356,10 +390,16 @@ export function transitionClassesOut(el, component, directives, hideCallback) {
     const leaveStart = convertClassStringToArray((directives.find(i => i.value === 'leave-start') || { expression: '' }).expression)
     const leaveStart = convertClassStringToArray((directives.find(i => i.value === 'leave-start') || { expression: '' }).expression)
     const leaveEnd = convertClassStringToArray((directives.find(i => i.value === 'leave-end') || { expression: '' }).expression)
     const leaveEnd = convertClassStringToArray((directives.find(i => i.value === 'leave-end') || { expression: '' }).expression)
 
 
-    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback)
+    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback, TRANSITION_TYPE_OUT)
 }
 }
 
 
-export function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2) {
+export function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type) {
+    // clear the previous transition if exists to avoid caching the wrong classes
+    if (el.__x_transition) {
+        cancelAnimationFrame(el.__x_transition.nextFrame)
+        el.__x_transition.callback && el.__x_transition.callback()
+    }
+
     const originalClasses = el.__x_original_classes || []
     const originalClasses = el.__x_original_classes || []
 
 
     const stages = {
     const stages = {
@@ -386,14 +426,35 @@ export function transitionClasses(el, classesDuring, classesStart, classesEnd, h
         },
         },
     }
     }
 
 
-    transition(el, stages)
+    transition(el, stages, type)
 }
 }
 
 
-export function transition(el, stages) {
+export function transition(el, stages, type) {
+    el.__x_transition = {
+        // Set transition type so we can avoid clearing transition if the direction is the same
+       type: type,
+        // create a callback for the last stages of the transition so we can call it
+        // from different point and early terminate it. Once will ensure that function
+        // is only called one time.
+        callback: once(() => {
+            stages.hide()
+
+            // Adding an "isConnected" check, in case the callback
+            // removed the element from the DOM.
+            if (el.isConnected) {
+                stages.cleanup()
+            }
+
+            delete el.__x_transition
+        }),
+        // This store the next animation frame so we can cancel it
+        nextFrame: null
+    }
+
     stages.start()
     stages.start()
     stages.during()
     stages.during()
 
 
-    requestAnimationFrame(() => {
+    el.__x_transition.nextFrame =requestAnimationFrame(() => {
         // Note: Safari's transitionDuration property will list out comma separated transition durations
         // 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.
         // for every single transition property. Let's grab the first one and call it a day.
         let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000
         let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000
@@ -404,19 +465,10 @@ export function transition(el, stages) {
 
 
         stages.show()
         stages.show()
 
 
-        requestAnimationFrame(() => {
+        el.__x_transition.nextFrame =requestAnimationFrame(() => {
             stages.end()
             stages.end()
 
 
-            // Assign current transition to el in case we need to force it.
-            setTimeout(() => {
-                stages.hide()
-
-                // Adding an "isConnected" check, in case the callback
-                // removed the element from the DOM.
-                if (el.isConnected) {
-                    stages.cleanup()
-                }
-            }, duration)
+            setTimeout(el.__x_transition.callback, duration)
         })
         })
     });
     });
 }
 }
@@ -424,3 +476,16 @@ export function transition(el, stages) {
 export function isNumeric(subject){
 export function isNumeric(subject){
     return ! isNaN(subject)
     return ! isNaN(subject)
 }
 }
+
+// Thanks @vuejs
+// https://github.com/vuejs/vue/blob/4de4649d9637262a9b007720b59f80ac72a5620c/src/shared/util.js
+export function once(callback) {
+    let called = false
+
+    return function () {
+        if (! called) {
+            called = true
+            callback.apply(this, arguments)
+        }
+    }
+}

+ 12 - 0
test/bind.spec.js

@@ -483,3 +483,15 @@ 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('foo')).toBeTruthy()
     expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
     expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
 })
 })
+
+test('.camel modifier correctly sets name of attribute', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <svg x-bind:view-box.camel="'0 0 42 42'"></svg>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('svg').getAttribute('viewBox')).toEqual('0 0 42 42')
+})

+ 392 - 21
test/model.spec.js

@@ -2,7 +2,7 @@ import Alpine from 'alpinejs'
 import { wait, fireEvent } from '@testing-library/dom'
 import { wait, fireEvent } from '@testing-library/dom'
 
 
 global.MutationObserver = class {
 global.MutationObserver = class {
-    observe() {}
+    observe() { }
 }
 }
 
 
 test('x-model has value binding when initialized', async () => {
 test('x-model has value binding when initialized', async () => {
@@ -26,7 +26,7 @@ test('x-model updates value when updated via input event', async () => {
 
 
     Alpine.start()
     Alpine.start()
 
 
-    fireEvent.input(document.querySelector('input'), { target: { value: 'baz' }})
+    fireEvent.input(document.querySelector('input'), { target: { value: 'baz' } })
 
 
     await wait(() => { expect(document.querySelector('input').value).toEqual('baz') })
     await wait(() => { expect(document.querySelector('input').value).toEqual('baz') })
 })
 })
@@ -56,7 +56,7 @@ test('x-model casts value to number if number modifier is present', async () =>
 
 
     Alpine.start()
     Alpine.start()
 
 
-    fireEvent.input(document.querySelector('input'), { target: { value: '123' }})
+    fireEvent.input(document.querySelector('input'), { target: { value: '123' } })
 
 
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(123) })
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(123) })
 })
 })
@@ -71,27 +71,27 @@ test('x-model with number modifier returns: null if empty, original value if cas
 
 
     Alpine.start()
     Alpine.start()
 
 
-    fireEvent.input(document.querySelectorAll('input')[0], { target: { value: '' }})
+    fireEvent.input(document.querySelectorAll('input')[0], { target: { value: '' } })
 
 
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(null) })
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(null) })
 
 
-    fireEvent.input(document.querySelectorAll('input')[0], { target: { value: '-' }})
+    fireEvent.input(document.querySelectorAll('input')[0], { target: { value: '-' } })
 
 
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(null) })
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(null) })
 
 
-    fireEvent.input(document.querySelectorAll('input')[0], { target: { value: '-123' }})
+    fireEvent.input(document.querySelectorAll('input')[0], { target: { value: '-123' } })
 
 
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(-123) })
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.foo).toEqual(-123) })
 
 
-    fireEvent.input(document.querySelectorAll('input')[1], { target: { value: '' }})
+    fireEvent.input(document.querySelectorAll('input')[1], { target: { value: '' } })
 
 
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.bar).toEqual(null) })
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.bar).toEqual(null) })
 
 
-    fireEvent.input(document.querySelectorAll('input')[1], { target: { value: '-' }})
+    fireEvent.input(document.querySelectorAll('input')[1], { target: { value: '-' } })
 
 
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.bar).toEqual('-') })
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.bar).toEqual('-') })
 
 
-    fireEvent.input(document.querySelectorAll('input')[1], { target: { value: '-123' }})
+    fireEvent.input(document.querySelectorAll('input')[1], { target: { value: '-123' } })
 
 
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.bar).toEqual(-123) })
     await wait(() => { expect(document.querySelector('[x-data]').__x.$data.bar).toEqual(-123) })
 })
 })
@@ -107,7 +107,7 @@ test('x-model trims value if trim modifier is present', async () => {
 
 
     Alpine.start()
     Alpine.start()
 
 
-    fireEvent.input(document.querySelector('input'), { target: { value: 'bar   ' }})
+    fireEvent.input(document.querySelector('input'), { target: { value: 'bar   ' } })
 
 
     await wait(() => { expect(document.querySelector('span').innerText).toEqual('bar') })
     await wait(() => { expect(document.querySelector('span').innerText).toEqual('bar') })
 })
 })
@@ -121,7 +121,7 @@ test('x-model updates value when updated via changed event when lazy modifier is
 
 
     Alpine.start()
     Alpine.start()
 
 
-    fireEvent.change(document.querySelector('input'), { target: { value: 'baz' }})
+    fireEvent.change(document.querySelector('input'), { target: { value: 'baz' } })
 
 
     await wait(() => { expect(document.querySelector('input').value).toEqual('baz') })
     await wait(() => { expect(document.querySelector('input').value).toEqual('baz') })
 })
 })
@@ -140,7 +140,7 @@ test('x-model binds checkbox value', async () => {
     expect(document.querySelector('input').checked).toEqual(true)
     expect(document.querySelector('input').checked).toEqual(true)
     expect(document.querySelector('span').getAttribute('bar')).toEqual("true")
     expect(document.querySelector('span').getAttribute('bar')).toEqual("true")
 
 
-    fireEvent.change(document.querySelector('input'), { target: { checked: false }})
+    fireEvent.change(document.querySelector('input'), { target: { checked: false } })
 
 
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("false") })
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("false") })
 })
 })
@@ -161,7 +161,7 @@ test('x-model binds checkbox value to array', async () => {
     expect(document.querySelectorAll('input')[1].checked).toEqual(false)
     expect(document.querySelectorAll('input')[1].checked).toEqual(false)
     expect(document.querySelector('span').getAttribute('bar')).toEqual("bar")
     expect(document.querySelector('span').getAttribute('bar')).toEqual("bar")
 
 
-    fireEvent.change(document.querySelectorAll('input')['1'], { target: { checked: true }})
+    fireEvent.change(document.querySelectorAll('input')['1'], { target: { checked: true } })
 
 
     await wait(() => {
     await wait(() => {
         expect(document.querySelectorAll('input')[0].checked).toEqual(true)
         expect(document.querySelectorAll('input')[0].checked).toEqual(true)
@@ -192,16 +192,16 @@ test('x-model checkbox array binding supports .number modifier', async () => {
     expect(document.querySelectorAll('input[type=checkbox]')[2].checked).toEqual(false)
     expect(document.querySelectorAll('input[type=checkbox]')[2].checked).toEqual(false)
     expect(document.querySelector('span').getAttribute('bar')).toEqual("[2]")
     expect(document.querySelector('span').getAttribute('bar')).toEqual("[2]")
 
 
-    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[2], { target: { checked: true }})
+    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[2], { target: { checked: true } })
 
 
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("[2,3]") })
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("[2,3]") })
 
 
-    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[0], { target: { checked: true }})
+    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[0], { target: { checked: true } })
 
 
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("[2,3,1]") })
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("[2,3,1]") })
 
 
-    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[0], { target: { checked: false }})
-    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[1], { target: { checked: false }})
+    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[0], { target: { checked: false } })
+    fireEvent.change(document.querySelectorAll('input[type=checkbox]')[1], { target: { checked: false } })
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("[3]") })
     await wait(() => { expect(document.querySelector('span').getAttribute('bar')).toEqual("[3]") })
 })
 })
 
 
@@ -221,7 +221,7 @@ test('x-model binds radio value', async () => {
     expect(document.querySelectorAll('input')[1].checked).toEqual(false)
     expect(document.querySelectorAll('input')[1].checked).toEqual(false)
     expect(document.querySelector('span').getAttribute('bar')).toEqual('bar')
     expect(document.querySelector('span').getAttribute('bar')).toEqual('bar')
 
 
-    fireEvent.change(document.querySelectorAll('input')[1], { target: { checked: true }})
+    fireEvent.change(document.querySelectorAll('input')[1], { target: { checked: true } })
 
 
     await wait(() => {
     await wait(() => {
         expect(document.querySelectorAll('input')[0].checked).toEqual(false)
         expect(document.querySelectorAll('input')[0].checked).toEqual(false)
@@ -250,7 +250,7 @@ test('x-model binds select dropdown', async () => {
     expect(document.querySelectorAll('option')[2].selected).toEqual(false)
     expect(document.querySelectorAll('option')[2].selected).toEqual(false)
     expect(document.querySelector('span').innerText).toEqual('bar')
     expect(document.querySelector('span').innerText).toEqual('bar')
 
 
-    fireEvent.change(document.querySelector('select'), { target: { value: 'baz' }});
+    fireEvent.change(document.querySelector('select'), { target: { value: 'baz' } });
 
 
     await wait(() => {
     await wait(() => {
         expect(document.querySelectorAll('option')[0].selected).toEqual(false)
         expect(document.querySelectorAll('option')[0].selected).toEqual(false)
@@ -304,7 +304,7 @@ test('x-model binds nested keys', async () => {
     expect(document.querySelector('input').value).toEqual('foo')
     expect(document.querySelector('input').value).toEqual('foo')
     expect(document.querySelector('span').innerText).toEqual('foo')
     expect(document.querySelector('span').innerText).toEqual('foo')
 
 
-    fireEvent.input(document.querySelector('input'), { target: { value: 'bar' }})
+    fireEvent.input(document.querySelector('input'), { target: { value: 'bar' } })
 
 
     await wait(() => {
     await wait(() => {
         expect(document.querySelector('input').value).toEqual('bar')
         expect(document.querySelector('input').value).toEqual('bar')
@@ -325,7 +325,7 @@ test('x-model undefined nested model key defaults to empty string', async () =>
     expect(document.querySelector('input').value).toEqual('')
     expect(document.querySelector('input').value).toEqual('')
     expect(document.querySelector('span').innerText).toEqual('')
     expect(document.querySelector('span').innerText).toEqual('')
 
 
-    fireEvent.input(document.querySelector('input'), { target: { value: 'bar' }})
+    fireEvent.input(document.querySelector('input'), { target: { value: 'bar' } })
 
 
     await wait(() => {
     await wait(() => {
         expect(document.querySelector('input').value).toEqual('bar')
         expect(document.querySelector('input').value).toEqual('bar')
@@ -352,3 +352,374 @@ test('x-model can listen for custom input event dispatches', async () => {
         expect(document.querySelector('span').innerText).toEqual('baz')
         expect(document.querySelector('span').innerText).toEqual('baz')
     })
     })
 })
 })
+
+// <input type="color">
+test('x-model bind color input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '#ff0000' }">
+        <input type="color" x-model="key">
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('#ff0000')
+    expect(document.querySelector('span').innerText).toEqual('#ff0000')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: '#00ff00' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('#00ff00')
+        expect(document.querySelector('span').innerText).toEqual('#00ff00')
+    })
+
+})
+
+// <input type="button">
+test('x-model bind button input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: 'foo' }">
+        <input type="button" x-model="key">
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('foo')
+    expect(document.querySelector('span').innerText).toEqual('foo')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: 'bar' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('bar')
+        expect(document.querySelector('span').innerText).toEqual('bar')
+    })
+})
+
+// <input type="date">
+test('x-model bind date input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '2020-07-10' }">
+      <input type="date" x-model="key" />
+      <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('2020-07-10')
+    expect(document.querySelector('span').innerText).toEqual('2020-07-10')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: '2021-01-01' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('2021-01-01')
+        expect(document.querySelector('span').innerText).toEqual('2021-01-01')
+    })
+})
+
+// <input type="datetime-local">
+test('x-model bind datetime-local input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '2020-01-01T20:00' }">
+      <input type="datetime-local" x-model="key" />
+      <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('2020-01-01T20:00')
+    expect(document.querySelector('span').innerText).toEqual('2020-01-01T20:00')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: '2021-02-02T20:00' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('2021-02-02T20:00')
+        expect(document.querySelector('span').innerText).toEqual('2021-02-02T20:00')
+    })
+})
+
+// <input type="email">
+test('x-model bind email input', async () => {
+})
+
+// <input type="month">
+test('x-model bind month input', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ key: '2020-04' }">
+        <input type="month" x-model="key" />
+        <span x-text="key"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('2020-04')
+    expect(document.querySelector('span').innerText).toEqual('2020-04')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: '2021-05' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('2021-05')
+        expect(document.querySelector('span').innerText).toEqual('2021-05')
+    })
+})
+
+
+// <input type="number">
+test('x-model bind number input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '11' }">
+        <input type="number" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('11')
+    expect(document.querySelector('span').innerText).toEqual('11')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: '2021' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('2021')
+        expect(document.querySelector('span').innerText).toEqual('2021')
+    })
+})
+
+// <input type="password">
+test('x-model bind password input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: 'SecretKey' }">
+        <input type="password" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('SecretKey')
+    expect(document.querySelector('span').innerText).toEqual('SecretKey')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: 'NewSecretKey' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('NewSecretKey')
+        expect(document.querySelector('span').innerText).toEqual('NewSecretKey')
+    })
+})
+
+// <input type="range">
+test('x-model bind range input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '10' }">
+        <input type="range" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('10')
+    expect(document.querySelector('span').innerText).toEqual('10')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: '20' } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('20')
+        expect(document.querySelector('span').innerText).toEqual('20')
+    })
+})
+
+// <input type="search">
+test('x-model bind search input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '' }">
+        <input type="search" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('')
+    expect(document.querySelector('span').innerText).toEqual('')
+
+    const newValue = 'Frontend Frameworks';
+    fireEvent.input(document.querySelector('input'), { target: { value: newValue } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual(newValue)
+        expect(document.querySelector('span').innerText).toEqual(newValue)
+    })
+})
+
+// <input type="tel">
+test('x-model bind tel input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '+12345678901' }">
+        <input type="tel" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('+12345678901')
+    expect(document.querySelector('span').innerText).toEqual('+12345678901')
+
+    const newValue = '+1239874560';
+    fireEvent.input(document.querySelector('input'), { target: { value: newValue } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual(newValue)
+        expect(document.querySelector('span').innerText).toEqual(newValue)
+    })
+})
+
+// <input type="tel">
+test('x-model bind tel input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '+12345678901' }">
+        <input type="tel" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('+12345678901')
+    expect(document.querySelector('span').innerText).toEqual('+12345678901')
+
+    const newValue = '+1239874560';
+    fireEvent.input(document.querySelector('input'), { target: { value: newValue } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual(newValue)
+        expect(document.querySelector('span').innerText).toEqual(newValue)
+    })
+})
+
+// <input type="tel">
+test('x-model bind time input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '22:00' }">
+        <input type="time" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('22:00')
+    expect(document.querySelector('span').innerText).toEqual('22:00')
+
+    const newValue = '23:00';
+    fireEvent.input(document.querySelector('input'), { target: { value: newValue } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual(newValue)
+        expect(document.querySelector('span').innerText).toEqual(newValue)
+    })
+})
+
+// <input type="time">
+test('x-model bind time input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '22:00' }">
+        <input type="time" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('22:00')
+    expect(document.querySelector('span').innerText).toEqual('22:00')
+
+    const newValue = '23:00';
+    fireEvent.input(document.querySelector('input'), { target: { value: newValue } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual(newValue)
+        expect(document.querySelector('span').innerText).toEqual(newValue)
+    })
+})
+
+// <input type="week">
+test('x-model bind week input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: '2020-W20' }">
+        <input type="week" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('2020-W20')
+    expect(document.querySelector('span').innerText).toEqual('2020-W20')
+
+    const newValue = '2020-W30';
+    fireEvent.input(document.querySelector('input'), { target: { value: newValue } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual(newValue)
+        expect(document.querySelector('span').innerText).toEqual(newValue)
+    })
+})
+
+// <input type="url">
+test('x-model bind url input', async () => {
+    document.body.innerHTML = `
+    <div x-data="{ key: 'https://example.com' }">
+        <input type="url" x-model="key" />
+        <span x-text="key"></span>
+    </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('input').value).toEqual('https://example.com')
+    expect(document.querySelector('span').innerText).toEqual('https://example.com')
+
+    const newValue = 'https://alpine.io';
+    fireEvent.input(document.querySelector('input'), { target: { value: newValue } });
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual(newValue)
+        expect(document.querySelector('span').innerText).toEqual(newValue)
+    })
+})
+
+test('x-model sets value before x-on directive expression is processed', async () => {
+    window.selectValueA
+    window.selectValueB
+
+    document.body.innerHTML = `
+        <div x-data="{ a: 'foo', b: 'foo' }">
+            <select x-model="a" @change="window.selectValueA = a">
+                <option>foo</option>
+                <option>bar</option>
+            </select>
+            <select @change="window.selectValueB = b" x-model="b">
+                <option>foo</option>
+                <option>bar</option>
+            </select>
+        </div>
+    `
+
+    Alpine.start()
+
+    fireEvent.change(document.querySelectorAll('select')[0], { target: { value: 'bar' } });
+    fireEvent.change(document.querySelectorAll('select')[1], { target: { value: 'bar' } });
+
+    await wait(() => {
+        expect(window.selectValueA).toEqual('bar')
+        expect(window.selectValueB).toEqual('bar')
+    })
+})

+ 21 - 0
test/on.spec.js

@@ -519,3 +519,24 @@ test('autocomplete event does not trigger keydown with modifier callback', async
 
 
     await wait(() => { expect(document.querySelector('span').innerText).toEqual(1) })
     await wait(() => { expect(document.querySelector('span').innerText).toEqual(1) })
 })
 })
+
+test('.camel modifier correctly binds event listener', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ foo: 'bar' }" x-on:event-name.camel.window="foo = 'bob'">
+            <button x-on:click="$dispatch('eventName')"></button>
+            <p x-text="foo"></p>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('p').innerText).toEqual('bar')
+
+    document.querySelector('button').click();
+
+    await wait(() => {
+        expect(document.querySelector('p').innerText).toEqual('bob');
+    });
+})
+
+

+ 47 - 0
test/show.spec.js

@@ -126,3 +126,50 @@ test('x-show works with nested x-shows of different functions (hiding vs showing
         expect(document.querySelector('h1').getAttribute('style')).toEqual(null)
         expect(document.querySelector('h1').getAttribute('style')).toEqual(null)
     })
     })
 })
 })
+
+// Regression in 2.4.0
+test('x-show with x-bind:style inside x-for works correctly', async () => {
+    document.body.innerHTML = `
+        <div x-data="{items: [{ cleared: false }, { cleared: false }]}">
+            <template x-for="(item, index) in items" :key="index">
+                <button x-show="! item.cleared"
+                    x-bind:style="'background: #999'"
+                    @click="item.cleared = true"
+                >
+                </button>
+            </template>
+        </div>
+    `
+    Alpine.start()
+
+    expect(document.querySelectorAll('button')[0].style.display).toEqual('')
+    expect(document.querySelectorAll('button')[1].style.display).toEqual('')
+
+    document.querySelectorAll('button')[0].click()
+
+    await wait(() => {
+        expect(document.querySelectorAll('button')[0].style.display).toEqual('none')
+        expect(document.querySelectorAll('button')[1].style.display).toEqual('')
+    })
+
+    document.querySelectorAll('button')[1].click()
+
+    await wait(() => {
+        expect(document.querySelectorAll('button')[0].style.display).toEqual('none')
+        expect(document.querySelectorAll('button')[1].style.display).toEqual('none')
+    })
+})
+
+test('x-show takes precedence over style bindings for display property', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ show: false }">
+            <span x-show="show" :style="'color: red;'"></span>
+            <span :style="'color: red;'" x-show="show"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelectorAll('span')[0].getAttribute('style')).toContain('display: none;')
+    expect(document.querySelectorAll('span')[1].getAttribute('style')).toContain('display: none;')
+})

+ 28 - 0
test/spread.spec.js

@@ -146,3 +146,31 @@ test('x-spread syntax supports x-transition', async () => {
         }, 10)
         }, 10)
     )
     )
 })
 })
+
+
+test('x-spread event handlers defined as functions receive the event object as their first argument', async () => {    
+    window.data = function () {
+        return {
+            eventType: null, 
+            button: {
+                ['@click']($event){
+                    this.eventType = $event.type;
+                }
+            }
+        };
+    };
+
+    document.body.innerHTML = `
+        <div x-data="window.data()">
+            <button x-spread="button">click me<button>
+        </div>
+    `;
+
+    Alpine.start();
+
+    document.querySelector("button").click();
+
+    await wait(() => {
+        expect(document.querySelector("div").__x.$data.eventType).toEqual("click");
+    });
+});

+ 125 - 13
test/transition.spec.js

@@ -135,15 +135,12 @@ test('transition out', async () => {
     expect(document.querySelector('span').classList.contains('leave-end')).toEqual(true)
     expect(document.querySelector('span').classList.contains('leave-end')).toEqual(true)
     expect(document.querySelector('span').getAttribute('style')).toEqual(null)
     expect(document.querySelector('span').getAttribute('style')).toEqual(null)
 
 
-    await new Promise((resolve) =>
-        setTimeout(() => {
-            expect(document.querySelector('span').classList.contains('leave')).toEqual(false)
-            expect(document.querySelector('span').classList.contains('leave-start')).toEqual(false)
-            expect(document.querySelector('span').classList.contains('leave-end')).toEqual(false)
-            expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
-            resolve();
-        }, 10)
-    )
+    await timeout(10)
+
+    expect(document.querySelector('span').classList.contains('leave')).toEqual(false)
+    expect(document.querySelector('span').classList.contains('leave-start')).toEqual(false)
+    expect(document.querySelector('span').classList.contains('leave-end')).toEqual(false)
+    expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
 })
 })
 
 
 test('if only transition leave directives are present, don\'t transition in at all', async () => {
 test('if only transition leave directives are present, don\'t transition in at all', async () => {
@@ -281,8 +278,8 @@ test('transition in not called when item is already visible', async () => {
     });
     });
 
 
     document.body.innerHTML = `
     document.body.innerHTML = `
-        <div x-data="{ show: true }">
-            <button x-on:click="show = true"></button>
+        <div x-data="{ show: true, foo: 'bar' }">
+            <button x-on:click="foo = 'bob'"></button>
 
 
             <span
             <span
                 x-show="show"
                 x-show="show"
@@ -330,8 +327,8 @@ test('transition out not called when item is already hidden', async () => {
     });
     });
 
 
     document.body.innerHTML = `
     document.body.innerHTML = `
-        <div x-data="{ show: false }">
-            <button x-on:click="show = false"></button>
+        <div x-data="{ show: false, foo: 'bar' }">
+            <button x-on:click="foo = 'bob'"></button>
 
 
             <span
             <span
                 x-show="show"
                 x-show="show"
@@ -620,3 +617,118 @@ test('x-transition supports css animation', async () => {
     )
     )
     expect(document.querySelector('span').classList.contains('animation-leave')).toEqual(false)
     expect(document.querySelector('span').classList.contains('animation-leave')).toEqual(false)
 })
 })
+
+test('x-transition do not overlap', async () => {
+    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
+        setTimeout(callback, 0)
+    });
+
+    document.body.innerHTML = `
+        <div x-data="{ show: true }">
+            <button x-on:click="show = ! show"></button>
+
+            <span x-show.transition="show"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    // Initial state
+    expect(document.querySelector('span').style.display).toEqual("")
+    expect(document.querySelector('span').style.opacity).toEqual("")
+    expect(document.querySelector('span').style.transform).toEqual("")
+    expect(document.querySelector('span').style.transformOrigin).toEqual("")
+
+    // Trigger transition out
+    document.querySelector('button').click()
+
+    // Trigger transition in before the previous one has finished
+    await timeout(10)
+    document.querySelector('button').click()
+
+    // Check the element is still visible and style properties are correct
+    await timeout(200)
+    expect(document.querySelector('span').style.display).toEqual("")
+    expect(document.querySelector('span').style.opacity).toEqual("")
+    expect(document.querySelector('span').style.transform).toEqual("")
+    expect(document.querySelector('span').style.transformOrigin).toEqual("")
+
+    // Hide the element
+    document.querySelector('button').click()
+    await timeout(200)
+    expect(document.querySelector('span').style.display).toEqual("none")
+    expect(document.querySelector('span').style.opacity).toEqual("")
+    expect(document.querySelector('span').style.transform).toEqual("")
+    expect(document.querySelector('span').style.transformOrigin).toEqual("")
+
+    // Trigger transition in
+    document.querySelector('button').click()
+
+    // Trigger transition out before the previous one has finished
+    await timeout(10)
+    document.querySelector('button').click()
+
+    // Check the element is hidden and style properties are correct
+    await timeout(200)
+    expect(document.querySelector('span').style.display).toEqual("none")
+    expect(document.querySelector('span').style.opacity).toEqual("")
+    expect(document.querySelector('span').style.transform).toEqual("")
+    expect(document.querySelector('span').style.transformOrigin).toEqual("")
+})
+
+test('x-transition using classes do not overlap', async () => {
+    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
+        setTimeout(callback, 0)
+    });
+    jest.spyOn(window, 'getComputedStyle').mockImplementation(el => {
+        return { transitionDuration: '.1s' }
+    });
+
+    document.body.innerHTML = `
+        <div x-data="{ show: true }">
+            <button x-on:click="show = ! show"></button>
+
+            <span x-show="show"
+                x-transition:enter="enter"
+                x-transition:leave="leave">
+            </span>
+        </div>
+    `
+
+    Alpine.start()
+
+    // Initial state
+    expect(document.querySelector('span').style.display).toEqual("")
+
+    const emptyClassList = document.querySelector('span').classList
+
+    // Trigger transition out
+    document.querySelector('button').click()
+
+    // Trigger transition in before the previous one has finished
+    await timeout(10)
+    document.querySelector('button').click()
+
+    // Check the element is still visible and class property is correct
+    await timeout(200)
+    expect(document.querySelector('span').style.display).toEqual("")
+    expect(document.querySelector('span').classList).toEqual(emptyClassList)
+
+    // Hide the element
+    document.querySelector('button').click()
+    await timeout(200)
+    expect(document.querySelector('span').style.display).toEqual("none")
+    expect(document.querySelector('span').classList).toEqual(emptyClassList)
+
+    // Trigger transition in
+    document.querySelector('button').click()
+
+    // Trigger transition out before the previous one has finished
+    await timeout(10)
+    document.querySelector('button').click()
+
+    // Check the element is hidden and class property is correct
+    await timeout(200)
+    expect(document.querySelector('span').style.display).toEqual("none")
+    expect(document.querySelector('span').classList).toEqual(emptyClassList)
+})

部分文件因文件數量過多而無法顯示