Browse Source

Fix overlapping transitions with parent x-shows

Caleb Porzio 4 years ago
parent
commit
e9bbfdb285
8 changed files with 299 additions and 186 deletions
  1. 96 79
      dist/alpine-ie11.js
  2. 59 50
      dist/alpine.js
  3. 12 10
      src/component.js
  4. 3 3
      src/directives/for.js
  5. 2 2
      src/directives/if.js
  6. 8 5
      src/directives/show.js
  7. 43 37
      src/utils.js
  8. 76 0
      test/transition.spec.js

+ 96 - 79
dist/alpine-ie11.js

@@ -5900,10 +5900,11 @@
   }
   var TRANSITION_TYPE_IN = 'in';
   var TRANSITION_TYPE_OUT = 'out';
-  function transitionIn(el, show, component) {
+  var TRANSITION_CANCELLED = 'cancelled';
+  function transitionIn(el, show, reject, component) {
     var _this6 = this;
 
-    var forceSkip = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+    var forceSkip = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
     // We don't want to transition on the initial page load.
     if (forceSkip) return show();
 
@@ -5927,22 +5928,22 @@
 
         return index < modifiers.indexOf('out');
       }.bind(this)) : modifiers;
-      transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter.
+      transitionHelperIn(el, modifiers, show, reject); // Otherwise, we can assume x-transition:enter.
     } else if (attrs.some(function (attr) {
       _newArrowCheck(this, _this6);
 
       return ['enter', 'enter-start', 'enter-end'].includes(attr.value);
     }.bind(this))) {
-      transitionClassesIn(el, component, attrs, show);
+      transitionClassesIn(el, component, attrs, show, reject);
     } else {
       // If neither, just show that damn thing.
       show();
     }
   }
-  function transitionOut(el, hide, component) {
+  function transitionOut(el, hide, reject, component) {
     var _this7 = this;
 
-    var forceSkip = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+    var forceSkip = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
     // We don't want to transition on the initial page load.
     if (forceSkip) return hide();
 
@@ -5964,18 +5965,18 @@
 
         return index > modifiers.indexOf('out');
       }.bind(this)) : modifiers;
-      transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
+      transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide, reject);
     } else if (attrs.some(function (attr) {
       _newArrowCheck(this, _this7);
 
       return ['leave', 'leave-start', 'leave-end'].includes(attr.value);
     }.bind(this))) {
-      transitionClassesOut(el, component, attrs, hide);
+      transitionClassesOut(el, component, attrs, hide, reject);
     } else {
       hide();
     }
   }
-  function transitionHelperIn(el, modifiers, showCallback) {
+  function transitionHelperIn(el, modifiers, showCallback, reject) {
     var _this8 = this;
 
     // Default values inspired by: https://material.io/design/motion/speed.html#duration
@@ -5993,9 +5994,9 @@
     };
     transitionHelper(el, modifiers, showCallback, function () {
       _newArrowCheck(this, _this8);
-    }.bind(this), styleValues, TRANSITION_TYPE_IN);
+    }.bind(this), reject, styleValues, TRANSITION_TYPE_IN);
   }
-  function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
+  function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback, reject) {
     var _this9 = this;
 
     // Make the "out" transition .5x slower than the "in". (Visually better)
@@ -6016,7 +6017,7 @@
     };
     transitionHelper(el, modifiers, function () {
       _newArrowCheck(this, _this9);
-    }.bind(this), hideCallback, styleValues, TRANSITION_TYPE_OUT);
+    }.bind(this), hideCallback, reject, styleValues, TRANSITION_TYPE_OUT);
   }
 
   function modifierValue(modifiers, key, fallback) {
@@ -6049,11 +6050,10 @@
     return rawValue;
   }
 
-  function transitionHelper(el, modifiers, hook1, hook2, styleValues, type) {
+  function transitionHelper(el, modifiers, hook1, hook2, reject, 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();
+      el.__x_transition.cancel && el.__x_transition.cancel();
     } // If the user set these style values, we'll put them back when we're done with them.
 
 
@@ -6097,7 +6097,7 @@
         el.style.transitionTimingFunction = null;
       }
     };
-    transition(el, stages, type);
+    transition(el, stages, type, reject);
   }
 
   var ensureStringExpression = function ensureStringExpression(expression, el, component) {
@@ -6106,7 +6106,7 @@
     return typeof expression === 'function' ? component.evaluateReturnExpression(el, expression) : expression;
   }.bind(undefined);
 
