Sfoglia il codice sorgente

Make x-show wait for children to hide

Caleb Porzio 5 anni fa
parent
commit
d157b04fc1
7 ha cambiato i file con 229 aggiunte e 16 eliminazioni
  1. 0 0
      dist/alpine.js
  2. 0 0
      dist/alpine.js.map
  3. 86 0
      examples/transition.html
  4. 34 1
      src/component.js
  5. 46 14
      src/directives/show.js
  6. 61 1
      test/show.spec.js
  7. 2 0
      test/transition.spec.js

File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js


File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js.map


+ 86 - 0
examples/transition.html

@@ -0,0 +1,86 @@
+<html>
+    <head>
+        <style>
+            .hidden { display: none; }
+            [x-cloak] { display: none; }
+            .opacity-0 { opacity: 0; }
+            .opacity-100 { opacity: 1; }
+            .transition-slow { transition-duration: 3000ms; }
+            .transition-medium { transition-duration: 2000ms; }
+            .transition-fast { transition-duration: 1000ms; }
+            .scale-90 { transform: scale(0.9); }
+            .scale-100 { transform: scale(1); }
+            .ease-in { transition-timing-function: cubic-bezier(0.4, 0, 1, 1); }
+            .ease-out { transition-timing-function: cubic-bezier(0, 0, 0.2, 1); }
+        </style>
+        <script src="/dist/alpine.js" defer></script>
+    </head>
+    <body>
+        <div x-data="{ open: false }">
+                <div x-show.immediate="open">
+                    dog
+                    <div x-show="open"
+                        x-transition:enter-start="opacity-0 scale-90"
+                        x-transition:enter="ease-in transition-slow"
+                        x-transition:enter-end="opacity-100 scale-100"
+                        x-transition:leave-start="opacity-100 scale-100"
+                        x-transition:leave="ease-out transition-slow"
+                        x-transition:leave-end="opacity-0 scale-90"
+                    >
+                        first
+                        <div x-show="open"
+                        x-transition:enter-start="opacity-0 scale-90"
+                        x-transition:enter="ease-in transition-medium"
+                        x-transition:enter-end="opacity-100 scale-100"
+                        x-transition:leave-start="opacity-100 scale-100"
+                        x-transition:leave="ease-out transition-medium"
+                        x-transition:leave-end="opacity-0 scale-90"
+                    >
+                        second
+                    </div>
+                    </div>
+                </div>
+                    <!-- <div x-show="open"
+                        x-transition:leave-start="opacity-100 scale-100"
+                        x-transition:leave="ease-in transition-medium"
+                        x-transition:leave-end="opacity-0 scale-90"
+                    >
+                        second
+                        <div x-show="open"
+                            x-transition:leave-start="opacity-100 scale-100"
+                            x-transition:leave="ease-in transition-fast"
+                            x-transition:leave-end="opacity-0 scale-90"
+                        >
+                            third
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div x-show="open">
+                main
+                <div x-show="open"
+                    x-transition:leave-start="opacity-100 scale-100"
+                    x-transition:leave="ease-in transition-fast"
+                    x-transition:leave-end="opacity-0 scale-90"
+                >
+                    first
+                    <div x-show="open"
+                        x-transition:leave-start="opacity-100 scale-100"
+                        x-transition:leave="ease-in transition-medium"
+                        x-transition:leave-end="opacity-0 scale-90"
+                    >
+                        second
+                        <div x-show="open"
+                            x-transition:leave-start="opacity-100 scale-100"
+                            x-transition:leave="ease-in transition-fast"
+                            x-transition:leave-end="opacity-0 scale-90"
+                        >
+                            third
+                        </div>
+                    </div>
+                </div>
+            </div> -->
+                <button @click="open = ! open">toggle</button>
+        </div>
+    </body>
+</html>

+ 34 - 1
src/component.js

@@ -31,6 +31,9 @@ export default class Component {
             this.nextTickStack.push(callback)
         }
 
+        this.showDirectiveStack = []
+        this.showDirectiveLastElement
+
         var initReturnedCallback
         if (initExpression) {
             // We want to allow data manipulation, but not trigger DOM updates just yet.
@@ -131,6 +134,8 @@ export default class Component {
             el.__x = new Component(el)
         })
 
+        this.executeAndClearRemainingShowDirectiveStack()
+
         // Walk through the $nextTick stack and clear it as we go.
         while (this.nextTickStack.length > 0) {
             this.nextTickStack.shift()()
@@ -157,6 +162,34 @@ export default class Component {
         }, el => {
             el.__x = new Component(el)
         })
