Skip to content

Commit

Permalink
feat: directive lifecycle hooks in v-for, v-if and component (#123)
Browse files Browse the repository at this point in the history
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
  • Loading branch information
LittleSound and sxzz authored May 26, 2024
1 parent 969f53f commit b5ecb72
Show file tree
Hide file tree
Showing 12 changed files with 679 additions and 124 deletions.
14 changes: 7 additions & 7 deletions packages/reactivity/src/effectScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ export class EffectScope {
*/
private index: number | undefined

constructor(public detached = false) {
this.parent = activeEffectScope
if (!detached && activeEffectScope) {
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this,
) - 1
constructor(
public detached = false,
parent = activeEffectScope,
) {
this.parent = parent
if (!detached && parent) {
this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-vapor/__tests__/directives/vShow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('directive: v-show', () => {
}).render()

expect(host.innerHTML).toBe('<button>toggle</button><div>child</div>')
expect(instance.dirs.get(n0)![0].dir).toBe(vShow)
expect(instance.scope.dirs!.get(n0)![0].dir).toBe(vShow)

const btn = host.querySelector('button')
btn?.click()
Expand Down
101 changes: 100 additions & 1 deletion packages/runtime-vapor/__tests__/for.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { createFor, nextTick, ref, renderEffect } from '../src'
import { NOOP } from '@vue/shared'
import {
type Directive,
children,
createFor,
nextTick,
ref,
renderEffect,
template,
withDirectives,
} from '../src'
import { makeRender } from './_utils'
import { unmountComponent } from '../src/apiRender'

const define = makeRender()

Expand Down Expand Up @@ -184,4 +195,92 @@ describe('createFor', () => {
await nextTick()
expect(host.innerHTML).toBe('<!--for-->')
})

test('should work with directive hooks', async () => {
const calls: string[] = []
const list = ref([0])
const update = ref(0)
const add = () => list.value.push(list.value.length)
const spySrcFn = vi.fn(() => list.value)

const vDirective: Directive = {
created: (el, { value }) => calls.push(`${value} created`),
beforeMount: (el, { value }) => calls.push(`${value} beforeMount`),
mounted: (el, { value }) => calls.push(`${value} mounted`),
beforeUpdate: (el, { value }) => calls.push(`${value} beforeUpdate`),
updated: (el, { value }) => calls.push(`${value} updated`),
beforeUnmount: (el, { value }) => calls.push(`${value} beforeUnmount`),
unmounted: (el, { value }) => calls.push(`${value} unmounted`),
}

const t0 = template('<p></p>')
const { instance } = define(() => {
const n1 = createFor(spySrcFn, block => {
const n2 = t0()
const n3 = children(n2, 0)
withDirectives(n3, [[vDirective, () => block.s[0]]])
return [n2, NOOP]
})
renderEffect(() => update.value)
return [n1]
}).render()

await nextTick()
// `${item index} ${hook name}`
expect(calls).toEqual(['0 created', '0 beforeMount', '0 mounted'])
calls.length = 0
expect(spySrcFn).toHaveBeenCalledTimes(1)

add()
await nextTick()
expect(calls).toEqual([
'0 beforeUpdate',
'1 created',
'1 beforeMount',
'0 updated',
'1 mounted',
])
calls.length = 0
expect(spySrcFn).toHaveBeenCalledTimes(2)

list.value.reverse()
await nextTick()
expect(calls).toEqual([
'1 beforeUpdate',
'0 beforeUpdate',
'1 updated',
'0 updated',
])
expect(spySrcFn).toHaveBeenCalledTimes(3)
list.value.reverse()
await nextTick()
calls.length = 0
expect(spySrcFn).toHaveBeenCalledTimes(4)

update.value++
await nextTick()
expect(calls).toEqual([
'0 beforeUpdate',
'1 beforeUpdate',
'0 updated',
'1 updated',
])
calls.length = 0
expect(spySrcFn).toHaveBeenCalledTimes(4)

list.value.pop()
await nextTick()
expect(calls).toEqual([
'0 beforeUpdate',
'1 beforeUnmount',
'0 updated',
'1 unmounted',
])
calls.length = 0
expect(spySrcFn).toHaveBeenCalledTimes(5)

unmountComponent(instance)
expect(calls).toEqual(['0 beforeUnmount', '0 unmounted'])
expect(spySrcFn).toHaveBeenCalledTimes(5)
})
})
120 changes: 119 additions & 1 deletion packages/runtime-vapor/__tests__/if.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {
children,
createIf,
insert,
nextTick,
ref,
renderEffect,
setText,
template,
withDirectives,
} from '../src'
import type { Mock } from 'vitest'
import { makeRender } from './_utils'
import { unmountComponent } from '../src/apiRender'

const define = makeRender()

Expand All @@ -24,6 +27,8 @@ describe('createIf', () => {
let spyElseFn: Mock<any, any>
const count = ref(0)

const spyConditionFn = vi.fn(() => count.value)

// templates can be reused through caching.
const t0 = template('<div></div>')
const t1 = template('<p></p>')
Expand All @@ -34,7 +39,7 @@ describe('createIf', () => {

insert(
createIf(
() => count.value,
spyConditionFn,
// v-if
(spyIfFn ||= vi.fn(() => {
const n2 = t1()
Expand All @@ -55,24 +60,28 @@ describe('createIf', () => {
}).render()

expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
expect(spyConditionFn).toHaveBeenCalledTimes(1)
expect(spyIfFn!).toHaveBeenCalledTimes(0)
expect(spyElseFn!).toHaveBeenCalledTimes(1)

count.value++
await nextTick()
expect(host.innerHTML).toBe('<div><p>1</p><!--if--></div>')
expect(spyConditionFn).toHaveBeenCalledTimes(2)
expect(spyIfFn!).toHaveBeenCalledTimes(1)
expect(spyElseFn!).toHaveBeenCalledTimes(1)

count.value++
await nextTick()
expect(host.innerHTML).toBe('<div><p>2</p><!--if--></div>')
expect(spyConditionFn).toHaveBeenCalledTimes(3)
expect(spyIfFn!).toHaveBeenCalledTimes(1)
expect(spyElseFn!).toHaveBeenCalledTimes(1)

count.value = 0
await nextTick()
expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
expect(spyConditionFn).toHaveBeenCalledTimes(4)
expect(spyIfFn!).toHaveBeenCalledTimes(1)
expect(spyElseFn!).toHaveBeenCalledTimes(2)
})
Expand Down Expand Up @@ -124,4 +133,113 @@ describe('createIf', () => {
await nextTick()
expect(host.innerHTML).toBe('<!--if-->')
})

test('should work with directive hooks', async () => {
const calls: string[] = []
const show1 = ref(true)
const show2 = ref(true)
const update = ref(0)

const spyConditionFn1 = vi.fn(() => show1.value)
const spyConditionFn2 = vi.fn(() => show2.value)

const vDirective: any = {
created: (el: any, { value }: any) => calls.push(`${value} created`),
beforeMount: (el: any, { value }: any) =>
calls.push(`${value} beforeMount`),
mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
beforeUpdate: (el: any, { value }: any) =>
calls.push(`${value} beforeUpdate`),
updated: (el: any, { value }: any) => calls.push(`${value} updated`),
beforeUnmount: (el: any, { value }: any) =>
calls.push(`${value} beforeUnmount`),
unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
}

const t0 = template('<p></p>')
const { instance } = define(() => {
const n1 = createIf(
spyConditionFn1,
() => {
const n2 = t0()
withDirectives(children(n2, 0), [
[vDirective, () => (update.value, '1')],
])
return n2
},
() =>
createIf(
spyConditionFn2,
() => {
const n2 = t0()
withDirectives(children(n2, 0), [[vDirective, () => '2']])
return n2
},
() => {
const n2 = t0()
withDirectives(children(n2, 0), [[vDirective, () => '3']])
return n2
},
),
)
return [n1]
}).render()

await nextTick()
expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
calls.length = 0
expect(spyConditionFn1).toHaveBeenCalledTimes(1)
expect(spyConditionFn2).toHaveBeenCalledTimes(0)

show1.value = false
await nextTick()
expect(calls).toEqual([
'1 beforeUnmount',
'2 created',
'2 beforeMount',
'1 unmounted',
'2 mounted',
])
calls.length = 0
expect(spyConditionFn1).toHaveBeenCalledTimes(2)
expect(spyConditionFn2).toHaveBeenCalledTimes(1)

show2.value = false
await nextTick()
expect(calls).toEqual([
'2 beforeUnmount',
'3 created',
'3 beforeMount',
'2 unmounted',
'3 mounted',
])
calls.length = 0
expect(spyConditionFn1).toHaveBeenCalledTimes(2)
expect(spyConditionFn2).toHaveBeenCalledTimes(2)

show1.value = true
await nextTick()
expect(calls).toEqual([
'3 beforeUnmount',
'1 created',
'1 beforeMount',
'3 unmounted',
'1 mounted',
])
calls.length = 0
expect(spyConditionFn1).toHaveBeenCalledTimes(3)
expect(spyConditionFn2).toHaveBeenCalledTimes(2)

update.value++
await nextTick()
expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
calls.length = 0
expect(spyConditionFn1).toHaveBeenCalledTimes(3)
expect(spyConditionFn2).toHaveBeenCalledTimes(2)

unmountComponent(instance)
expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
expect(spyConditionFn1).toHaveBeenCalledTimes(3)
expect(spyConditionFn2).toHaveBeenCalledTimes(2)
})
})
Loading

0 comments on commit b5ecb72

Please sign in to comment.