From 6f4601ceebdc59da90767d47fef46e147931f8ea Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 14 Jul 2016 17:51:25 -0400 Subject: [PATCH] test transition-group --- src/core/vdom/patch.js | 21 +- .../runtime/components/transition-group.js | 52 +-- .../web/runtime/modules/transition.js | 5 +- .../unit/features/transition/inject-styles.js | 4 + .../transition/transition-group.spec.js | 297 ++++++++++++++++++ .../features/transition/transition.spec.js | 2 +- 6 files changed, 346 insertions(+), 35 deletions(-) create mode 100644 test/unit/features/transition/transition-group.spec.js diff --git a/src/core/vdom/patch.js b/src/core/vdom/patch.js index 3fa1a6c8..33f4f745 100644 --- a/src/core/vdom/patch.js +++ b/src/core/vdom/patch.js @@ -218,7 +218,7 @@ export function createPatchFunction (backend) { } } - function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue) { + function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 @@ -229,6 +229,11 @@ export function createPatchFunction (backend) { let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, before + // removeOnly is a special flag used only by + // to ensure removed elements stay in correct relative positions + // during leaving transitions + const canMove = !removeOnly + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left @@ -244,12 +249,12 @@ export function createPatchFunction (backend) { newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) - nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) + canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) - nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) + canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { @@ -274,7 +279,7 @@ export function createPatchFunction (backend) { } else { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined - nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) + canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } @@ -288,7 +293,7 @@ export function createPatchFunction (backend) { } } - function patchVnode (oldVnode, vnode, insertedVnodeQueue) { + function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) return let i, hook const hasData = isDef(i = vnode.data) @@ -304,7 +309,7 @@ export function createPatchFunction (backend) { } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) + if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) @@ -384,7 +389,7 @@ export function createPatchFunction (backend) { } } - return function patch (oldVnode, vnode, hydrating) { + return function patch (oldVnode, vnode, hydrating, removeOnly) { let elm, parent let isInitialPatch = false const insertedVnodeQueue = [] @@ -396,7 +401,7 @@ export function createPatchFunction (backend) { } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue) + patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // mounting to a real element diff --git a/src/platforms/web/runtime/components/transition-group.js b/src/platforms/web/runtime/components/transition-group.js index 70b5528a..f2a4aca8 100644 --- a/src/platforms/web/runtime/components/transition-group.js +++ b/src/platforms/web/runtime/components/transition-group.js @@ -30,11 +30,11 @@ export default { props, render (h) { - const prevMap = this.map - const map = this.map = {} + const tag = this.tag || this.$vnode.data.tag || 'span' + const map = Object.create(null) + const prevChildren = this.prevChildren = this.children const rawChildren = this.$slots.default || [] - const children = [] - const kept = [] + const children = this.children = [] const transitionData = extractTransitionData(this) for (let i = 0; i < rawChildren.length; i++) { @@ -44,12 +44,6 @@ export default { children.push(c) map[c.key] = c ;(c.data || (c.data = {})).transition = transitionData - const prev = prevMap && prevMap[c.key] - if (prev) { - prev.data.kept = true - c.data.pos = prev.elm.getBoundingClientRect() - kept.push(c) - } } else if (process.env.NODE_ENV !== 'production') { const opts = c.componentOptions const name = opts @@ -60,17 +54,20 @@ export default { } } - const tag = this.tag || this.$vnode.data.tag || 'span' - if (prevMap) { - this.kept = h(tag, null, kept) - this.removed = [] - for (const key in prevMap) { - const c = prevMap[key] - if (!c.data.kept) { - c.data.pos = c.elm.getBoundingClientRect() - this.removed.push(c) + if (prevChildren) { + const kept = [] + const removed = [] + for (let i = 0; i < prevChildren.length; i++) { + const c = prevChildren[i] + c.data.pos = c.elm.getBoundingClientRect() + if (map[c.key]) { + kept.push(c) + } else { + removed.push(c) } } + this.kept = h(tag, null, kept) + this.removed = removed } return h(tag, null, children) @@ -78,12 +75,17 @@ export default { beforeUpdate () { // force removing pass - this.__patch__(this._vnode, this.kept) + this.__patch__( + this._vnode, + this.kept, + false, // hydrating + true // removeOnly (!important, avoids unnecessary moves) + ) this._vnode = this.kept }, updated () { - const children = this.kept.children.concat(this.removed) + const children = this.prevChildren const moveClass = this.moveClass || (this.name + '-move') if (!children.length || !this.hasMove(children[0].elm, moveClass)) { return @@ -108,12 +110,13 @@ export default { children.forEach(c => { if (c.data.moved) { const el = c.elm + /* istanbul ignore if */ + if (el._pendingMoveCb) { + el._pendingMoveCb() + } const s = el.style addTransitionClass(el, moveClass) s.transform = s.WebkitTransform = s.transitionDuration = '' - if (el._pendingMoveCb) { - el.removeEventListener(transitionEndEvent, el._pendingMoveCb) - } el.addEventListener(transitionEndEvent, el._pendingMoveCb = function cb () { el.removeEventListener(transitionEndEvent, cb) el._pendingMoveCb = null @@ -125,6 +128,7 @@ export default { methods: { hasMove (el, moveClass) { + /* istanbul ignore if */ if (!hasTransition) { return false } diff --git a/src/platforms/web/runtime/modules/transition.js b/src/platforms/web/runtime/modules/transition.js index 566f2201..696484a9 100644 --- a/src/platforms/web/runtime/modules/transition.js +++ b/src/platforms/web/runtime/modules/transition.js @@ -1,6 +1,7 @@ /* @flow */ import { inBrowser } from 'core/util/index' +import { isIE9 } from 'web/util/index' import { cached, extend } from 'shared/util' import { mergeVNodeHook } from 'core/vdom/helpers' import { @@ -52,7 +53,7 @@ export function enter (vnode: VNodeWithData) { const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled - const expectsCSS = css !== false + const expectsCSS = css !== false && !isIE9 const userWantsControl = enterHook && // enterHook may be a bound method which exposes @@ -127,7 +128,7 @@ export function leave (vnode: VNodeWithData, rm: Function) { delayLeave } = data - const expectsCSS = css !== false + const expectsCSS = css !== false && !isIE9 const userWantsControl = leave && // leave hook may be a bound method which exposes diff --git a/test/unit/features/transition/inject-styles.js b/test/unit/features/transition/inject-styles.js index 96db88b1..a512f829 100644 --- a/test/unit/features/transition/inject-styles.js +++ b/test/unit/features/transition/inject-styles.js @@ -15,6 +15,10 @@ export default function injectStyles () { -webkit-transition: opacity ${duration}ms ease; transition: opacity ${duration}ms ease; } + .group-move { + -webkit-transition: -webkit-transform ${duration}ms ease; + transition: transform ${duration}ms ease; + } .v-appear, .v-enter, .v-leave-active, .test-appear, .test-enter, .test-leave-active, .hello, .bye.active, diff --git a/test/unit/features/transition/transition-group.spec.js b/test/unit/features/transition/transition-group.spec.js new file mode 100644 index 00000000..c43f1b27 --- /dev/null +++ b/test/unit/features/transition/transition-group.spec.js @@ -0,0 +1,297 @@ +import Vue from 'vue' +import injectStyles from './inject-styles' +import { isIE9 } from 'web/util/index' +import { nextFrame } from 'web/runtime/transition-util' + +if (!isIE9) { + describe('Transition group', () => { + const duration = injectStyles() + + let el + beforeEach(() => { + el = document.createElement('div') + document.body.appendChild(el) + }) + + function createBasicVM (useIs, appear) { + const vm = new Vue({ + template: ` +
+ ${useIs ? `` : ``} +
{{ item }}
+ ${useIs ? `
` : ``} +
+ `, + data: { + items: ['a', 'b', 'c'] + } + }).$mount(el) + if (!appear) { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + } + return vm + } + + it('enter', done => { + const vm = createBasicVM() + vm.items.push('d', 'e') + waitForUpdate(() => { + expect(vm.$el.innerHTML).toBe( + `` + + ['a', 'b', 'c'].map(i => `
${i}
`).join('') + + `
d
` + + `
e
` + + `
` + ) + }).thenWaitFor(nextFrame).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + ['a', 'b', 'c'].map(i => `
${i}
`).join('') + + `
d
` + + `
e
` + + `
` + ) + }).thenWaitFor(duration + 10).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + }).then(done) + }) + + it('leave', done => { + const vm = createBasicVM() + vm.items = ['b'] + waitForUpdate(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
` + ) + }).thenWaitFor(nextFrame).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
` + ) + }).thenWaitFor(duration + 10).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + }).then(done) + }) + + it('enter + leave', done => { + const vm = createBasicVM() + vm.items = ['b', 'c', 'd'] + waitForUpdate(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
d
` + + `
` + ) + }).thenWaitFor(nextFrame).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
d
` + + `
` + ) + }).thenWaitFor(duration + 10).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + }).then(done) + }) + + it('use with "is" attribute', done => { + const vm = createBasicVM(true) + vm.items = ['b', 'c', 'd'] + waitForUpdate(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
d
` + + `
` + ) + }).thenWaitFor(nextFrame).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
d
` + + `
` + ) + }).thenWaitFor(duration + 10).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + }).then(done) + }) + + it('appear', done => { + const vm = createBasicVM(false, true /* appear */) + waitForUpdate(() => { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + }).thenWaitFor(nextFrame).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + }).thenWaitFor(duration + 10).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + vm.items.map(i => `
${i}
`).join('') + + `
` + ) + }).then(done) + }) + + it('events', done => { + let next + const beforeEnterSpy = jasmine.createSpy() + const afterEnterSpy = jasmine.createSpy() + const afterLeaveSpy = jasmine.createSpy() + const vm = new Vue({ + template: ` +
+ +
{{ item }}
+
+
+ `, + data: { + items: ['a', 'b', 'c'] + }, + methods: { + beforeEnter (el) { + expect(el.textContent).toBe('d') + beforeEnterSpy() + }, + afterEnter (el) { + expect(el.textContent).toBe('d') + afterEnterSpy() + next() + }, + afterLeave (el) { + expect(el.textContent).toBe('a') + afterLeaveSpy() + next() + } + } + }).$mount(el) + + vm.items.push('d') + waitForUpdate(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
d
` + + `
` + ) + expect(beforeEnterSpy.calls.count()).toBe(1) + }).thenWaitFor(_next => { next = _next }).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
a
` + + `
b
` + + `
c
` + + `
d
` + + `
` + ) + expect(afterEnterSpy.calls.count()).toBe(1) + vm.items.shift() + }).thenWaitFor(_next => { next = _next }).then(() => { + expect(vm.$el.innerHTML).toBe( + `` + + `
b
` + + `
c
` + + `
d
` + + `
` + ) + expect(afterLeaveSpy.calls.count()).toBe(1) + }).then(done) + }) + + it('move', done => { + const vm = new Vue({ + template: ` +
+ +
{{ item }}
+
+
+ `, + data: { + items: ['a', 'b', 'c'] + } + }).$mount(el) + + vm.items = ['d', 'b', 'a'] + waitForUpdate(() => { + expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe( + `` + + `
d
` + + `
b
` + + `
a
` + + `
c
` + + `
` + ) + }).thenWaitFor(nextFrame).then(() => { + expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe( + `` + + `
d
` + + `
b
` + + `
a
` + + `
c
` + + `
` + ) + }).thenWaitFor(duration + 10).then(() => { + expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe( + `` + + `
d
` + + `
b
` + + `
a
` + + `
` + ) + }).then(done) + }) + + it('warn unkeyed children', () => { + new Vue({ + template: `
` + }).$mount() + expect(' children must be keyed:
').toHaveBeenWarned() + }) + }) +} diff --git a/test/unit/features/transition/transition.spec.js b/test/unit/features/transition/transition.spec.js index 0e1e7fdc..d08b2852 100644 --- a/test/unit/features/transition/transition.spec.js +++ b/test/unit/features/transition/transition.spec.js @@ -4,7 +4,7 @@ import { isIE9 } from 'web/util/index' import { nextFrame } from 'web/runtime/transition-util' if (!isIE9) { - describe('Transition system', () => { + describe('Transition basic', () => { const duration = injectStyles() let el