+
+        this.executeAndClearRemainingShowDirectiveStack()
+
+        // Walk through the $nextTick stack and clear it as we go.
+        while (this.nextTickStack.length > 0) {
+            this.nextTickStack.shift()()
+        }
+    }
+
+    executeAndClearRemainingShowDirectiveStack() {
+        // 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)
+                })
+            })
+        }).reduce((nestedPromise, promise) => {
+            return nestedPromise.then(() => {
+                return promise.then(finish => finish())
+            })
+        }, Promise.resolve(() => {}))
+
+        // We've processed the handler stack. let's clear it.
+        this.showDirectiveStack = []
+        this.showDirectiveLastElement = undefined
     }
 
     updateElement(el, extraVars) {
@@ -211,7 +244,7 @@ export default class Component {
                 case 'show':
                     var output = this.evaluateReturnExpression(el, expression, extraVars)
 
-                    handleShowDirective(el, output, initialUpdate)
+                    handleShowDirective(this, el, output, modifiers, initialUpdate)
                     break;
 
                 case 'if':

+ 46 - 14
src/directives/show.js

@@ -1,21 +1,53 @@
 import { transitionIn, transitionOut } from '../utils'
 
-export function handleShowDirective(el, value, initialUpdate = false) {
-    if (! value) {
-        if ( el.style.display !== 'none' ) {
-            transitionOut(el, () => {
+export function handleShowDirective(component, el, value, modifiers, initialUpdate = false) {
+    const handle = (resolve) => {
+        const hide = () => {
+            resolve(() => {
                 el.style.display = 'none'
-            }, initialUpdate)
+            })
         }
-    } else {
-        if ( el.style.display !== '' ) {
-            transitionIn(el, () => {
-                if (el.style.length === 1) {
-                    el.removeAttribute('style')
-                } else {
-                    el.style.removeProperty('display')
-                }
-            }, initialUpdate)
+
+        const show = () => {
+            resolve(() => {})
+            // Perform showing immediately.
+            if (el.style.length === 1) {
+                el.removeAttribute('style')
+            } else {
+                el.style.removeProperty('display')
+            }
+        }
+
+        if (! value) {
+            if ( el.style.display !== 'none' ) {
+                transitionOut(el, hide, initialUpdate)
+            }
+        } else {
+            if ( el.style.display !== '' ) {
+                transitionIn(el, show, initialUpdate)
+            }
         }
     }
+
+    // The working of x-show is a bit complex because we need to
+    // wait for any child transitions to finish before hiding
+    // some element. Also, this has to be done recursively.
+
+    // If x-show.immediate, foregoe the waiting.
+    if (modifiers.includes('immediate')) {
+        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
+    // can execute the previous x-show stack (if one exists).
+    if (component.showDirectiveLastElement && ! component.showDirectiveLastElement.contains(el)) {
+        component.executeAndClearRemainingShowDirectiveCallbacks()
+    }
+
+    // We'll push the handler onto a stack to be handled later.
+    component.showDirectiveStack.push(handle)
+
+    component.showDirectiveLastElement = el
 }

+ 61 - 1
test/show.spec.js

@@ -23,7 +23,7 @@ test('x-show toggles display: none; with no other style attributes', async () =>
     await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;') })
 })
 
-test('x-show toggles display: none; with no other style attributes', async () => {
+test('x-show toggles display: none; with other style attributes', async () => {
     document.body.innerHTML = `
         <div x-data="{ show: true }">
             <span x-show="show" style="color: blue;"></span>
@@ -40,3 +40,63 @@ test('x-show toggles display: none; with no other style attributes', async () =>
 
     await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('color: blue; display: none;') })
 })
+
+test('x-show waits for transitions within it to finish before hiding an elements', async () => {
+    document.body.innerHTML = `
+        <style>
+            .transition { transition-property: background-color,border-color,color,fill,stroke,opacity,box-shadow,transform; }
+            .duration-75 {
+                transition-duration: 75ms;
+            }
+        </style>
+        <div x-data="{ show: true }">
+            <span x-show="show">
+                <h1 x-show="show" x-transition:leave="transition duration-75"></h1>
+            </span>
+
+            <button x-on:click="show = false"></button>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+
+    document.querySelector('button').click()
+
+    await new Promise((resolve) => setTimeout(() => { resolve(); }, 50))
+
+    await wait(() => {
+        expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+        expect(document.querySelector('h1').getAttribute('style')).toEqual(null)
+    })
+})
+
+test('x-show does NOT wait for transitions to finish if .immediate is present', async () => {
+    document.body.innerHTML = `
+        <style>
+            .transition { transition-property: background-color,border-color,color,fill,stroke,opacity,box-shadow,transform; }
+            .duration-75 {
+                transition-duration: 75ms;
+            }
+        </style>
+        <div x-data="{ show: true }">
+            <span x-show.immediate="show">
+                <h1 x-show="show" x-transition:leave="transition duration-75"></h1>
+            </span>
+
+            <button x-on:click="show = false"></button>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+
+    document.querySelector('button').click()
+
+    await new Promise(resolve => setTimeout(resolve, 1))
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
+    expect(document.querySelector('h1').getAttribute('style')).toEqual(null)
+})

+ 2 - 0
test/transition.spec.js

@@ -281,6 +281,8 @@ test('transition out not called when item is already hidden', async () => {
 
     Alpine.start()
 
+    await new Promise(resolve => setTimeout(resolve, 1))
+
     expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
 
     document.querySelector('button').click()

Some files were not shown because too many files changed in this diff