-  function transitionClassesIn(el, component, directives, showCallback) {
+  function transitionClassesIn(el, component, directives, showCallback, reject) {
     var _this11 = this;
 
     var enter = convertClassStringToArray(ensureStringExpression((directives.find(function (i) {
@@ -6132,9 +6132,9 @@
     }).expression, el, component));
     transitionClasses(el, enter, enterStart, enterEnd, showCallback, function () {
       _newArrowCheck(this, _this11);
-    }.bind(this), TRANSITION_TYPE_IN);
+    }.bind(this), TRANSITION_TYPE_IN, reject);
   }
-  function transitionClassesOut(el, component, directives, hideCallback) {
+  function transitionClassesOut(el, component, directives, hideCallback, reject) {
     var _this12 = this;
 
     var leave = convertClassStringToArray(ensureStringExpression((directives.find(function (i) {
@@ -6160,13 +6160,12 @@
     }).expression, el, component));
     transitionClasses(el, leave, leaveStart, leaveEnd, function () {
       _newArrowCheck(this, _this12);
-    }.bind(this), hideCallback, TRANSITION_TYPE_OUT);
+    }.bind(this), hideCallback, TRANSITION_TYPE_OUT, reject);
   }
-  function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type) {
+  function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type, reject) {
     // 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();
+      el.__x_transition.cancel && el.__x_transition.cancel();
     }
 
     var originalClasses = el.__x_original_classes || [];
@@ -6219,29 +6218,36 @@
         }.bind(this))));
       }
     };
-    transition(el, stages, type);
+    transition(el, stages, type, reject);
   }
-  function transition(el, stages, type) {
+  function transition(el, stages, type, reject) {
     var _this15 = this;
 
+    var finish = once(function () {
+      _newArrowCheck(this, _this15);
+
+      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;
+    }.bind(this));
     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(function () {
+      cancel: once(function () {
         _newArrowCheck(this, _this15);
 
-        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;
+        reject(TRANSITION_CANCELLED);
+        finish();
       }.bind(this)),
+      finish: finish,
       // This store the next animation frame so we can cancel it
       nextFrame: null
     };
@@ -6265,7 +6271,7 @@
         _newArrowCheck(this, _this16);
 
         stages.end();
-        setTimeout(el.__x_transition.callback, duration);
+        setTimeout(el.__x_transition.finish, duration);
       }.bind(this));
     }.bind(this));
   }
@@ -6306,6 +6312,8 @@
 
         transitionIn(nextEl, function () {
           _newArrowCheck(this, _this2);
+        }.bind(this), function () {
+          _newArrowCheck(this, _this2);
         }.bind(this), component, initialUpdate);
         nextEl.__x_for = iterationScopeVariables;
         component.initializeElements(nextEl, function () {
@@ -6438,6 +6446,8 @@
         _newArrowCheck(this, _this5);
 
         nextElementFromOldLoopImmutable.remove();
+      }.bind(this), function () {
+        _newArrowCheck(this, _this5);
       }.bind(this), component);
       nextElementFromOldLoop = nextSibling && nextSibling.__x_for_key !== undefined ? nextSibling : false;
     };
@@ -6585,6 +6595,7 @@
       _newArrowCheck(this, _this);
 
       el.style.display = 'none';
+      el.__x_is_shown = false;
     }.bind(this);
 
     var show = function show() {
@@ -6595,6 +6606,8 @@
       } else {
         el.style.removeProperty('display');
       }
+
+      el.__x_is_shown = true;
     }.bind(this);
 
     if (initialUpdate === true) {
@@ -6607,7 +6620,7 @@
       return;
     }
 
-    var handle = function handle(resolve) {
+    var handle = function handle(resolve, reject) {
       var _this2 = this;
 
       _newArrowCheck(this, _this);
@@ -6618,7 +6631,7 @@
             _newArrowCheck(this, _this2);
 
             show();
-          }.bind(this), component);
+          }.bind(this), reject, component);
         }
 
         resolve(function () {
@@ -6636,7 +6649,7 @@
 
               hide();
             }.bind(this));
-          }.bind(this), component);
+          }.bind(this), reject, component);
         } else {
           resolve(function () {
             _newArrowCheck(this, _this2);
@@ -6654,6 +6667,8 @@
         _newArrowCheck(this, _this);
 
         return finish();
+      }.bind(this), function () {
+        _newArrowCheck(this, _this);
       }.bind(this));
       return;
     } // x-show is encountered during a DOM tree walk. If an element
@@ -6680,6 +6695,8 @@
       el.parentElement.insertBefore(clone, el.nextElementSibling);
       transitionIn(el.nextElementSibling, function () {
         _newArrowCheck(this, _this);
+      }.bind(this), function () {
+        _newArrowCheck(this, _this);
       }.bind(this), component, initialUpdate);
       component.initializeElements(el.nextElementSibling, extraVars);
       el.nextElementSibling.__x_inserted_me = true;
@@ -6688,6 +6705,8 @@
         _newArrowCheck(this, _this);
 
         el.nextElementSibling.remove();
