Browse Source

test transition-group

dev
Evan You 8 years ago
parent
commit
6f4601ceeb
  1. 21
      src/core/vdom/patch.js
  2. 52
      src/platforms/web/runtime/components/transition-group.js
  3. 5
      src/platforms/web/runtime/modules/transition.js
  4. 4
      test/unit/features/transition/inject-styles.js
  5. 297
      test/unit/features/transition/transition-group.spec.js
  6. 2
      test/unit/features/transition/transition.spec.js

21
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 <transition-group>
// 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

52
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
}

5
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

4
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,

297
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: `
<div>
${useIs ? `<span is="transition-group">` : `<transition-group${appear ? ` appear` : ``}>`}
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
${useIs ? `</span>` : `</transition-group>`}
</div>
`,
data: {
items: ['a', 'b', 'c']
}
}).$mount(el)
if (!appear) {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test">${i}</div>`).join('') +
`</span>`
)
}
return vm
}
it('enter', done => {
const vm = createBasicVM()
vm.items.push('d', 'e')
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
['a', 'b', 'c'].map(i => `<div class="test">${i}</div>`).join('') +
`<div class="test v-enter v-enter-active">d</div>` +
`<div class="test v-enter v-enter-active">e</div>` +
`</span>`
)
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
['a', 'b', 'c'].map(i => `<div class="test">${i}</div>`).join('') +
`<div class="test v-enter-active">d</div>` +
`<div class="test v-enter-active">e</div>` +
`</span>`
)
}).thenWaitFor(duration + 10).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test">${i}</div>`).join('') +
`</span>`
)
}).then(done)
})
it('leave', done => {
const vm = createBasicVM()
vm.items = ['b']
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test v-leave v-leave-active">a</div>` +
`<div class="test">b</div>` +
`<div class="test v-leave v-leave-active">c</div>` +
`</span>`
)
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test v-leave-active">a</div>` +
`<div class="test">b</div>` +
`<div class="test v-leave-active">c</div>` +
`</span>`
)
}).thenWaitFor(duration + 10).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test">${i}</div>`).join('') +
`</span>`
)
}).then(done)
})
it('enter + leave', done => {
const vm = createBasicVM()
vm.items = ['b', 'c', 'd']
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test v-leave v-leave-active">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test v-enter v-enter-active">d</div>` +
`</span>`
)
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test v-leave-active">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test v-enter-active">d</div>` +
`</span>`
)
}).thenWaitFor(duration + 10).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test">${i}</div>`).join('') +
`</span>`
)
}).then(done)
})
it('use with "is" attribute', done => {
const vm = createBasicVM(true)
vm.items = ['b', 'c', 'd']
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test v-leave v-leave-active">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test v-enter v-enter-active">d</div>` +
`</span>`
)
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test v-leave-active">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test v-enter-active">d</div>` +
`</span>`
)
}).thenWaitFor(duration + 10).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test">${i}</div>`).join('') +
`</span>`
)
}).then(done)
})
it('appear', done => {
const vm = createBasicVM(false, true /* appear */)
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test v-enter v-enter-active">${i}</div>`).join('') +
`</span>`
)
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test v-enter-active">${i}</div>`).join('') +
`</span>`
)
}).thenWaitFor(duration + 10).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
vm.items.map(i => `<div class="test">${i}</div>`).join('') +
`</span>`
)
}).then(done)
})
it('events', done => {
let next
const beforeEnterSpy = jasmine.createSpy()
const afterEnterSpy = jasmine.createSpy()
const afterLeaveSpy = jasmine.createSpy()
const vm = new Vue({
template: `
<div>
<transition-group @before-enter="beforeEnter" @after-enter="afterEnter" @after-leave="afterLeave">
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
</transition-group>
</div>
`,
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(
`<span>` +
`<div class="test">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test v-enter v-enter-active">d</div>` +
`</span>`
)
expect(beforeEnterSpy.calls.count()).toBe(1)
}).thenWaitFor(_next => { next = _next }).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test">a</div>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test">d</div>` +
`</span>`
)
expect(afterEnterSpy.calls.count()).toBe(1)
vm.items.shift()
}).thenWaitFor(_next => { next = _next }).then(() => {
expect(vm.$el.innerHTML).toBe(
`<span>` +
`<div class="test">b</div>` +
`<div class="test">c</div>` +
`<div class="test">d</div>` +
`</span>`
)
expect(afterLeaveSpy.calls.count()).toBe(1)
}).then(done)
})
it('move', done => {
const vm = new Vue({
template: `
<div>
<transition-group name="group">
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
</transition-group>
</div>
`,
data: {
items: ['a', 'b', 'c']
}
}).$mount(el)
vm.items = ['d', 'b', 'a']
waitForUpdate(() => {
expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe(
`<span>` +
`<div class="test group-enter group-enter-active">d</div>` +
`<div class="test">b</div>` +
`<div class="test group-move">a</div>` +
`<div class="test group-leave group-leave-active group-move">c</div>` +
`</span>`
)
}).thenWaitFor(nextFrame).then(() => {
expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe(
`<span>` +
`<div class="test group-enter-active">d</div>` +
`<div class="test">b</div>` +
`<div class="test group-move">a</div>` +
`<div class="test group-leave-active group-move">c</div>` +
`</span>`
)
}).thenWaitFor(duration + 10).then(() => {
expect(vm.$el.innerHTML.replace(/\s?style=""\s?/g, '')).toBe(
`<span>` +
`<div class="test">d</div>` +
`<div class="test">b</div>` +
`<div class="test">a</div>` +
`</span>`
)
}).then(done)
})
it('warn unkeyed children', () => {
new Vue({
template: `<div><transition-group><div v-for="i in 3"></div></transition-group></div>`
}).$mount()
expect('<transition-group> children must be keyed: <div>').toHaveBeenWarned()
})
})
}

2
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

Loading…
Cancel
Save