diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap index ba867bd75..bf50a8e82 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformRef.spec.ts.snap @@ -1,12 +1,13 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiler: template ref transform > dynamic ref 1`] = ` -"import { setRef as _setRef, template as _template } from 'vue/vapor'; +"import { renderEffect as _renderEffect, setRef as _setRef, template as _template } from 'vue/vapor'; const t0 = _template("
") export function render(_ctx) { const n0 = t0() - _setRef(n0, _ctx.foo) + let r0 + _renderEffect(() => r0 = _setRef(n0, _ctx.foo, r0)) return n0 }" `; @@ -18,7 +19,7 @@ const t0 = _template("") export function render(_ctx) { const n0 = _createFor(() => ([1,2,3]), (_block) => { const n2 = t0() - _setRef(n2, "foo", true) + _setRef(n2, "foo", void 0, true) return [n2, () => {}] }) return n0 diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap index ba867bd75..bf50a8e82 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap @@ -1,12 +1,13 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiler: template ref transform > dynamic ref 1`] = ` -"import { setRef as _setRef, template as _template } from 'vue/vapor'; +"import { renderEffect as _renderEffect, setRef as _setRef, template as _template } from 'vue/vapor'; const t0 = _template("") export function render(_ctx) { const n0 = t0() - _setRef(n0, _ctx.foo) + let r0 + _renderEffect(() => r0 = _setRef(n0, _ctx.foo, r0)) return n0 }" `; @@ -18,7 +19,7 @@ const t0 = _template("") export function render(_ctx) { const n0 = _createFor(() => ([1,2,3]), (_block) => { const n2 = t0() - _setRef(n2, "foo", true) + _setRef(n2, "foo", void 0, true) return [n2, () => {}] }) return n0 diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts index eeee1adfa..22f68f953 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts @@ -43,7 +43,6 @@ describe('compiler: template ref transform', () => { }, }, }) - expect(code).matchSnapshot() expect(code).contains('_setRef(n0, "foo")') }) @@ -56,21 +55,28 @@ describe('compiler: template ref transform', () => { flags: DynamicFlag.REFERENCED, }) expect(ir.template).toEqual(['']) - expect(ir.block.operation).lengthOf(1) - expect(ir.block.operation[0]).toMatchObject({ - type: IRNodeTypes.SET_TEMPLATE_REF, - element: 0, - value: { - content: 'foo', - isStatic: false, - loc: { - start: { line: 1, column: 12, offset: 11 }, - end: { line: 1, column: 15, offset: 14 }, - }, + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.DECLARE_OLD_REF, + id: 0, }, - }) + ]) + expect(ir.block.effect).toMatchObject([ + { + operations: [ + { + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 0, + value: { + content: 'foo', + isStatic: false, + }, + }, + ], + }, + ]) expect(code).matchSnapshot() - expect(code).contains('_setRef(n0, _ctx.foo)') + expect(code).contains('_setRef(n0, _ctx.foo, r0)') }) test('ref + v-if', () => { @@ -82,21 +88,17 @@ describe('compiler: template ref transform', () => { expect(ir.block.operation[0].type).toBe(IRNodeTypes.IF) const { positive } = ir.block.operation[0] as IfIRNode - - expect(positive.operation).lengthOf(1) - expect(positive.operation[0]).toMatchObject({ - type: IRNodeTypes.SET_TEMPLATE_REF, - element: 2, - value: { - content: 'foo', - isStatic: true, - loc: { - start: { line: 1, column: 10, offset: 9 }, - end: { line: 1, column: 15, offset: 14 }, + expect(positive.operation).toMatchObject([ + { + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 2, + value: { + content: 'foo', + isStatic: true, }, + effect: false, }, - }) - + ]) expect(code).matchSnapshot() expect(code).contains('_setRef(n2, "foo")') }) @@ -107,21 +109,19 @@ describe('compiler: template ref transform', () => { ) const { render } = ir.block.operation[0] as ForIRNode - expect(render.operation).lengthOf(1) - expect(render.operation[0]).toMatchObject({ - type: IRNodeTypes.SET_TEMPLATE_REF, - element: 2, - value: { - content: 'foo', - isStatic: true, - loc: { - start: { line: 1, column: 10, offset: 9 }, - end: { line: 1, column: 15, offset: 14 }, + expect(render.operation).toMatchObject([ + { + type: IRNodeTypes.SET_TEMPLATE_REF, + element: 2, + value: { + content: 'foo', + isStatic: true, }, + refFor: true, + effect: false, }, - refFor: true, - }) + ]) expect(code).matchSnapshot() - expect(code).contains('_setRef(n2, "foo", true)') + expect(code).contains('_setRef(n2, "foo", void 0, true)') }) }) diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts index 224c1fadf..0d1627e78 100644 --- a/packages/compiler-vapor/src/generators/operation.ts +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -7,7 +7,7 @@ import { genSetHtml } from './html' import { genIf } from './if' import { genSetModelValue } from './modelValue' import { genDynamicProps, genSetProp } from './prop' -import { genSetTemplateRef } from './templateRef' +import { genDeclareOldRef, genSetTemplateRef } from './templateRef' import { genCreateTextNode, genSetText } from './text' import { type CodeFragment, @@ -59,6 +59,8 @@ export function genOperation( return genFor(oper, context) case IRNodeTypes.CREATE_COMPONENT_NODE: return genCreateComponent(oper, context) + case IRNodeTypes.DECLARE_OLD_REF: + return genDeclareOldRef(oper) } return [] diff --git a/packages/compiler-vapor/src/generators/templateRef.ts b/packages/compiler-vapor/src/generators/templateRef.ts index 8c1022d6e..a7fa524bb 100644 --- a/packages/compiler-vapor/src/generators/templateRef.ts +++ b/packages/compiler-vapor/src/generators/templateRef.ts @@ -1,6 +1,6 @@ import { genExpression } from './expression' import type { CodegenContext } from '../generate' -import type { SetTemplateRefIRNode } from '../ir' +import type { DeclareOldRefIRNode, SetTemplateRefIRNode } from '../ir' import { type CodeFragment, NEWLINE, genCall } from './utils' export function genSetTemplateRef( @@ -10,11 +10,17 @@ export function genSetTemplateRef( const { vaporHelper } = context return [ NEWLINE, + oper.effect && `r${oper.element} = `, ...genCall( vaporHelper('setRef'), `n${oper.element}`, genExpression(oper.value, context), + oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined, oper.refFor && 'true', ), ] } + +export function genDeclareOldRef(oper: DeclareOldRefIRNode): CodeFragment[] { + return [NEWLINE, `let r${oper.id}`] +} diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index e5ba223d7..4cc427cd9 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -32,6 +32,7 @@ export enum IRNodeTypes { CREATE_COMPONENT_NODE, WITH_DIRECTIVE, + DECLARE_OLD_REF, // consider make it more general IF, FOR, @@ -145,6 +146,7 @@ export interface SetTemplateRefIRNode extends BaseIRNode { element: number value: SimpleExpressionNode refFor: boolean + effect: boolean } export interface SetModelValueIRNode extends BaseIRNode { @@ -194,6 +196,11 @@ export interface CreateComponentIRNode extends BaseIRNode { root: boolean } +export interface DeclareOldRefIRNode extends BaseIRNode { + type: IRNodeTypes.DECLARE_OLD_REF + id: number +} + export type IRNode = OperationNode | RootIRNode export type OperationNode = | SetPropIRNode @@ -211,6 +218,7 @@ export type OperationNode = | IfIRNode | ForIRNode | CreateComponentIRNode + | DeclareOldRefIRNode export enum DynamicFlag { NONE = 0, diff --git a/packages/compiler-vapor/src/transforms/transformTemplateRef.ts b/packages/compiler-vapor/src/transforms/transformTemplateRef.ts index fd0cc0fc8..8dbd9ae73 100644 --- a/packages/compiler-vapor/src/transforms/transformTemplateRef.ts +++ b/packages/compiler-vapor/src/transforms/transformTemplateRef.ts @@ -6,7 +6,7 @@ import { import type { NodeTransform } from '../transform' import { IRNodeTypes } from '../ir' import { normalizeBindShorthand } from './vBind' -import { findProp } from '../utils' +import { findProp, isConstantExpression } from '../utils' import { EMPTY_EXPRESSION } from './utils' export const transformTemplateRef: NodeTransform = (node, context) => { @@ -24,11 +24,20 @@ export const transformTemplateRef: NodeTransform = (node, context) => { : EMPTY_EXPRESSION } - return () => - context.registerOperation({ + return () => { + const id = context.reference() + const effect = !isConstantExpression(value) + effect && + context.registerOperation({ + type: IRNodeTypes.DECLARE_OLD_REF, + id, + }) + context.registerEffect([value], { type: IRNodeTypes.SET_TEMPLATE_REF, - element: context.reference(), + element: id, value, refFor: !!context.inVFor, + effect, }) + } } diff --git a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts index 6dc10eff3..41997b31a 100644 --- a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts @@ -1,4 +1,18 @@ -import { ref, setRef, template } from '../../src' +import type { NodeRef } from 'packages/runtime-vapor/src/dom/templateRef' +import { + createFor, + createIf, + getCurrentInstance, + insert, + nextTick, + reactive, + ref, + renderEffect, + setRef, + setText, + template, + watchEffect, +} from '../../src' import { makeRender } from '../_utils' const define = makeRender() @@ -23,4 +37,593 @@ describe('api: template ref', () => { const { host } = render() expect(el.value).toBe(host.children[0]) }) + + it('string ref update', async () => { + const t0 = template('') + const fooEl = ref(null) + const barEl = ref(null) + const refKey = ref('foo') + + const { render } = define({ + setup() { + return { + foo: fooEl, + bar: barEl, + } + }, + render() { + const n0 = t0() + let r0: NodeRef | undefined + renderEffect(() => { + r0 = setRef(n0 as Element, refKey.value, r0) + }) + return n0 + }, + }) + const { host } = render() + expect(fooEl.value).toBe(host.children[0]) + expect(barEl.value).toBe(null) + + refKey.value = 'bar' + await nextTick() + expect(barEl.value).toBe(host.children[0]) + expect(fooEl.value).toBe(null) + }) + + it('string ref unmount', async () => { + const t0 = template('') + const el = ref(null) + const toggle = ref(true) + + const { render } = define({ + setup() { + return { + refKey: el, + } + }, + render() { + const n0 = createIf( + () => toggle.value, + () => { + const n1 = t0() + setRef(n1 as Element, 'refKey') + return n1 + }, + ) + return n0 + }, + }) + const { host } = render() + expect(el.value).toBe(host.children[0]) + + toggle.value = false + await nextTick() + expect(el.value).toBe(null) + }) + + it('function ref mount', () => { + const fn = vi.fn() + const t0 = template('') + const { render } = define({ + render() { + const n0 = t0() + setRef(n0 as Element, fn) + return n0 + }, + }) + + const { host } = render() + expect(fn.mock.calls[0][0]).toBe(host.children[0]) + }) + + it('function ref update', async () => { + const fn1 = vi.fn() + const fn2 = vi.fn() + const fn = ref(fn1) + + const t0 = template('') + const { render } = define({ + render() { + const n0 = t0() + let r0: NodeRef | undefined + renderEffect(() => { + r0 = setRef(n0 as Element, fn.value, r0) + }) + return n0 + }, + }) + + const { host } = render() + + expect(fn1.mock.calls).toHaveLength(1) + expect(fn1.mock.calls[0][0]).toBe(host.children[0]) + expect(fn2.mock.calls).toHaveLength(0) + + fn.value = fn2 + await nextTick() + expect(fn1.mock.calls).toHaveLength(1) + expect(fn2.mock.calls).toHaveLength(1) + expect(fn2.mock.calls[0][0]).toBe(host.children[0]) + }) + + it('function ref unmount', async () => { + const fn = vi.fn() + const toggle = ref(true) + + const t0 = template('') + const { render } = define({ + render() { + const n0 = createIf( + () => toggle.value, + () => { + const n1 = t0() + setRef(n1 as Element, fn) + return n1 + }, + ) + return n0 + }, + }) + const { host } = render() + expect(fn.mock.calls[0][0]).toBe(host.children[0]) + toggle.value = false + await nextTick() + expect(fn.mock.calls[1][0]).toBe(undefined) + }) + + it('should work with direct reactive property', () => { + const state = reactive({ + refKey: null, + }) + + const t0 = template('') + const { render } = define({ + setup() { + return state + }, + render() { + const n0 = t0() + setRef(n0 as Element, 'refKey') + return n0 + }, + }) + const { host } = render() + expect(state.refKey).toBe(host.children[0]) + }) + + test('multiple root refs', () => { + const refKey1 = ref(null) + const refKey2 = ref(null) + const refKey3 = ref(null) + + const t0 = template('') + const t1 = template('') + const t2 = template('') + const { render } = define({ + setup() { + return { + refKey1, + refKey2, + refKey3, + } + }, + render() { + const n0 = t0() + const n1 = t1() + const n2 = t2() + setRef(n0 as Element, 'refKey1') + setRef(n1 as Element, 'refKey2') + setRef(n2 as Element, 'refKey3') + return [n0, n1, n2] + }, + }) + const { host } = render() + // Note: toBe Condition is different from core test case + // Core test case is expecting refKey1.value to be host.children[1] + expect(refKey1.value).toBe(host.children[0]) + expect(refKey2.value).toBe(host.children[1]) + expect(refKey3.value).toBe(host.children[2]) + }) + + // #1505 + test('reactive template ref in the same template', async () => { + const t0 = template('') + const el = ref