+      }.bind(this), function () {
+        _newArrowCheck(this, _this);
       }.bind(this), component, initialUpdate);
     }
   }
@@ -7416,41 +7435,39 @@
         // The goal here is to start all the x-show transitions
         // and build a nested promise chain so that elements
         // only hide when the children are finished hiding.
-        this.showDirectiveStack.reverse().map(function (thing) {
+        this.showDirectiveStack.reverse().map(function (handler) {
           var _this14 = this;
 
           _newArrowCheck(this, _this13);
 
-          return new Promise(function (resolve) {
-            var _this15 = this;
-
+          return new Promise(function (resolve, reject) {
             _newArrowCheck(this, _this14);
 
-            thing(function (finish) {
-              _newArrowCheck(this, _this15);
-
-              resolve(finish);
-            }.bind(this));
+            handler(resolve, reject);
           }.bind(this));
-        }.bind(this)).reduce(function (nestedPromise, promise) {
-          var _this16 = this;
+        }.bind(this)).reduce(function (promiseChain, promise) {
+          var _this15 = this;
 
           _newArrowCheck(this, _this13);
 
-          return nestedPromise.then(function () {
-            var _this17 = this;
+          return promiseChain.then(function () {
+            var _this16 = this;
 
-            _newArrowCheck(this, _this16);
+            _newArrowCheck(this, _this15);
 
-            return promise.then(function (finish) {
-              _newArrowCheck(this, _this17);
+            return promise.then(function (finishElement) {
+              _newArrowCheck(this, _this16);
 
-              return finish();
+              finishElement();
             }.bind(this));
           }.bind(this));
         }.bind(this), Promise.resolve(function () {
           _newArrowCheck(this, _this13);
-        }.bind(this))); // We've processed the handler stack. let's clear it.
+        }.bind(this)))["catch"](function (e) {
+          _newArrowCheck(this, _this13);
+
+          if (e !== TRANSITION_CANCELLED) throw e;
+        }.bind(this)); // We've processed the handler stack. let's clear it.
 
         this.showDirectiveStack = [];
         this.showDirectiveLastElement = undefined;
@@ -7463,10 +7480,10 @@
     }, {
       key: "registerListeners",
       value: function registerListeners(el, extraVars) {
-        var _this18 = this;
+        var _this17 = this;
 
         getXAttrs(el, this).forEach(function (_ref5) {
-          _newArrowCheck(this, _this18);
+          _newArrowCheck(this, _this17);
 
           var type = _ref5.type,
               value = _ref5.value,
@@ -7487,15 +7504,15 @@
     }, {
       key: "resolveBoundAttributes",
       value: function resolveBoundAttributes(el) {
-        var _this19 = this;
+        var _this18 = this;
 
         var initialUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
         var extraVars = arguments.length > 2 ? arguments[2] : undefined;
         var attrs = getXAttrs(el, this);
         attrs.forEach(function (_ref6) {
-          var _this20 = this;
+          var _this19 = this;
 
-          _newArrowCheck(this, _this19);
+          _newArrowCheck(this, _this18);
 
           var type = _ref6.type,
               value = _ref6.value,
@@ -7531,7 +7548,7 @@
               // 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.
               if (attrs.some(function (i) {
-                _newArrowCheck(this, _this20);
+                _newArrowCheck(this, _this19);
 
                 return i.type === 'for';
               }.bind(this))) return;
@@ -7552,10 +7569,10 @@
     }, {
       key: "evaluateReturnExpression",
       value: function evaluateReturnExpression(el, expression) {
-        var _this21 = this;
+        var _this20 = this;
 
         var extraVars = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {
-          _newArrowCheck(this, _this21);
+          _newArrowCheck(this, _this20);
         }.bind(this);
         return saferEval(expression, this.$data, _objectSpread2(_objectSpread2({}, extraVars()), {}, {
           $dispatch: this.getDispatchFunction(el)
@@ -7564,10 +7581,10 @@
     }, {
       key: "evaluateCommandExpression",
       value: function evaluateCommandExpression(el, expression) {
-        var _this22 = this;
+        var _this21 = this;
 
         var extraVars = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {
-          _newArrowCheck(this, _this22);
+          _newArrowCheck(this, _this21);
         }.bind(this);
         return saferEvalNoReturn(expression, this.$data, _objectSpread2(_objectSpread2({}, extraVars()), {}, {
           $dispatch: this.getDispatchFunction(el)
@@ -7587,7 +7604,7 @@
     }, {
       key: "listenForNewElementsToInitialize",
       value: function listenForNewElementsToInitialize() {
-        var _this23 = this;
+        var _this22 = this;
 
         var targetNode = this.$el;
         var observerOptions = {
@@ -7596,9 +7613,9 @@
           subtree: true
         };
         var observer = new MutationObserver(function (mutations) {
-          var _this24 = this;
+          var _this23 = this;
 
-          _newArrowCheck(this, _this23);
+          _newArrowCheck(this, _this22);
 
           for (var i = 0; i < mutations.length; i++) {
             // Filter out mutations triggered from child components.
@@ -7607,16 +7624,16 @@
 
             if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
               (function () {
-                var _this25 = this;
+                var _this24 = this;
 
                 var rawData = saferEval(mutations[i].target.getAttribute('x-data') || '{}', {
-                  $el: _this24.$el
+                  $el: _this23.$el
                 });
                 Object.keys(rawData).forEach(function (key) {
-                  _newArrowCheck(this, _this25);
+                  _newArrowCheck(this, _this24);
 
-                  if (_this24.$data[key] !== rawData[key]) {
-                    _this24.$data[key] = rawData[key];
+                  if (_this23.$data[key] !== rawData[key]) {
+                    _this23.$data[key] = rawData[key];
                   }
                 }.bind(this));
               })();
@@ -7624,7 +7641,7 @@
 
             if (mutations[i].addedNodes.length > 0) {
               mutations[i].addedNodes.forEach(function (node) {
-                _newArrowCheck(this, _this24);
+                _newArrowCheck(this, _this23);
 
                 if (node.nodeType !== 1 || node.__x_inserted_me) return;
 
@@ -7643,7 +7660,7 @@
     }, {
       key: "getRefsProxy",
       value: function getRefsProxy() {
-        var _this26 = this;
+        var _this25 = this;
 
         var self = this;
         var refObj = {};
@@ -7655,7 +7672,7 @@
         // we just loop on the element, look for any x-ref and create a tmp property on a fake object.
 
         this.walkAndSkipNestedComponents(self.$el, function (el) {
-          _newArrowCheck(this, _this26);
+          _newArrowCheck(this, _this25);
 
           if (el.hasAttribute('x-ref')) {
             refObj[el.getAttribute('x-ref')] = true;
@@ -7669,14 +7686,14 @@
 
         return new Proxy(refObj, {
           get: function get(object, property) {
-            var _this27 = this;
+            var _this26 = this;
 
             if (property === '$isAlpineProxy') return true;
             var ref; // We can't just query the DOM because it's hard to filter out refs in
             // nested components.
 
             self.walkAndSkipNestedComponents(self.$el, function (el) {
-              _newArrowCheck(this, _this27);
+              _newArrowCheck(this, _this26);
 
               if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
                 ref = el;

+ 59 - 50
dist/alpine.js

@@ -209,7 +209,8 @@
   }
   const TRANSITION_TYPE_IN = 'in';
   const TRANSITION_TYPE_OUT = 'out';
-  function transitionIn(el, show, component, forceSkip = false) {
+  const TRANSITION_CANCELLED = 'cancelled';
+  function transitionIn(el, show, reject, component, forceSkip = false) {
     // We don't want to transition on the initial page load.
     if (forceSkip) return show();
 
@@ -229,15 +230,15 @@
       const 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((i, index) => index < modifiers.indexOf('out')) : modifiers;
-      transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter.
+      transitionHelperIn(el, modifiers, show, reject); // Otherwise, we can assume x-transition:enter.
     } else if (attrs.some(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value))) {
-      transitionClassesIn(el, component, attrs, show);
+      transitionClassesIn(el, component, attrs, show, reject);
     } else {
       // If neither, just show that damn thing.
       show();
     }
   }
-  function transitionOut(el, hide, component, forceSkip = false) {
+  function transitionOut(el, hide, reject, component, forceSkip = false) {
     // We don't want to transition on the initial page load.
     if (forceSkip) return hide();
 
@@ -255,14 +256,14 @@
       if (modifiers.includes('in') && !modifiers.includes('out')) return hide();
       const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out');
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers;
-      transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
+      transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide, reject);
     } else if (attrs.some(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value))) {
-      transitionClassesOut(el, component, attrs, hide);
+      transitionClassesOut(el, component, attrs, hide, reject);
     } else {
       hide();
     }
   }
-  function transitionHelperIn(el, modifiers, showCallback) {
+  function transitionHelperIn(el, modifiers, showCallback, reject) {
     // Default values inspired by: https://material.io/design/motion/speed.html#duration
     const styleValues = {
       duration: modifierValue(modifiers, 'duration', 150),
@@ -276,9 +277,9 @@
         scale: 100
       }
     };
-    transitionHelper(el, modifiers, showCallback, () => {}, styleValues, TRANSITION_TYPE_IN);
+    transitionHelper(el, modifiers, showCallback, () => {}, reject, styleValues, TRANSITION_TYPE_IN);
   }
-  function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
+  function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback, reject) {
     // Make the "out" transition .5x slower than the "in". (Visually better)
     // HOWEVER, if they explicitly set a duration for the "out" transition,
     // use that.
@@ -295,7 +296,7 @@
         scale: modifierValue(modifiers, 'scale', 95)
       }
     };
-    transitionHelper(el, modifiers, () => {}, hideCallback, styleValues, TRANSITION_TYPE_OUT);
+    transitionHelper(el, modifiers, () => {}, hideCallback, reject, styleValues, TRANSITION_TYPE_OUT);
   }
 
   function modifierValue(modifiers, key, fallback) {
@@ -328,11 +329,10 @@
     return rawValue;
   }
 
-  function transitionHelper(el, modifiers, hook1, hook2, styleValues, type) {
+  function transitionHelper(el, modifiers, hook1, hook2, reject, 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();
+      el.__x_transition.cancel && el.__x_transition.cancel();
     } // If the user set these style values, we'll put them back when we're done with them.
 
 
@@ -382,14 +382,14 @@
       }
 
     };
-    transition(el, stages, type);
+    transition(el, stages, type, reject);
   }
 
   const ensureStringExpression = (expression, el, component) => {
     return typeof expression === 'function' ? component.evaluateReturnExpression(el, expression) : expression;
   };
 
-  function transitionClassesIn(el, component, directives, showCallback) {
+  function transitionClassesIn(el, component, directives, showCallback, reject) {
     const enter = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter') || {
       expression: ''
     }).expression, el, component));
@@ -399,9 +399,9 @@
     const enterEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-end') || {
       expression: ''
     }).expression, el, component));
-    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}, TRANSITION_TYPE_IN);
+    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}, TRANSITION_TYPE_IN, reject);
   }
-  function transitionClassesOut(el, component, directives, hideCallback) {
+  function transitionClassesOut(el, component, directives, hideCallback, reject) {
     const leave = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'leave') || {
       expression: ''
     }).expression, el, component));
@@ -411,13 +411,12 @@
     const leaveEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'leave-end') || {
       expression: ''
     }).expression, el, component));
-    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback, TRANSITION_TYPE_OUT);
+    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback, TRANSITION_TYPE_OUT, reject);
   }
-  function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type) {
+  function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type, reject) {
     // 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();
+      el.__x_transition.cancel && el.__x_transition.cancel();
     }
 
     const originalClasses = el.__x_original_classes || [];
@@ -450,25 +449,30 @@
       }
 
     };
-    transition(el, stages, type);
+    transition(el, stages, type, reject);
   }
-  function transition(el, stages, type) {
+  function transition(el, stages, type, reject) {
+    const finish = 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;
+    });
     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;
+      cancel: once(() => {
+        reject(TRANSITION_CANCELLED);
+        finish();
       }),
+      finish,
       // This store the next animation frame so we can cancel it
       nextFrame: null
     };
@@ -486,7 +490,7 @@
       stages.show();
       el.__x_transition.nextFrame = requestAnimationFrame(() => {
         stages.end();
-        setTimeout(el.__x_transition.callback, duration);
+        setTimeout(el.__x_transition.finish, duration);
       });
     });
   }
@@ -519,7 +523,7 @@
       if (!nextEl) {
         nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl); // And transition it in if it's not the first page load.
 
-        transitionIn(nextEl, () => {}, component, initialUpdate);
+        transitionIn(nextEl, () => {}, () => {}, component, initialUpdate);
         nextEl.__x_for = iterationScopeVariables;
         component.initializeElements(nextEl, () => nextEl.__x_for); // Otherwise update the element we found.
       } else {
@@ -623,7 +627,7 @@
       let nextSibling = nextElementFromOldLoop.nextElementSibling;
       transitionOut(nextElementFromOldLoop, () => {
         nextElementFromOldLoopImmutable.remove();
-      }, component);
+      }, () => {}, component);
       nextElementFromOldLoop = nextSibling && nextSibling.__x_for_key !== undefined ? nextSibling : false;
     }
   }
@@ -731,6 +735,7 @@
   function handleShowDirective(component, el, value, modifiers, initialUpdate = false) {
     const hide = () => {
       el.style.display = 'none';
+      el.__x_is_shown = false;
     };
 
     const show = () => {
@@ -739,6 +744,8 @@
       } else {
         el.style.removeProperty('display');
       }
+
+      el.__x_is_shown = true;
     };
 
     if (initialUpdate === true) {
@@ -751,12 +758,12 @@
       return;
     }
 
-    const handle = resolve => {
+    const handle = (resolve, reject) => {
       if (value) {
         if (el.style.display === 'none' || el.__x_transition) {
           transitionIn(el, () => {
             show();
-          }, component);
+          }, reject, component);
         }
 
         resolve(() => {});
@@ -766,7 +773,7 @@
             resolve(() => {
               hide();
             });
-          }, component);
+          }, reject, component);
         } else {
           resolve(() => {});
         }
@@ -778,7 +785,7 @@
 
 
     if (modifiers.includes('immediate')) {
-      handle(finish => finish());
+      handle(finish => finish(), () => {});
       return;
     } // x-show is encountered during a DOM tree walk. If an element
     // we encounter is NOT a child of another x-show element we
@@ -800,13 +807,13 @@
     if (expressionResult && (!elementHasAlreadyBeenAdded || el.__x_transition)) {
       const clone = document.importNode(el.content, true);
       el.parentElement.insertBefore(clone, el.nextElementSibling);
-      transitionIn(el.nextElementSibling, () => {}, component, 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();
-      }, component, initialUpdate);
+      }, () => {}, component, initialUpdate);
     }
   }
 
@@ -1610,17 +1617,19 @@
       // The goal here is to start all the x-show transitions
       // and build a nested promise chain so that elements
       // only hide when the children are finished hiding.
-      this.showDirectiveStack.reverse().map(thing => {
-        return new Promise(resolve => {
-          thing(finish => {
-            resolve(finish);
-          });
+      this.showDirectiveStack.reverse().map(handler => {
+        return new Promise((resolve, reject) => {
+          handler(resolve, reject);
         });
-      }).reduce((nestedPromise, promise) => {
-        return nestedPromise.then(() => {
-          return promise.then(finish => finish());
+      }).reduce((promiseChain, promise) => {
+        return promiseChain.then(() => {
+          return promise.then(finishElement => {
+            finishElement();
+          });
         });
-      }, Promise.resolve(() => {})); // We've processed the handler stack. let's clear it.
+      }, Promise.resolve(() => {})).catch(e => {
+        if (e !== TRANSITION_CANCELLED) throw e;
+      }); // We've processed the handler stack. let's clear it.
 
       this.showDirectiveStack = [];
       this.showDirectiveLastElement = undefined;

+ 12 - 10
src/component.js

@@ -1,4 +1,4 @@
-import { walk, saferEval, saferEvalNoReturn, getXAttrs, debounce, convertClassStringToArray } from './utils'
+import { walk, saferEval, saferEvalNoReturn, getXAttrs, debounce, convertClassStringToArray, TRANSITION_CANCELLED } from './utils'
 import { handleForDirective } from './directives/for'
 import { handleAttributeBindingDirective } from './directives/bind'
 import { handleTextDirective } from './directives/text'
@@ -256,17 +256,19 @@ export default class Component {
         // The goal here is to start all the x-show transitions
         // and build a nested promise chain so that elements
         // only hide when the children are finished hiding.
-        this.showDirectiveStack.reverse().map(thing => {
-            return new Promise(resolve => {
-                thing(finish => {
-                    resolve(finish)
-                })
+        this.showDirectiveStack.reverse().map(handler => {
+            return new Promise((resolve, reject) => {
+                handler(resolve, reject)
             })
-        }).reduce((nestedPromise, promise) => {
-            return nestedPromise.then(() => {
-                return promise.then(finish => finish())
+        }).reduce((promiseChain, promise) => {
+            return promiseChain.then(() => {
+                return promise.then(finishElement => {
+                    finishElement()
+                })
             })
-        }, Promise.resolve(() => {}))
+        }, Promise.resolve(() => {})).catch(e => {
+            if (e !== TRANSITION_CANCELLED) throw e
+        })
 
         // We've processed the handler stack. let's clear it.
         this.showDirectiveStack = []

+ 3 - 3
src/directives/for.js

@@ -21,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, () => {}, component, initialUpdate)
+            transitionIn(nextEl, () => {}, () => {}, component, initialUpdate)
 
             nextEl.__x_for = iterationScopeVariables
             component.initializeElements(nextEl, () => nextEl.__x_for)
@@ -91,7 +91,7 @@ function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, e
     if (ifAttribute && ! component.evaluateReturnExpression(el, ifAttribute.expression)) {
         return []
     }
-    
+
     let items = component.evaluateReturnExpression(el, iteratorNames.items, extraVars)
 
     // This adds support for the `i in n` syntax.
@@ -137,7 +137,7 @@ function removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component) {
         let nextSibling = nextElementFromOldLoop.nextElementSibling
         transitionOut(nextElementFromOldLoop, () => {
             nextElementFromOldLoopImmutable.remove()
-        }, component)
+        }, () => {}, 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, () => {}, component, 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()
-        }, component, initialUpdate)
+        }, () => {}, component, initialUpdate)
     }
 }

+ 8 - 5
src/directives/show.js

@@ -3,6 +3,7 @@ import { transitionIn, transitionOut } from '../utils'
 export function handleShowDirective(component, el, value, modifiers, initialUpdate = false) {
     const hide = () => {
         el.style.display = 'none'
+        el.__x_is_shown = false
     }
 
     const show = () => {
@@ -11,6 +12,8 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
         } else {
             el.style.removeProperty('display')
         }
+
+        el.__x_is_shown = true
     }
 
     if (initialUpdate === true) {
@@ -22,12 +25,12 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
         return
     }
 
-    const handle = (resolve) => {
+    const handle = (resolve, reject) => {
         if (value) {
-            if(el.style.display === 'none' || el.__x_transition) {
+            if (el.style.display === 'none' || el.__x_transition) {
                 transitionIn(el, () => {
                     show()
-                }, component)
+                }, reject, component)
             }
             resolve(() => {})
         } else {
@@ -36,7 +39,7 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
                     resolve(() => {
                         hide()
                     })
-                }, component)
+                }, reject, component)
             } else {
                 resolve(() => {})
             }
@@ -49,7 +52,7 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
 
     // If x-show.immediate, foregoe the waiting.
     if (modifiers.includes('immediate')) {
-        handle(finish => finish())
+        handle(finish => finish(), () => {})
         return
     }
 

+ 43 - 37
src/utils.js

@@ -185,10 +185,11 @@ export function convertClassStringToArray(classList, filterFn = Boolean) {
     return classList.split(' ').filter(filterFn)
 }
 
-const TRANSITION_TYPE_IN = 'in'
-const TRANSITION_TYPE_OUT = 'out'
+export const TRANSITION_TYPE_IN = 'in'
+export const TRANSITION_TYPE_OUT = 'out'
+export const TRANSITION_CANCELLED = 'cancelled'
 
-export function transitionIn(el, show, component, forceSkip = false) {
+export function transitionIn(el, show, reject, component, forceSkip = false) {
     // We don't want to transition on the initial page load.
     if (forceSkip) return show()
 
@@ -214,17 +215,17 @@ export function transitionIn(el, show, component, forceSkip = false) {
         modifiers = settingBothSidesOfTransition
             ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers
 
-        transitionHelperIn(el, modifiers, show)
+        transitionHelperIn(el, modifiers, show, reject)
     // Otherwise, we can assume x-transition:enter.
     } else if (attrs.some(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value))) {
-        transitionClassesIn(el, component, attrs, show)
+        transitionClassesIn(el, component, attrs, show, reject)
     } else {
     // If neither, just show that damn thing.
         show()
     }
 }
 
-export function transitionOut(el, hide, component, forceSkip = false) {
+export function transitionOut(el, hide, reject, component, forceSkip = false) {
     // We don't want to transition on the initial page load.
     if (forceSkip) return hide()
 
@@ -247,15 +248,15 @@ export function transitionOut(el, hide, component, forceSkip = false) {
         modifiers = settingBothSidesOfTransition
             ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers
 
-        transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide)
+        transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide, reject)
     } else if (attrs.some(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value))) {
-        transitionClassesOut(el, component, attrs, hide)
+        transitionClassesOut(el, component, attrs, hide, reject)
     } else {
         hide()
     }
 }
 
-export function transitionHelperIn(el, modifiers, showCallback) {
+export function transitionHelperIn(el, modifiers, showCallback, reject) {
     // Default values inspired by: https://material.io/design/motion/speed.html#duration
     const styleValues = {
         duration: modifierValue(modifiers, 'duration', 150),
@@ -270,10 +271,10 @@ export function transitionHelperIn(el, modifiers, showCallback) {
         },
     }
 
-    transitionHelper(el, modifiers, showCallback, () => {}, styleValues, TRANSITION_TYPE_IN)
+    transitionHelper(el, modifiers, showCallback, () => {}, reject, styleValues, TRANSITION_TYPE_IN)
 }
 
-export function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
+export function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback, reject) {
     // Make the "out" transition .5x slower than the "in". (Visually better)
     // HOWEVER, if they explicitly set a duration for the "out" transition,
     // use that.
@@ -294,7 +295,7 @@ export function transitionHelperOut(el, modifiers, settingBothSidesOfTransition,
         },
     }
 
-    transitionHelper(el, modifiers, () => {}, hideCallback, styleValues, TRANSITION_TYPE_OUT)
+    transitionHelper(el, modifiers, () => {}, hideCallback, reject, styleValues, TRANSITION_TYPE_OUT)
 }
 
 function modifierValue(modifiers, key, fallback) {
@@ -329,11 +330,10 @@ function modifierValue(modifiers, key, fallback) {
     return rawValue
 }
 
-export function transitionHelper(el, modifiers, hook1, hook2, styleValues, type) {
+export function transitionHelper(el, modifiers, hook1, hook2, reject, 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()
+        el.__x_transition.cancel && el.__x_transition.cancel()
     }
 
     // If the user set these style values, we'll put them back when we're done with them.
@@ -380,7 +380,7 @@ export function transitionHelper(el, modifiers, hook1, hook2, styleValues, type)
         },
     }
 
-    transition(el, stages, type)
+    transition(el, stages, type, reject)
 }
 
 const ensureStringExpression = (expression, el, component) => {
@@ -389,27 +389,26 @@ const ensureStringExpression = (expression, el, component) => {
         : expression
 }
 
-export function transitionClassesIn(el, component, directives, showCallback) {
+export function transitionClassesIn(el, component, directives, showCallback, reject) {
     const enter = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter') || { expression: '' }).expression, el, component))
     const enterStart = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-start') || { expression: '' }).expression, el, component))
     const enterEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-end') || { expression: '' }).expression, el, component))
 
-    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}, TRANSITION_TYPE_IN)
+    transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}, TRANSITION_TYPE_IN, reject)
 }
 
-export function transitionClassesOut(el, component, directives, hideCallback) {
+export function transitionClassesOut(el, component, directives, hideCallback, reject) {
     const leave = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'leave') || { expression: '' }).expression, el, component))
     const leaveStart = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'leave-start') || { expression: '' }).expression, el, component))
     const leaveEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'leave-end') || { expression: '' }).expression, el, component))
 
-    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback, TRANSITION_TYPE_OUT)
+    transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback, TRANSITION_TYPE_OUT, reject)
 }
 
-export function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type) {
+export function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type, reject) {
     // 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()
+        el.__x_transition.cancel && el.__x_transition.cancel()
     }
 
     const originalClasses = el.__x_original_classes || []
@@ -438,27 +437,34 @@ export function transitionClasses(el, classesDuring, classesStart, classesEnd, h
         },
     }
 
-    transition(el, stages, type)
+    transition(el, stages, type, reject)
 }
 
-export function transition(el, stages, type) {
+export function transition(el, stages, type, reject) {
+    const finish = 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
+    })
+
     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()
-            }
+        cancel: once(() => {
+            reject(TRANSITION_CANCELLED)
 
-            delete el.__x_transition
+            finish()
         }),
+        finish,
         // This store the next animation frame so we can cancel it
         nextFrame: null
     }
@@ -466,7 +472,7 @@ export function transition(el, stages, type) {
     stages.start()
     stages.during()
 
-    el.__x_transition.nextFrame =requestAnimationFrame(() => {
+    el.__x_transition.nextFrame = requestAnimationFrame(() => {
         // 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.
         let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000
@@ -477,10 +483,10 @@ export function transition(el, stages, type) {
 
         stages.show()
 
-        el.__x_transition.nextFrame =requestAnimationFrame(() => {
+        el.__x_transition.nextFrame = requestAnimationFrame(() => {
             stages.end()
 
-            setTimeout(el.__x_transition.callback, duration)
+            setTimeout(el.__x_transition.finish, duration)
         })
     });
 }

+ 76 - 0
test/transition.spec.js

@@ -732,3 +732,79 @@ test('x-transition using classes do not overlap', async () => {
     expect(document.querySelector('span').style.display).toEqual("none")
     expect(document.querySelector('span').classList).toEqual(emptyClassList)
 })
+
+test('x-transition with parent x-show does 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>
+
+            <h1 x-show="show">
+                <span x-show.transition="show"></span>
+            </h1>
+        </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("")
+    expect(document.querySelector('h1').style.display).toEqual("")
+    expect(document.querySelector('h1').style.opacity).toEqual("")
+    expect(document.querySelector('h1').style.transform).toEqual("")
+    expect(document.querySelector('h1').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("")
+    expect(document.querySelector('h1').style.display).toEqual("")
+    expect(document.querySelector('h1').style.opacity).toEqual("")
+    expect(document.querySelector('h1').style.transform).toEqual("")
+    expect(document.querySelector('h1').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("")
+    expect(document.querySelector('h1').style.display).toEqual("none")
+    expect(document.querySelector('h1').style.opacity).toEqual("")
+    expect(document.querySelector('h1').style.transform).toEqual("")
+    expect(document.querySelector('h1').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("")
+    expect(document.querySelector('h1').style.display).toEqual("none")
+    expect(document.querySelector('h1').style.opacity).toEqual("")
+    expect(document.querySelector('h1').style.transform).toEqual("")
+    expect(document.querySelector('h1').style.transformOrigin).toEqual("")
+})