")
export function render(_ctx) {
const n0 = _createIf(() => (_ctx.arr.length > 0), () => {
const n2 = _createFor(() => (_ctx.arr), (_for_item0, _for_key0) => {
const n4 = t0()
- const x4 = _txt(n4)
- _renderEffect(() => _setText(x4, "item: " + _toDisplayString(_for_item0.value)))
+ _setTextBinding(n4, () => "item: " + _toDisplayString(_for_item0.value))
return n4
}, (item, index) => (index), 8)
return n2
@@ -191,7 +188,7 @@ export function render(_ctx) {
`;
exports[`compiler: v-if > template v-if 1`] = `
-"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
+"import { toDisplayString as _toDisplayString, setTextBinding as _setTextBinding, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("
", 2)
const t1 = _template("hello", 2)
const t2 = _template("
")
@@ -201,8 +198,7 @@ export function render(_ctx) {
const n2 = t0()
const n3 = t1()
const n4 = t2()
- const x4 = _txt(n4)
- _renderEffect(() => _setText(x4, _toDisplayString(_ctx.msg)))
+ _setTextBinding(n4, () => _toDisplayString(_ctx.msg))
return [n2, n3, n4]
}, null, 2)
return n0
@@ -328,13 +324,13 @@ export function render(_ctx) {
`;
exports[`compiler: v-if > v-on with v-if 1`] = `
-"import { setDynamicEvents as _setDynamicEvents, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
+"import { setDynamicEventsBinding as _setDynamicEventsBinding, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("w/ v-if", 1)
export function render(_ctx) {
const n0 = _createIf(() => (true), () => {
const n2 = t0()
- _renderEffect(() => _setDynamicEvents(n2, { click: _ctx.clickEvent }))
+ _setDynamicEventsBinding(n2, () => ({ click: _ctx.clickEvent }))
return n2
}, null, 1, true)
return n0
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
index ff5662f3ab4..c12f566c2bd 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
@@ -267,13 +267,13 @@ export function render(_ctx) {
`;
exports[`compiler: vModel transform > should support w/ dynamic v-bind 1`] = `
-"import { applyDynamicModel as _applyDynamicModel, setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { applyDynamicModel as _applyDynamicModel, setDynamicPropsBinding as _setDynamicPropsBinding, template as _template } from 'vue';
const t0 = _template(" ", 1)
export function render(_ctx) {
const n0 = t0()
_applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value))
- _renderEffect(() => _setDynamicProps(n0, [_ctx.obj]))
+ _setDynamicPropsBinding(n0, () => [_ctx.obj])
return n0
}"
`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap
index d9175df28de..d8d873eca39 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOn.spec.ts.snap
@@ -36,51 +36,62 @@ export function render(_ctx) {
`;
exports[`v-on > dynamic arg 1`] = `
+"import { setEventBinding as _setEventBinding, template as _template } from 'vue';
+const t0 = _template("", 1)
+
+export function render(_ctx) {
+ const n0 = t0()
+ _setEventBinding(n0, () => _ctx.event, e => _ctx.handler(e))
+ return n0
+}"
+`;
+
+exports[`v-on > dynamic arg falls back to onBinding when expressions need declarations 1`] = `
"import { onBinding as _onBinding, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- _renderEffect(() => _onBinding(n0, _ctx.event, e => _ctx.handler(e)))
+ _renderEffect(() => {
+ const _event = _ctx.event
+ _onBinding(n0, _event+_event, e => _ctx.handler(e))
+ })
return n0
}"
`;
exports[`v-on > dynamic arg with complex exp prefixing 1`] = `
-"import { onBinding as _onBinding, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { setEventBinding as _setEventBinding, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- _renderEffect(() => _onBinding(n0, _ctx.event(_ctx.foo), e => _ctx.handler(e)))
+ _setEventBinding(n0, () => _ctx.event(_ctx.foo), e => _ctx.handler(e))
return n0
}"
`;
exports[`v-on > dynamic arg with event options 1`] = `
-"import { onBinding as _onBinding, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { setEventBinding as _setEventBinding, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- _renderEffect(() => {
-
- _onBinding(n0, _ctx.event, e => _ctx.handler(e), {
- capture: true,
- once: true
- })
+ _setEventBinding(n0, () => _ctx.event, e => _ctx.handler(e), {
+ capture: true,
+ once: true
})
return n0
}"
`;
exports[`v-on > dynamic arg with prefixing 1`] = `
-"import { onBinding as _onBinding, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { setEventBinding as _setEventBinding, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- _renderEffect(() => _onBinding(n0, _ctx.event, e => _ctx.handler(e)))
+ _setEventBinding(n0, () => _ctx.event, e => _ctx.handler(e))
return n0
}"
`;
@@ -387,12 +398,12 @@ export function render(_ctx) {
`;
exports[`v-on > should transform click.middle 2`] = `
-"import { onBinding as _onBinding, withModifiers as _withModifiers, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { setEventBinding as _setEventBinding, withModifiers as _withModifiers, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- _renderEffect(() => _onBinding(n0, (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers(e => _ctx.test(e), ["middle"])))
+ _setEventBinding(n0, () => (_ctx.event) === "click" ? "mouseup" : (_ctx.event), _withModifiers(e => _ctx.test(e), ["middle"]))
return n0
}"
`;
@@ -410,12 +421,12 @@ export function render(_ctx) {
`;
exports[`v-on > should transform click.right 2`] = `
-"import { onBinding as _onBinding, withModifiers as _withModifiers, withKeys as _withKeys, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { setEventBinding as _setEventBinding, withModifiers as _withModifiers, withKeys as _withKeys, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- _renderEffect(() => _onBinding(n0, (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers(e => _ctx.test(e), ["right"]), ["right"])))
+ _setEventBinding(n0, () => (_ctx.event) === "click" ? "contextmenu" : (_ctx.event), _withKeys(_withModifiers(e => _ctx.test(e), ["right"]), ["right"]))
return n0
}"
`;
@@ -433,12 +444,12 @@ export function render(_ctx) {
`;
exports[`v-on > should wrap both for dynamic key event w/ left/right modifiers 1`] = `
-"import { onBinding as _onBinding, withModifiers as _withModifiers, withKeys as _withKeys, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { setEventBinding as _setEventBinding, withModifiers as _withModifiers, withKeys as _withKeys, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- _renderEffect(() => _onBinding(n0, _ctx.e, _withKeys(_withModifiers(e => _ctx.test(e), ["left"]), ["left"])))
+ _setEventBinding(n0, () => _ctx.e, _withKeys(_withModifiers(e => _ctx.test(e), ["left"]), ["left"]))
return n0
}"
`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
index 57c8a129001..765831d586b 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
@@ -123,3 +123,14 @@ export function render(_ctx) {
return n0
}"
`;
+
+exports[`compiler: v-once > with v-on object 1`] = `
+"import { setDynamicEvents as _setDynamicEvents, template as _template } from 'vue';
+const t0 = _template("
", 1)
+
+export function render(_ctx) {
+ const n0 = t0()
+ _setDynamicEvents(n0, _ctx.obj)
+ return n0
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
index 29f4e9326ad..9710a348ee7 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
@@ -777,7 +777,7 @@ export function render(_ctx) {
`;
exports[`compiler: transform slot > withVaporCtx optimization > slot with v-for but no component should not have withVaporCtx 1`] = `
-"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, createAssetComponent as _createAssetComponent, template as _template } from 'vue';
+"import { toDisplayString as _toDisplayString, setTextBinding as _setTextBinding, createFor as _createFor, createAssetComponent as _createAssetComponent, template as _template } from 'vue';
const t0 = _template("
")
export function render(_ctx) {
@@ -785,12 +785,10 @@ export function render(_ctx) {
"default": () => {
const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
const n2 = t0()
- const x2 = _txt(n2)
- _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value)))
+ _setTextBinding(n2, () => _toDisplayString(_for_item0.value))
return n2
}, undefined, 8)
- const x0 = _txt(n0)
- _renderEffect(() => _setText(x0, _toDisplayString(_ctx.item)))
+ _setTextBinding(n0, () => _toDisplayString(_ctx.item))
return n0
}
}, true)
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
index 80068f205b3..f1fc058256f 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
@@ -1,25 +1,23 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`v-text > should convert v-text to setText 1`] = `
-"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { toDisplayString as _toDisplayString, setTextBinding as _setTextBinding, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
- const x0 = _txt(n0)
- _renderEffect(() => _setText(x0, _toDisplayString(_ctx.str)))
+ _setTextBinding(n0, () => _toDisplayString(_ctx.str))
return n0
}"
`;
exports[`v-text > should raise error and ignore children when v-text is present 1`] = `
-"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { toDisplayString as _toDisplayString, setTextBinding as _setTextBinding, template as _template } from 'vue';
const t0 = _template("
", 1)
export function render(_ctx) {
const n0 = t0()
- const x0 = _txt(n0)
- _renderEffect(() => _setText(x0, _toDisplayString(_ctx.test)))
+ _setTextBinding(n0, () => _toDisplayString(_ctx.test))
return n0
}"
`;
@@ -35,21 +33,21 @@ export function render(_ctx) {
`;
exports[`v-text > work with component 1`] = `
-"import { createAssetComponent as _createAssetComponent, toDisplayString as _toDisplayString, setBlockText as _setBlockText, renderEffect as _renderEffect } from 'vue';
+"import { createAssetComponent as _createAssetComponent, toDisplayString as _toDisplayString, setBlockTextBinding as _setBlockTextBinding } from 'vue';
export function render(_ctx) {
const n0 = _createAssetComponent("Comp", null, null, true)
- _renderEffect(() => _setBlockText(n0, _toDisplayString(_ctx.foo)))
+ _setBlockTextBinding(n0, () => _toDisplayString(_ctx.foo))
return n0
}"
`;
exports[`v-text > work with dynamic component 1`] = `
-"import { createDynamicComponent as _createDynamicComponent, toDisplayString as _toDisplayString, setBlockText as _setBlockText, renderEffect as _renderEffect } from 'vue';
+"import { createDynamicComponent as _createDynamicComponent, toDisplayString as _toDisplayString, setBlockTextBinding as _setBlockTextBinding } from 'vue';
export function render(_ctx) {
const n0 = _createDynamicComponent(() => (_ctx.Comp), null, null, true)
- _renderEffect(() => _setBlockText(n0, _toDisplayString(_ctx.foo)))
+ _setBlockTextBinding(n0, () => _toDisplayString(_ctx.foo))
return n0
}"
`;
diff --git a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
index 814ca6678c4..0205b2552d0 100644
--- a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
@@ -127,6 +127,46 @@ describe('compiler: children transform', () => {
expect(code).toMatchSnapshot()
})
+ test('lowers mixed generated text binding without hiding segment display conversion', () => {
+ const { code, helpers } = compileWithElementTransform(
+ `
{{ a }} - {{ b }}
`,
+ )
+
+ expect(code).toMatchSnapshot()
+ expect(code).toContain(
+ `_setTextBinding(n0, () => _toDisplayString(_ctx.a) + " - " + _toDisplayString(_ctx.b))`,
+ )
+ expect(code).not.toContain(`_txt(`)
+ expect(code).not.toContain(`_renderEffect`)
+ expect(code).not.toContain(`_setText(`)
+ expect(Array.from(helpers)).containSubset([
+ 'setTextBinding',
+ 'toDisplayString',
+ ])
+ expect(helpers).not.contains('txt')
+ expect(helpers).not.contains('renderEffect')
+ expect(helpers).not.contains('setText')
+ })
+
+ test('does not lower generated text binding when expressions need declarations', () => {
+ const { code, helpers } = compileWithElementTransform(
+ `
{{ foo + foo }}
`,
+ )
+
+ expect(code).toMatchSnapshot()
+ expect(code).toContain(`const x0 = _txt(n0)`)
+ expect(code).toContain(`const _foo = _ctx.foo`)
+ expect(code).toContain(`_setText(x0, _toDisplayString(_foo + _foo))`)
+ expect(code).not.toContain(`_setTextBinding(`)
+ expect(Array.from(helpers)).containSubset([
+ 'txt',
+ 'renderEffect',
+ 'setText',
+ 'toDisplayString',
+ ])
+ expect(helpers).not.contains('setTextBinding')
+ })
+
test('anchor insertion in middle', () => {
const { code } = compileWithElementTransform(
`
diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
index 1c784edea56..14f0bfc1bff 100644
--- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
@@ -857,7 +857,7 @@ describe('compiler: element transform', () => {
],
},
])
- expect(code).contains('_setDynamicProps(n0, [_ctx.obj])')
+ expect(code).contains('_setDynamicPropsBinding(n0, () => [_ctx.obj])')
})
test('v-bind="obj" after static prop', () => {
@@ -893,7 +893,9 @@ describe('compiler: element transform', () => {
],
},
])
- expect(code).contains('_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj])')
+ expect(code).contains(
+ '_setMergedDynamicPropsBinding(n0, { id: "foo" }, () => _ctx.obj)',
+ )
})
test('v-bind="obj" before static prop', () => {
@@ -919,7 +921,9 @@ describe('compiler: element transform', () => {
],
},
])
- expect(code).contains('_setDynamicProps(n0, [_ctx.obj, { id: "foo" }])')
+ expect(code).contains(
+ '_setMergedDynamicPropsBinding(n0, null, () => _ctx.obj, { id: "foo" })',
+ )
})
test('v-bind="obj" between static props', () => {
@@ -947,8 +951,29 @@ describe('compiler: element transform', () => {
},
])
expect(code).contains(
- '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }])',
+ '_setMergedDynamicPropsBinding(n0, { id: "foo" }, () => _ctx.obj, { class: "bar" })',
+ )
+ })
+
+ test('v-bind="obj" between static props on svg', () => {
+ const { code } = compileWithElementTransform(
+ `
`,
+ )
+ expect(code).toMatchSnapshot()
+ expect(code).contains(
+ '_setMergedDynamicPropsBinding(n0, { id: "foo" }, () => _ctx.obj, { class: "bar" }, true)',
+ )
+ })
+
+ test('multiple v-bind expressions with static props', () => {
+ const { code } = compileWithElementTransform(
+ `
`,
+ )
+ expect(code).toMatchSnapshot()
+ expect(code).contains(
+ '_setDynamicPropsBinding(n0, () => [{ id: "foo", [_ctx.key]: _ctx.foo }, _ctx.bar, { class: "baz" }])',
)
+ expect(code).not.contains('_setMergedDynamicPropsBinding')
})
test('props merging: event handlers', () => {
@@ -1074,7 +1099,7 @@ describe('compiler: element transform', () => {
],
},
])
- expect(code).contains('_setDynamicEvents(n0, _ctx.obj)')
+ expect(code).contains('_setDynamicEventsBinding(n0, () => _ctx.obj)')
})
test('component with dynamic prop arguments', () => {
diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
index b4de4691cdd..de7c06e52b6 100644
--- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
@@ -74,7 +74,8 @@ describe('compiler v-bind', () => {
})
expect(code).matchSnapshot()
- expect(code).contains('_setProp(n0, "id", _ctx.id')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains('_setPropBinding(n0, "id", () => _ctx.id')
})
test('no expression', () => {
@@ -104,7 +105,7 @@ describe('compiler v-bind', () => {
],
},
})
- expect(code).contains('_setProp(n0, "id", _ctx.id)')
+ expect(code).contains('_setPropBinding(n0, "id", () => _ctx.id)')
})
test('no expression (shorthand)', () => {
@@ -126,7 +127,19 @@ describe('compiler v-bind', () => {
],
},
})
- expect(code).contains('_setAttr(n0, "camel-case", _ctx.camelCase)')
+ expect(code).contains(
+ '_setAttrBinding(n0, "camel-case", () => _ctx.camelCase)',
+ )
+ })
+
+ test('does not lower prop binding when expressions need declarations', () => {
+ const { code } = compileWithVBind(`
`)
+
+ expect(code).matchSnapshot()
+ expect(code).contains('const _foo = _ctx.foo')
+ expect(code).contains('_renderEffect')
+ expect(code).contains('_setProp(n0, "id", _foo + _foo)')
+ expect(code).not.contains('_setPropBinding')
})
test('dynamic arg', () => {
@@ -286,7 +299,7 @@ describe('compiler v-bind', () => {
})
expect(code).matchSnapshot()
- expect(code).contains('_setProp(n0, "fooBar", _ctx.id)')
+ expect(code).contains('_setPropBinding(n0, "fooBar", () => _ctx.id)')
})
test('.camel modifier w/ no expression', () => {
@@ -309,8 +322,8 @@ describe('compiler v-bind', () => {
modifier: undefined,
},
})
- expect(code).contains('renderEffect')
- expect(code).contains('_setProp(n0, "fooBar", _ctx.fooBar)')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains('_setPropBinding(n0, "fooBar", () => _ctx.fooBar)')
})
test('.camel modifier w/ dynamic arg', () => {
@@ -339,9 +352,9 @@ describe('compiler v-bind', () => {
})
expect(code).matchSnapshot()
- expect(code).contains('renderEffect')
+ expect(code).not.contains('renderEffect')
expect(code).contains(
- `_setDynamicProps(n0, [{ [_camelize(_ctx.foo || "")]: _ctx.id }])`,
+ `_setDynamicPropsBinding(n0, () => [{ [_camelize(_ctx.foo || "")]: _ctx.id }])`,
)
})
@@ -396,8 +409,8 @@ describe('compiler v-bind', () => {
modifier: '.',
},
})
- expect(code).contains('renderEffect')
- expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.id)')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains('_setDOMPropBinding(n0, "fooBar", () => _ctx.id)')
})
test('.prop modifier w/ no expression', () => {
@@ -420,8 +433,8 @@ describe('compiler v-bind', () => {
modifier: '.',
},
})
- expect(code).contains('renderEffect')
- expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.fooBar)')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains('_setDOMPropBinding(n0, "fooBar", () => _ctx.fooBar)')
})
test('.prop modifier w/ dynamic arg', () => {
@@ -449,9 +462,9 @@ describe('compiler v-bind', () => {
],
],
})
- expect(code).contains('renderEffect')
+ expect(code).not.contains('renderEffect')
expect(code).contains(
- `_setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }])`,
+ `_setDynamicPropsBinding(n0, () => [{ ["." + _ctx.fooBar]: _ctx.id }])`,
)
})
@@ -504,8 +517,8 @@ describe('compiler v-bind', () => {
modifier: '.',
},
})
- expect(code).contains('renderEffect')
- expect(code).contains(' _setDOMProp(n0, "fooBar", _ctx.id)')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains(' _setDOMPropBinding(n0, "fooBar", () => _ctx.id)')
})
test('.prop modifier (shorthand) w/ no expression', () => {
@@ -528,20 +541,20 @@ describe('compiler v-bind', () => {
modifier: '.',
},
})
- expect(code).contains('renderEffect')
- expect(code).contains('_setDOMProp(n0, "fooBar", _ctx.fooBar)')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains('_setDOMPropBinding(n0, "fooBar", () => _ctx.fooBar)')
})
test('.prop modifier w/ innerHTML', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setHtml(n0, _ctx.foo)')
+ expect(code).contains('_setHtmlBinding(n0, () => _ctx.foo)')
})
test('.prop modifier (shorthand) w/ innerHTML', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setHtml(n0, _ctx.foo)')
+ expect(code).contains('_setHtmlBinding(n0, () => _ctx.foo)')
})
test('.prop modifier w/ textContent', () => {
@@ -559,25 +572,25 @@ describe('compiler v-bind', () => {
test('.prop modifier w/ value', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setValue(n0, _ctx.foo)')
+ expect(code).contains('_setValueBinding(n0, () => _ctx.foo)')
})
test('.prop modifier (shorthand) w/ value', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setValue(n0, _ctx.foo)')
+ expect(code).contains('_setValueBinding(n0, () => _ctx.foo)')
})
test('.prop modifier w/ progress value', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setDOMProp(n0, "value", _ctx.foo)')
+ expect(code).contains('_setDOMPropBinding(n0, "value", () => _ctx.foo)')
})
test('.prop modifier (shorthand) w/ progress value', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setDOMProp(n0, "value", _ctx.foo)')
+ expect(code).contains('_setDOMPropBinding(n0, "value", () => _ctx.foo)')
})
test('.attr modifier', () => {
@@ -600,8 +613,8 @@ describe('compiler v-bind', () => {
modifier: '^',
},
})
- expect(code).contains('renderEffect')
- expect(code).contains('_setAttr(n0, "foo-bar", _ctx.id)')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains('_setAttrBinding(n0, "foo-bar", () => _ctx.id)')
})
test('.attr modifier w/ no expression', () => {
@@ -625,32 +638,32 @@ describe('compiler v-bind', () => {
},
})
- expect(code).contains('renderEffect')
- expect(code).contains('_setAttr(n0, "foo-bar", _ctx.fooBar)')
+ expect(code).not.contains('renderEffect')
+ expect(code).contains('_setAttrBinding(n0, "foo-bar", () => _ctx.fooBar)')
})
test('.attr modifier w/ innerHTML', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setAttr(n0, "innerHTML", _ctx.foo)')
+ expect(code).contains('_setAttrBinding(n0, "innerHTML", () => _ctx.foo)')
})
test('.attr modifier w/ textContent', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setAttr(n0, "textContent", _ctx.foo)')
+ expect(code).contains('_setAttrBinding(n0, "textContent", () => _ctx.foo)')
})
test('.attr modifier w/ value', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setAttr(n0, "value", _ctx.foo)')
+ expect(code).contains('_setAttrBinding(n0, "value", () => _ctx.foo)')
})
test('.attr modifier w/ progress value', () => {
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setAttr(n0, "value", _ctx.foo)')
+ expect(code).contains('_setAttrBinding(n0, "value", () => _ctx.foo)')
})
test('attributes must be set as attribute', () => {
@@ -685,7 +698,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setHtml(n0, _ctx.foo)')
+ expect(code).contains('_setHtmlBinding(n0, () => _ctx.foo)')
})
test(':textContext', () => {
@@ -701,7 +714,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setValue(n0, _ctx.foo)')
+ expect(code).contains('_setValueBinding(n0, () => _ctx.foo)')
})
test(':value w/ progress', () => {
@@ -709,7 +722,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setProp(n0, "value", _ctx.foo)')
+ expect(code).contains('_setPropBinding(n0, "value", () => _ctx.foo)')
})
test(':class w/ svg elements', () => {
@@ -718,7 +731,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
// should pass isSVG: true to the helper
- expect(code).contains('_setClass(n0, _ctx.cls, true))')
+ expect(code).contains('_setClassBinding(n0, () => _ctx.cls, true)')
})
test('constant boolean class and style bindings are emitted in template', () => {
@@ -784,7 +797,9 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClassName(n0, (_ctx.isActive ? 1 : 0)')
+ expect(code).contains(
+ '_setClassNameBinding(n0, () => (_ctx.isActive ? 1 : 0)',
+ )
expect(code).contains('"active"')
expect(code).not.contains('{ active:')
})
@@ -795,7 +810,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setClassName(n0, (_ctx.selected === _ctx.row.id ? 1 : 0), "danger")',
+ '_setClassNameBinding(n0, () => (_ctx.selected === _ctx.row.id ? 1 : 0), "danger")',
)
})
@@ -805,7 +820,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setClassName(n0, (_ctx.selected === _ctx.row.id ? 0 : 1), "danger")',
+ '_setClassNameBinding(n0, () => (_ctx.selected === _ctx.row.id ? 0 : 1), "danger")',
)
})
@@ -815,7 +830,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- `_setClassName(n0, (_ctx.selected === _ctx.row.id ? 1 : 0), "danger", "", "foo")`,
+ `_setClassNameBinding(n0, () => (_ctx.selected === _ctx.row.id ? 1 : 0), "danger", "", "foo")`,
)
})
@@ -824,7 +839,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClassName(n0, (_ctx.isBar ? 1 : 0)')
+ expect(code).contains('_setClassNameBinding(n0, () => (_ctx.isBar ? 1 : 0)')
expect(code).contains('" bar", "foo"')
expect(code).not.contains('{ bar:')
})
@@ -835,7 +850,7 @@ describe('compiler v-bind', () => {
`)
expect(code).contains('const t0 = _template("
", 1)')
- expect(code).contains('_setClassName(n0,')
+ expect(code).contains('_setClassNameBinding(n0,')
expect(code).contains('"base"')
expect(code).not.contains('class=\\"base')
})
@@ -846,7 +861,7 @@ describe('compiler v-bind', () => {
`)
expect(code).contains('const t0 = _template("
", 1)')
- expect(code).contains('_setClass(n0, ["base", _ctx.cls])')
+ expect(code).contains('_setClassBinding(n0, () => ["base", _ctx.cls])')
expect(code).not.contains('class=base')
expect(code).not.contains('_setClassName')
})
@@ -857,7 +872,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setClassName(n0, (_ctx.isBar ? 1 : 0), "bar", "", "foo")',
+ '_setClassNameBinding(n0, () => (_ctx.isBar ? 1 : 0), "bar", "", "foo")',
)
})
@@ -867,7 +882,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setClassName(n0, (_ctx.ok ? 1 : 0) | (_ctx.bar ? 2 : 0), [" active", " foo"], "", "tail")',
+ '_setClassNameBinding(n0, () => (_ctx.ok ? 1 : 0) | (_ctx.bar ? 2 : 0), [" active", " foo"], "", "tail")',
)
})
@@ -877,7 +892,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setClassName(n0, (_ctx.ok ? 1 : 0) | (_ctx.bar ? 2 : 0)',
+ '_setClassNameBinding(n0, () => (_ctx.ok ? 1 : 0) | (_ctx.bar ? 2 : 0)',
)
expect(code).contains('[" active", " foo"]')
expect(code).not.contains('{ active:')
@@ -889,7 +904,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setClassName(n0, (_ctx.selected === _ctx.row.id ? 1 : 0) | (_ctx.active ? 2 : 0), [" danger", " is-active"], "foo")',
+ '_setClassNameBinding(n0, () => (_ctx.selected === _ctx.row.id ? 1 : 0) | (_ctx.active ? 2 : 0), [" danger", " is-active"], "foo")',
)
expect(code).not.contains('{ danger:')
})
@@ -899,7 +914,9 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClassName(n0, (_ctx.isActive ? 1 : 0)')
+ expect(code).contains(
+ '_setClassNameBinding(n0, () => (_ctx.isActive ? 1 : 0)',
+ )
expect(code).contains('"foo bar"')
expect(code).not.contains("'foo bar':")
})
@@ -909,7 +926,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClassName(n0, (_ctx.isBar ? 1 : 0)')
+ expect(code).contains('_setClassNameBinding(n0, () => (_ctx.isBar ? 1 : 0)')
expect(code).contains('" bar", "bar"')
expect(code).not.contains('{ bar:')
})
@@ -919,7 +936,9 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClassName(n0, (_ctx.isActive ? 1 : 0)')
+ expect(code).contains(
+ '_setClassNameBinding(n0, () => (_ctx.isActive ? 1 : 0)',
+ )
expect(code).contains('" foo bar", "foo"')
expect(code).not.contains("'foo bar':")
})
@@ -930,7 +949,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setClassName(n0, (_ctx.ok ? 1 : 0), " baz", "foo bar")',
+ '_setClassNameBinding(n0, () => (_ctx.ok ? 1 : 0), " baz", "foo bar")',
)
})
@@ -940,7 +959,7 @@ describe('compiler v-bind', () => {
)
const { code } = compileWithVBind(`
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClass(n0, {')
+ expect(code).contains('_setClassBinding(n0, () => ({')
expect(code).not.contains('_setClassName')
})
@@ -951,7 +970,7 @@ describe('compiler v-bind', () => {
const { code } = compileWithVBind(`
`)
expect(code).contains('_setClassName')
expect(code).contains('(_ctx.a30 ? 1073741824 : 0)')
- expect(code).not.contains('_setClass(n0, {')
+ expect(code).not.contains('_setClassBinding(n0, () => ({')
})
test('computed object class key falls back to setClass', () => {
@@ -959,7 +978,9 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClass(n0, { [_ctx.name]: _ctx.active })')
+ expect(code).contains(
+ '_setClassBinding(n0, () => ({ [_ctx.name]: _ctx.active })',
+ )
expect(code).not.contains('_setClassName')
})
@@ -968,7 +989,9 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setClass(n0, [_ctx.foo, { danger: _ctx.active }])')
+ expect(code).contains(
+ '_setClassBinding(n0, () => [_ctx.foo, { danger: _ctx.active }])',
+ )
expect(code).not.contains('_setClassName')
})
@@ -978,7 +1001,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
expect(code).contains(
- '_setDynamicProps(n0, [{ class: ["foo", { bar: _ctx.isBar }] }, _ctx.mayBeHasClass])',
+ '_setDynamicPropsBinding(n0, () => [{ class: ["foo", { bar: _ctx.isBar }] }, _ctx.mayBeHasClass])',
)
expect(code).not.contains('_setClassName')
})
@@ -988,7 +1011,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setStyle(n0, _ctx.style))')
+ expect(code).contains('_setStyleBinding(n0, () => _ctx.style)')
})
test('v-bind w/ svg elements', () => {
@@ -996,7 +1019,7 @@ describe('compiler v-bind', () => {
`)
expect(code).matchSnapshot()
- expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true))')
+ expect(code).contains('_setDynamicPropsBinding(n0, () => [_ctx.obj], true)')
})
test('number value', () => {
diff --git a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts
index fff8b3bd3e8..a87e534134a 100644
--- a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts
@@ -25,7 +25,7 @@ describe('v-html', () => {
},
)
- expect(helpers).contains('setHtml')
+ expect(helpers).contains('setHtmlBinding')
expect(ir.block.operation).toMatchObject([])
expect(ir.block.effect).toMatchObject([
@@ -57,13 +57,25 @@ describe('v-html', () => {
test('work with dynamic component', () => {
const { code } = compileWithVHtml(`
`)
expect(code).matchSnapshot()
- expect(code).contains('setBlockHtml(n0, _ctx.foo))')
+ expect(code).contains('setBlockHtmlBinding(n0, () => _ctx.foo)')
+ })
+
+ test('wrap object literal expression in binding getter', () => {
+ const { code } = compileWithVHtml(`
`)
+ expect(code).contains('_setHtmlBinding(n0, () => ({ foo: _ctx.bar }))')
+ expect(code).matchSnapshot()
+ })
+
+ test('wrap component object literal expression in binding getter', () => {
+ const { code } = compileWithVHtml(`
`)
+ expect(code).contains('_setBlockHtmlBinding(n0, () => ({ foo: _ctx.bar }))')
+ expect(code).matchSnapshot()
})
test('work with component', () => {
const { code } = compileWithVHtml(`
`)
expect(code).matchSnapshot()
- expect(code).contains('setBlockHtml(n0, _ctx.foo))')
+ expect(code).contains('setBlockHtmlBinding(n0, () => _ctx.foo)')
})
test('should raise error and ignore children when v-html is present', () => {
@@ -75,7 +87,7 @@ describe('v-html', () => {
},
)
- expect(helpers).contains('setHtml')
+ expect(helpers).contains('setHtmlBinding')
// children should have been removed
expect([...ir.template.keys()]).toEqual(['
'])
diff --git a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts
index 37a8c8e622c..d536f9ad20f 100644
--- a/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vOn.spec.ts
@@ -87,8 +87,9 @@ describe('v-on', () => {
`
`,
)
- expect(helpers).contains('onBinding')
- expect(helpers).contains('renderEffect')
+ expect(helpers).contains('setEventBinding')
+ expect(helpers).not.contains('on')
+ expect(helpers).not.contains('renderEffect')
expect(ir.block.operation).toMatchObject([])
expect(ir.block.effect[0].operations[0]).toMatchObject({
@@ -107,7 +108,9 @@ describe('v-on', () => {
})
expect(code).matchSnapshot()
- expect(code).contains(`_onBinding(n0, _ctx.event, e => _ctx.handler(e))`)
+ expect(code).contains(
+ `_setEventBinding(n0, () => _ctx.event, e => _ctx.handler(e))`,
+ )
})
test('dynamic arg with prefixing', () => {
@@ -126,9 +129,11 @@ describe('v-on', () => {
},
)
- expect(helpers).contains('onBinding')
+ expect(helpers).contains('setEventBinding')
expect(code).matchSnapshot()
- expect(code).contains(`_onBinding(n0, _ctx.event, e => _ctx.handler(e), {`)
+ expect(code).contains(
+ `_setEventBinding(n0, () => _ctx.event, e => _ctx.handler(e), {`,
+ )
expect(code).contains('capture: true')
expect(code).contains('once: true')
expect(code).not.contains('effect: true')
@@ -142,8 +147,9 @@ describe('v-on', () => {
},
)
- expect(helpers).contains('onBinding')
- expect(helpers).contains('renderEffect')
+ expect(helpers).contains('setEventBinding')
+ expect(helpers).not.contains('on')
+ expect(helpers).not.contains('renderEffect')
expect(ir.block.operation).toMatchObject([])
expect(ir.block.effect[0].operations[0]).toMatchObject({
@@ -164,6 +170,19 @@ describe('v-on', () => {
expect(code).matchSnapshot()
})
+ test('dynamic arg falls back to onBinding when expressions need declarations', () => {
+ const { code, helpers } = compileWithVOn(
+ `
`,
+ )
+
+ expect(helpers).contains('onBinding')
+ expect(helpers).contains('renderEffect')
+ expect(helpers).not.contains('setEventBinding')
+ expect(code).matchSnapshot()
+ expect(code).contains('const _event = _ctx.event')
+ expect(code).contains('_onBinding(n0, _event+_event, e => _ctx.handler(e))')
+ })
+
test('should wrap as function if expression is inline statement', () => {
const { code, ir, helpers } = compileWithVOn(`
`)
@@ -748,7 +767,7 @@ describe('v-on', () => {
expect(code).contains(
'n0.$evtkeyup = _withKeys(e => _ctx.foo(e), ["enter"])',
)
- expect(code).contains('_onBinding(n1, _ctx.event, _withKeys1')
+ expect(code).contains('_setEventBinding(n1, () => _ctx.event, _withKeys1')
expect(code).contains('e => _ctx.bar(e), ["enter"]))')
})
diff --git a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
index 8d83db62b5f..5d3228ad525 100644
--- a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
@@ -122,6 +122,16 @@ describe('compiler: v-once', () => {
])
})
+ test('with v-on object', () => {
+ const { ir, code } = compileWithOnce(`
`)
+
+ expect(code).toMatchSnapshot()
+ expect(ir.block.effect).lengthOf(0)
+ expect(code).contains('_setDynamicEvents(n0, _ctx.obj)')
+ expect(code).not.contains('_setDynamicEventsBinding')
+ expect(code).not.contains('_renderEffect')
+ })
+
test('on component', () => {
const { ir, code } = compileWithOnce(`
`)
expect(code).toMatchSnapshot()
diff --git a/packages/compiler-vapor/__tests__/transforms/vText.spec.ts b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts
index ce2410efb2b..c65f18c1638 100644
--- a/packages/compiler-vapor/__tests__/transforms/vText.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts
@@ -22,7 +22,7 @@ describe('v-text', () => {
},
})
- expect(helpers).contains('setText')
+ expect(helpers).contains('setTextBinding')
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.GET_TEXT_CHILD,
@@ -61,13 +61,17 @@ describe('v-text', () => {
test('work with dynamic component', () => {
const { code } = compileWithVText(`
`)
expect(code).matchSnapshot()
- expect(code).contains('setBlockText(n0, _toDisplayString(_ctx.foo))')
+ expect(code).contains(
+ 'setBlockTextBinding(n0, () => _toDisplayString(_ctx.foo))',
+ )
})
test('work with component', () => {
const { code } = compileWithVText(`
`)
expect(code).matchSnapshot()
- expect(code).contains('setBlockText(n0, _toDisplayString(_ctx.foo))')
+ expect(code).contains(
+ 'setBlockTextBinding(n0, () => _toDisplayString(_ctx.foo))',
+ )
})
test('work with plain template createElement path', () => {
diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts
index fd2879c0eaa..fb25e584ee1 100644
--- a/packages/compiler-vapor/src/generators/block.ts
+++ b/packages/compiler-vapor/src/generators/block.ts
@@ -16,11 +16,7 @@ import {
genMulti,
} from './utils'
import type { CodegenContext } from '../generate'
-import {
- genEffects,
- genOperationWithInsertionState,
- genOperations,
-} from './operation'
+import { genOperationsAndEffects } from './operation'
import { genChildren, genSelf } from './template'
import { toValidAssetId } from '@vue/compiler-dom'
@@ -92,17 +88,15 @@ export function genBlockContent(
effectEnd: number,
push: (...items: CodeFragment[]) => number,
) => {
- while (operationIndex < operationEnd) {
- push(
- ...genOperationWithInsertionState(operation[operationIndex], context),
- )
- operationIndex++
- }
-
- if (effectIndex < effectEnd) {
- push(...genEffects(effect.slice(effectIndex, effectEnd), context))
- effectIndex = effectEnd
- }
+ push(
+ ...genOperationsAndEffects(
+ operation.slice(operationIndex, operationEnd),
+ effect.slice(effectIndex, effectEnd),
+ context,
+ ),
+ )
+ operationIndex = operationEnd
+ effectIndex = effectEnd
}
const flushBeforeDynamic = (
dynamic: IRDynamicInfo,
@@ -141,13 +135,19 @@ export function genBlockContent(
}
}
- if (operationIndex < operation.length) {
- push(...genOperations(operation.slice(operationIndex), context))
- }
- if (effectIndex < effect.length) {
- push(...genEffects(effect.slice(effectIndex), context, genEffectsExtraFrag))
- } else if (genEffectsExtraFrag) {
- push(...genEffects([], context, genEffectsExtraFrag))
+ if (
+ operationIndex < operation.length ||
+ effectIndex < effect.length ||
+ genEffectsExtraFrag
+ ) {
+ push(
+ ...genOperationsAndEffects(
+ operation.slice(operationIndex),
+ effect.slice(effectIndex),
+ context,
+ genEffectsExtraFrag,
+ ),
+ )
}
push(NEWLINE, `return `)
diff --git a/packages/compiler-vapor/src/generators/event.ts b/packages/compiler-vapor/src/generators/event.ts
index c84498217b4..2d6827c94bb 100644
--- a/packages/compiler-vapor/src/generators/event.ts
+++ b/packages/compiler-vapor/src/generators/event.ts
@@ -12,7 +12,7 @@ import {
type SetDynamicEventsIRNode,
type SetEventIRNode,
} from '../ir'
-import { genExpression } from './expression'
+import { genExpression, genExpressionGetter } from './expression'
import {
type CodeFragment,
DELIMITERS_OBJECT_NEWLINE,
@@ -27,7 +27,6 @@ export function genSetEvent(
): CodeFragment[] {
const { helper } = context
const { element, key, keyOverride, value, modifiers, delegate, effect } = oper
-
let handler: CodeFragment[] | undefined
if (delegate) {
@@ -45,16 +44,14 @@ export function genSetEvent(
}
}
- const name = genName()
- const eventOptions = genEventOptions()
return [
NEWLINE,
...genCall(
helper(effect ? 'onBinding' : delegate ? 'delegate' : 'on'),
`n${element}`,
- name,
+ genName(),
genHandler(),
- eventOptions,
+ genCurrentEventOptions(),
),
]
@@ -87,7 +84,7 @@ export function genSetEvent(
}
}
- function genEventOptions(): CodeFragment[] | undefined {
+ function genCurrentEventOptions(): CodeFragment[] | undefined {
let { options } = modifiers
if (!options.length) return
@@ -110,6 +107,23 @@ export function genSetEvent(
}
}
+export function genSetEventBinding(
+ oper: SetEventIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const { helper } = context
+ return [
+ NEWLINE,
+ ...genCall(
+ helper('setEventBinding'),
+ `n${oper.element}`,
+ ['() => ', ...genEventName(oper, context)],
+ genEventHandler(context, [oper.value], oper.modifiers),
+ genEventOptions(oper),
+ ),
+ ]
+}
+
export function genSetDynamicEvents(
oper: SetDynamicEventsIRNode,
context: CodegenContext,
@@ -125,6 +139,49 @@ export function genSetDynamicEvents(
]
}
+export function genSetDynamicEventsBinding(
+ oper: SetDynamicEventsIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const { helper } = context
+ return [
+ NEWLINE,
+ ...genCall(
+ helper('setDynamicEventsBinding'),
+ `n${oper.element}`,
+ genExpressionGetter(oper.event, context),
+ ),
+ ]
+}
+
+function genEventName(
+ { key, keyOverride }: SetEventIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const expr = genExpression(key, context)
+ if (keyOverride) {
+ // TODO unit test
+ const find = JSON.stringify(keyOverride[0])
+ const replacement = JSON.stringify(keyOverride[1])
+ const wrapped: CodeFragment[] = ['(', ...expr, ')']
+ return [...wrapped, ` === ${find} ? ${replacement} : `, ...wrapped]
+ } else {
+ return expr
+ }
+}
+
+function genEventOptions({
+ modifiers,
+}: SetEventIRNode): CodeFragment[] | undefined {
+ const { options } = modifiers
+ if (!options.length) return
+
+ return genMulti(
+ DELIMITERS_OBJECT_NEWLINE,
+ ...options.map((option): CodeFragment[] => [`${option}: true`]),
+ )
+}
+
interface GenEventHandlerOptions {
// Generate handler expressions suitable for passing as component props
// (avoid wrapping member expressions with invocation).
diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts
index dbd5ea760df..e987ce7acd9 100644
--- a/packages/compiler-vapor/src/generators/expression.ts
+++ b/packages/compiler-vapor/src/generators/expression.ts
@@ -118,6 +118,25 @@ export function genExpression(
}
}
+export function genExpressionGetter(
+ node: SimpleExpressionNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const expr = genExpression(node, context)
+ if (isObjectExpression(node)) {
+ return ['() => (', ...expr, ')']
+ }
+ return ['() => ', ...expr]
+}
+
+function isObjectExpression(node: SimpleExpressionNode): boolean {
+ const ast = node.ast
+ return (
+ node.content.trim().charCodeAt(0) === 123 ||
+ !!(ast && ast.type === 'ObjectExpression')
+ )
+}
+
function genIdentifier(
raw: string,
context: CodegenContext,
diff --git a/packages/compiler-vapor/src/generators/html.ts b/packages/compiler-vapor/src/generators/html.ts
index 711ee421d86..d348f9cb925 100644
--- a/packages/compiler-vapor/src/generators/html.ts
+++ b/packages/compiler-vapor/src/generators/html.ts
@@ -1,6 +1,6 @@
import type { CodegenContext } from '../generate'
import type { SetHtmlIRNode } from '../ir'
-import { genExpression } from './expression'
+import { genExpression, genExpressionGetter } from './expression'
import { type CodeFragment, NEWLINE, genCall } from './utils'
export function genSetHtml(
@@ -20,3 +20,20 @@ export function genSetHtml(
),
]
}
+
+export function genSetHtmlBinding(
+ oper: SetHtmlIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const { helper } = context
+
+ const { value, element, isComponent } = oper
+ return [
+ NEWLINE,
+ ...genCall(
+ helper(isComponent ? 'setBlockHtmlBinding' : 'setHtmlBinding'),
+ `n${element}`,
+ genExpressionGetter(value, context),
+ ),
+ ]
+}
diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts
index fcefe437346..3c28d65c0a7 100644
--- a/packages/compiler-vapor/src/generators/operation.ts
+++ b/packages/compiler-vapor/src/generators/operation.ts
@@ -3,17 +3,39 @@ import {
IRNodeTypes,
type InsertionStateTypes,
type OperationNode,
+ type SetDynamicEventsIRNode,
+ type SetDynamicPropsIRNode,
+ type SetEventIRNode,
+ type SetHtmlIRNode,
+ type SetPropIRNode,
+ type SetTextIRNode,
isBlockOperation,
} from '../ir'
import type { CodegenContext } from '../generate'
import { genInsertNode, genPrependNode } from './dom'
-import { genSetDynamicEvents, genSetEvent } from './event'
+import {
+ genSetDynamicEvents,
+ genSetDynamicEventsBinding,
+ genSetEvent,
+ genSetEventBinding,
+} from './event'
import { genFor } from './for'
-import { genSetHtml } from './html'
+import { genSetHtml, genSetHtmlBinding } from './html'
import { genIf } from './if'
-import { genDynamicProps, genSetProp } from './prop'
+import {
+ canSetPropBinding,
+ genDynamicProps,
+ genDynamicPropsBinding,
+ genSetProp,
+ genSetPropBinding,
+} from './prop'
import { genSetTemplateRef } from './templateRef'
-import { genGetTextChild, genSetText } from './text'
+import {
+ genGetTextChild,
+ genSetBlockTextBinding,
+ genSetText,
+ genSetTextBinding,
+} from './text'
import {
type CodeFragment,
INDENT_END,
@@ -31,10 +53,58 @@ import { genKey, genSetBlockKey } from './key'
export function genOperations(
opers: OperationNode[],
context: CodegenContext,
+ withInsertionState = true,
): CodeFragment[] {
const [frag, push] = buildCodeFragment()
for (const operation of opers) {
- push(...genOperationWithInsertionState(operation, context))
+ push(
+ ...(withInsertionState
+ ? genOperationWithInsertionState(operation, context)
+ : genOperation(operation, context)),
+ )
+ }
+ return frag
+}
+
+export function genOperationsAndEffects(
+ opers: OperationNode[],
+ effects: IREffect[],
+ context: CodegenContext,
+ genExtraFrag?: () => CodeFragment[],
+ withInsertionState = true,
+): CodeFragment[] {
+ let processedExpressions: ProcessedExpressions | undefined
+ if (!genExtraFrag) {
+ const binding = resolveSingleOperationBinding(opers, effects, context)
+ if (binding) {
+ processedExpressions = processExpressions(
+ context,
+ effects[0].expressions,
+ true,
+ )
+ // Keep declaration-producing effects on the normal renderEffect path. The
+ // block getter form is valid but saves little after minification. Reuse the
+ // processed result in the fallback path so expression analysis only runs once.
+ if (!hasDeclarations(processedExpressions)) {
+ const [frag, push] = buildCodeFragment()
+ push(...genOperations(binding.operations, context, withInsertionState))
+ push(
+ ...context.withExpressionReplacements(
+ processedExpressions!.expressionReplacements,
+ () => context.withId(binding.genBinding, processedExpressions!.ids),
+ ),
+ )
+ return frag
+ }
+ }
+ }
+
+ const [frag, push] = buildCodeFragment()
+ push(...genOperations(opers, context, withInsertionState))
+ if (effects.length) {
+ push(...genEffects(effects, context, genExtraFrag, processedExpressions))
+ } else if (genExtraFrag) {
+ push(...genEffects([], context, genExtraFrag))
}
return frag
}
@@ -102,18 +172,22 @@ export function genEffects(
effects: IREffect[],
context: CodegenContext,
genExtraFrag?: () => CodeFragment[],
+ processedExpressions?: ProcessedExpressions,
): CodeFragment[] {
const { helper } = context
const expressions = effects.flatMap(effect => effect.expressions)
const [frag, push, unshift] = buildCodeFragment()
const shouldDeclare = genExtraFrag === undefined
let operationsCount = 0
+ const processed =
+ processedExpressions ||
+ processExpressions(context, expressions, shouldDeclare)
const {
ids,
frag: declarationFrags,
varNames,
expressionReplacements,
- } = processExpressions(context, expressions, shouldDeclare)
+ } = processed
return context.withExpressionReplacements(expressionReplacements, () => {
push(...declarationFrags)
for (let i = 0; i < effects.length; i++) {
@@ -157,6 +231,136 @@ export function genEffects(
})
}
+type SingleOperationBindingLowering = {
+ operations: OperationNode[]
+ genBinding: () => CodeFragment[]
+}
+
+type ProcessedExpressions = ReturnType
+
+function hasDeclarations({ frag, varNames }: ProcessedExpressions): boolean {
+ return frag.length > 0 || varNames.length > 0
+}
+
+function resolveSingleOperationBinding(
+ opers: OperationNode[],
+ effects: IREffect[],
+ context: CodegenContext,
+): SingleOperationBindingLowering | undefined {
+ if (effects.length !== 1) return
+
+ const effect = effects[0]
+ if (effect.operations.length !== 1) return
+
+ switch (effect.operations[0].type) {
+ case IRNodeTypes.SET_PROP:
+ return resolveSetPropBinding(opers, effect.operations[0], context)
+ case IRNodeTypes.SET_DYNAMIC_PROPS:
+ return resolveDynamicPropsBinding(opers, effect.operations[0], context)
+ case IRNodeTypes.SET_HTML:
+ return resolveSetHtmlBinding(opers, effect.operations[0], context)
+ case IRNodeTypes.SET_TEXT:
+ return resolveSetTextBinding(opers, effect.operations[0], context)
+ case IRNodeTypes.SET_EVENT:
+ return resolveSetEventBinding(opers, effect.operations[0], context)
+ case IRNodeTypes.SET_DYNAMIC_EVENTS:
+ return resolveDynamicEventsBinding(opers, effect.operations[0], context)
+ default:
+ return
+ }
+}
+
+function resolveSetHtmlBinding(
+ opers: OperationNode[],
+ setHtml: SetHtmlIRNode,
+ context: CodegenContext,
+): SingleOperationBindingLowering | undefined {
+ return {
+ operations: opers,
+ genBinding: () => genSetHtmlBinding(setHtml, context),
+ }
+}
+
+function resolveSetPropBinding(
+ opers: OperationNode[],
+ setProp: SetPropIRNode,
+ context: CodegenContext,
+): SingleOperationBindingLowering | undefined {
+ if (!canSetPropBinding(setProp)) return
+
+ return {
+ operations: opers,
+ genBinding: () => genSetPropBinding(setProp, context),
+ }
+}
+
+function resolveDynamicPropsBinding(
+ opers: OperationNode[],
+ dynamicProps: SetDynamicPropsIRNode,
+ context: CodegenContext,
+): SingleOperationBindingLowering | undefined {
+ return {
+ operations: opers,
+ genBinding: () => genDynamicPropsBinding(dynamicProps, context),
+ }
+}
+
+function resolveSetEventBinding(
+ opers: OperationNode[],
+ setEvent: SetEventIRNode,
+ context: CodegenContext,
+): SingleOperationBindingLowering | undefined {
+ if (!setEvent.effect) return
+
+ return {
+ operations: opers,
+ genBinding: () => genSetEventBinding(setEvent, context),
+ }
+}
+
+function resolveDynamicEventsBinding(
+ opers: OperationNode[],
+ dynamicEvents: SetDynamicEventsIRNode,
+ context: CodegenContext,
+): SingleOperationBindingLowering | undefined {
+ return {
+ operations: opers,
+ genBinding: () => genSetDynamicEventsBinding(dynamicEvents, context),
+ }
+}
+
+function resolveSetTextBinding(
+ opers: OperationNode[],
+ setText: SetTextIRNode,
+ context: CodegenContext,
+): SingleOperationBindingLowering | undefined {
+ if (setText.isComponent) {
+ return {
+ operations: opers,
+ genBinding: () => genSetBlockTextBinding(setText, context),
+ }
+ }
+
+ if (opers.length === 0) return
+
+ const getTextChild = opers[opers.length - 1]
+ // Only generated text children can fold GET_TEXT_CHILD into setTextBinding.
+ // Existing non-generated text targets still need genSetText().
+ if (
+ getTextChild.type !== IRNodeTypes.GET_TEXT_CHILD ||
+ !setText.generated ||
+ setText.isComponent ||
+ setText.element !== getTextChild.parent
+ ) {
+ return
+ }
+
+ return {
+ operations: opers.slice(0, -1),
+ genBinding: () => genSetTextBinding(setText, context),
+ }
+}
+
export function genEffect(
{ operations }: IREffect,
context: CodegenContext,
diff --git a/packages/compiler-vapor/src/generators/prop.ts b/packages/compiler-vapor/src/generators/prop.ts
index d8864782bc6..61efe8c1f36 100644
--- a/packages/compiler-vapor/src/generators/prop.ts
+++ b/packages/compiler-vapor/src/generators/prop.ts
@@ -9,11 +9,13 @@ import type { CodegenContext } from '../generate'
import {
IRDynamicPropsKind,
type IRProp,
+ type IRProps,
+ type IRPropsDynamicAttribute,
type SetDynamicPropsIRNode,
type SetPropIRNode,
type VaporHelper,
} from '../ir'
-import { genExpression } from './expression'
+import { genExpression, genExpressionGetter } from './expression'
import {
type CodeFragment,
DELIMITERS_ARRAY,
@@ -49,6 +51,8 @@ export type HelperConfig = {
acceptRoot?: boolean
}
+type DirectDynamicPropSource = IRProp[] | IRPropsDynamicAttribute
+
// this should be kept in sync with runtime-vapor/src/dom/prop.ts
const helpers = {
setText: { name: 'setText' },
@@ -62,6 +66,18 @@ const helpers = {
setDOMProp: { name: 'setDOMProp', needKey: true },
} as const satisfies Partial>
+const bindingHelpers = {
+ setHtml: 'setHtmlBinding',
+ setClass: 'setClassBinding',
+ setClassName: 'setClassNameBinding',
+ setStyle: 'setStyleBinding',
+ setValue: 'setValueBinding',
+ setAttr: 'setAttrBinding',
+ setProp: 'setPropBinding',
+ setDOMProp: 'setDOMPropBinding',
+ setDynamicProps: 'setDynamicPropsBinding',
+} as const satisfies Partial>
+
// only the static key prop will reach here
export function genSetProp(
oper: SetPropIRNode,
@@ -94,6 +110,51 @@ export function genSetProp(
]
}
+export function canSetPropBinding(oper: SetPropIRNode): boolean {
+ const {
+ prop: { key, modifier },
+ tag,
+ } = oper
+ // `textContent` currently reuses setText(), whose target is not a generated
+ // text child here. Keep that case on the existing renderEffect path.
+ return getRuntimeHelper(tag, key.content, modifier).name !== 'setText'
+}
+
+export function genSetPropBinding(
+ oper: SetPropIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const { helper } = context
+ const {
+ prop: { key, values, modifier },
+ tag,
+ } = oper
+ const resolvedHelper = getRuntimeHelper(tag, key.content, modifier)
+ if (
+ key.content === 'class' &&
+ !resolvedHelper.isSVG &&
+ resolvedHelper.name === 'setClass'
+ ) {
+ const className = genSetClassNameBinding(oper, context)
+ if (className) return className
+ }
+
+ const bindingHelper =
+ bindingHelpers[resolvedHelper.name as keyof typeof bindingHelpers]
+ if (!bindingHelper) return genSetProp(oper, context)
+
+ return [
+ NEWLINE,
+ ...genCall(
+ [helper(bindingHelper), null],
+ `n${oper.element}`,
+ resolvedHelper.needKey ? genExpression(key, context) : false,
+ genPropValueGetter(values, context),
+ resolvedHelper.isSVG && 'true',
+ ),
+ ]
+}
+
interface ClassNameEntry {
className: string
condition?: SimpleExpressionNode
@@ -146,6 +207,41 @@ function genSetClassName(
]
}
+function genSetClassNameBinding(
+ oper: SetPropIRNode,
+ context: CodegenContext,
+): CodeFragment[] | undefined {
+ const info = resolveClassName(oper.prop.values, context)
+ if (!info) return
+
+ const { helper } = context
+ const flags = genClassFlags(info.entries, context)
+ const classFragments = info.entries.map(entry =>
+ JSON.stringify(
+ !info.prefix && info.entries.length === 1
+ ? entry.className
+ : ` ${entry.className}`,
+ ),
+ )
+ const fragments =
+ classFragments.length === 1
+ ? classFragments[0]
+ : genMulti(DELIMITERS_ARRAY, ...classFragments)
+
+ return [
+ NEWLINE,
+ ...genCall(
+ // Use an empty prefix placeholder so suffix can be emitted alone.
+ [helper('setClassNameBinding'), '""'],
+ `n${oper.element}`,
+ ['() => ', ...flags],
+ fragments,
+ info.prefix && JSON.stringify(info.prefix),
+ info.suffix && JSON.stringify(info.suffix),
+ ),
+ ]
+}
+
function resolveClassName(
values: SimpleExpressionNode[],
context: CodegenContext,
@@ -339,13 +435,7 @@ export function genDynamicProps(
): CodeFragment[] {
const { helper } = context
const isSVG = isSVGTag(oper.tag)
- const values = oper.props.map(props =>
- Array.isArray(props)
- ? genLiteralObjectProps(props, context) // static and dynamic arg props
- : props.kind === IRDynamicPropsKind.ATTRIBUTE
- ? genLiteralObjectProps([props], context) // dynamic arg props
- : genExpression(props.value, context),
- ) // v-bind=""
+ const values = genDynamicPropValues(oper, context)
return [
NEWLINE,
...genCall(
@@ -357,6 +447,145 @@ export function genDynamicProps(
]
}
+export function genDynamicPropsBinding(
+ oper: SetDynamicPropsIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const merged = genMergedDynamicPropsBinding(oper, context)
+ if (merged) return merged
+
+ const { helper } = context
+ const isSVG = isSVGTag(oper.tag)
+ const values = genDynamicPropValues(oper, context)
+ return [
+ NEWLINE,
+ ...genCall(
+ [helper('setDynamicPropsBinding'), null],
+ `n${oper.element}`,
+ ['() => ', ...genMulti(DELIMITERS_ARRAY, ...values)],
+ isSVG && 'true',
+ ),
+ ]
+}
+
+function genMergedDynamicPropsBinding(
+ oper: SetDynamicPropsIRNode,
+ context: CodegenContext,
+): CodeFragment[] | undefined {
+ const sources = resolveMergedDynamicPropSources(oper.props)
+ if (!sources) {
+ return
+ }
+
+ const { helper } = context
+ const isSVG = isSVGTag(oper.tag)
+ const before = sources.before
+ ? genDirectDynamicPropSource(sources.before, context)
+ : 'null'
+ const after = sources.after
+ ? genDirectDynamicPropSource(sources.after, context)
+ : isSVG
+ ? 'null'
+ : false
+ return [
+ NEWLINE,
+ ...genCall(
+ [helper('setMergedDynamicPropsBinding'), null],
+ `n${oper.element}`,
+ before,
+ genDynamicPropSourceGetter(sources.getter, context),
+ after,
+ isSVG && 'true',
+ ),
+ ]
+}
+
+function resolveMergedDynamicPropSources(props: IRProps[]):
+ | {
+ before?: DirectDynamicPropSource
+ getter: IRProps
+ after?: DirectDynamicPropSource
+ }
+ | undefined {
+ if (props.length <= 1) return
+
+ let before: DirectDynamicPropSource | undefined
+ let getter: IRProps | undefined
+ let after: DirectDynamicPropSource | undefined
+ for (const prop of props) {
+ if (isDirectDynamicPropSource(prop)) {
+ if (getter) {
+ if (after) return
+ after = prop
+ } else {
+ if (before) return
+ before = prop
+ }
+ } else {
+ if (getter) return
+ getter = prop
+ }
+ }
+
+ return getter && (before || after) ? { before, getter, after } : undefined
+}
+
+function genDirectDynamicPropSource(
+ prop: DirectDynamicPropSource,
+ context: CodegenContext,
+): CodeFragment[] {
+ return genLiteralObjectProps(Array.isArray(prop) ? prop : [prop], context)
+}
+
+function genDynamicPropSourceGetter(
+ prop: IRProps,
+ context: CodegenContext,
+): CodeFragment[] {
+ if (Array.isArray(prop)) {
+ return ['() => (', ...genLiteralObjectProps(prop, context), ')']
+ } else if (prop.kind === IRDynamicPropsKind.ATTRIBUTE) {
+ return ['() => (', ...genLiteralObjectProps([prop], context), ')']
+ } else {
+ return genExpressionGetter(prop.value, context)
+ }
+}
+
+function isDirectDynamicPropSource(
+ prop: IRProps,
+): prop is DirectDynamicPropSource {
+ return Array.isArray(prop)
+ ? canUseDirectDynamicPropSource(prop)
+ : prop.kind === IRDynamicPropsKind.ATTRIBUTE &&
+ canUseDirectDynamicPropSource([prop])
+}
+
+function canUseDirectDynamicPropSource(props: IRProp[]): boolean {
+ return (
+ props.length > 0 &&
+ props.every(
+ prop =>
+ prop.key.isStatic &&
+ prop.values.length === 1 &&
+ prop.values[0].isStatic &&
+ !prop.handler &&
+ !prop.model,
+ )
+ )
+}
+
+function genDynamicPropValues(
+ oper: SetDynamicPropsIRNode,
+ context: CodegenContext,
+): CodeFragment[][] {
+ return oper.props.map(props =>
+ Array.isArray(props)
+ ? genLiteralObjectProps(props, context) // static and dynamic arg props
+ : props.kind === IRDynamicPropsKind.ATTRIBUTE
+ ? genLiteralObjectProps([props], context) // dynamic arg props
+ : genExpression(props.value, context),
+ ) // v-bind=""
+}
+
function genLiteralObjectProps(
props: IRProp[],
context: CodegenContext,
@@ -428,6 +657,24 @@ export function genPropValue(
)
}
+function genPropValueGetter(
+ values: SimpleExpressionNode[],
+ context: CodegenContext,
+): CodeFragment[] {
+ const value = genPropValue(values, context)
+ const singleValue = values.length === 1 && values[0]
+ if (singleValue) {
+ const content = singleValue.content.trim()
+ if (
+ content.charCodeAt(0) === 123 ||
+ (singleValue.ast && singleValue.ast.type === 'ObjectExpression')
+ ) {
+ return ['() => (', ...value, ')']
+ }
+ }
+ return ['() => ', ...value]
+}
+
function getRuntimeHelper(
tag: string,
key: string,
diff --git a/packages/compiler-vapor/src/generators/text.ts b/packages/compiler-vapor/src/generators/text.ts
index 4cded2f10bf..93c067d37e7 100644
--- a/packages/compiler-vapor/src/generators/text.ts
+++ b/packages/compiler-vapor/src/generators/text.ts
@@ -49,3 +49,33 @@ export function genGetTextChild(
`const x${oper.parent} = ${context.helper('txt')}(n${oper.parent})`,
]
}
+
+export function genSetTextBinding(
+ oper: SetTextIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const { helper } = context
+ const text = combineValues(oper.values, context)
+ return [
+ NEWLINE,
+ ...genCall(helper('setTextBinding'), `n${oper.element}`, [
+ '() => ',
+ ...text,
+ ]),
+ ]
+}
+
+export function genSetBlockTextBinding(
+ oper: SetTextIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const { helper } = context
+ const text = combineValues(oper.values, context)
+ return [
+ NEWLINE,
+ ...genCall(helper('setBlockTextBinding'), `n${oper.element}`, [
+ '() => ',
+ ...text,
+ ]),
+ ]
+}
diff --git a/packages/runtime-vapor/__tests__/bench/domBinding.bench.ts b/packages/runtime-vapor/__tests__/bench/domBinding.bench.ts
new file mode 100644
index 00000000000..35aa4d8f6aa
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/bench/domBinding.bench.ts
@@ -0,0 +1,418 @@
+import { type Ref, effectScope } from '@vue/reactivity'
+import { nextTick, ref } from '@vue/runtime-dom'
+import { toDisplayString } from '@vue/shared'
+import { bench, describe } from 'vitest'
+import {
+ onBinding,
+ renderEffect,
+ setAttrBinding,
+ setBlockHtmlBinding,
+ setBlockTextBinding,
+ setClassBinding,
+ setClassNameBinding,
+ setDOMPropBinding,
+ setDynamicEvents,
+ setDynamicEventsBinding,
+ setDynamicPropsBinding,
+ setEventBinding,
+ setHtmlBinding,
+ setMergedDynamicPropsBinding,
+ setPropBinding,
+ setStyleBinding,
+ setTextBinding,
+ setValueBinding,
+ template,
+ txt,
+} from '../../src'
+import {
+ setAttr,
+ setBlockHtml,
+ setBlockText,
+ setClass,
+ setClassName,
+ setDOMProp,
+ setDynamicProps,
+ setHtml,
+ setProp,
+ setStyle,
+ setText,
+ setValue,
+} from '../../src/dom/prop'
+
+const MANY_BINDINGS = 100
+const UPDATE_BATCH = 20
+const TEXT_UPDATE_BATCH = 100
+
+type Source = Ref
+type Setup = (el: any, source: Source, index: number) => void
+type TextNodeWithCache = Text & { $txt?: string }
+
+function createDiv(): HTMLElement {
+ return document.createElement('div')
+}
+
+function createInput(): HTMLInputElement {
+ return document.createElement('input')
+}
+
+function createTextParent(): ParentNode {
+ return template('
', 1)() as ParentNode
+}
+
+function noop(): void {}
+
+function init(create: () => any, setup: Setup): void {
+ const scope = effectScope()
+ scope.run(() => {
+ const source = ref(0)
+ for (let i = 0; i < MANY_BINDINGS; i++) {
+ setup(create(), source, i)
+ }
+ })
+ scope.stop()
+}
+
+async function update(create: () => any, setup: Setup): Promise {
+ const scope = effectScope()
+ const source = ref(0)
+ try {
+ scope.run(() => {
+ for (let i = 0; i < MANY_BINDINGS; i++) {
+ setup(create(), source, i)
+ }
+ })
+ for (let i = 1; i <= UPDATE_BATCH; i++) {
+ source.value = i
+ await nextTick()
+ }
+ } finally {
+ scope.stop()
+ }
+}
+
+function benchBinding(
+ name: string,
+ create: () => any,
+ oldPath: Setup,
+ bindingPath: Setup,
+): void {
+ describe(name, () => {
+ describe('update', () => {
+ bench('renderEffect + setter', async () => {
+ await update(create, oldPath)
+ })
+
+ bench('binding helper', async () => {
+ await update(create, bindingPath)
+ })
+ })
+
+ describe('init', () => {
+ bench('renderEffect + setter', () => {
+ init(create, oldPath)
+ })
+
+ bench('binding helper', () => {
+ init(create, bindingPath)
+ })
+ })
+ })
+}
+
+function initText(oldPath: boolean): void {
+ const scope = effectScope()
+ scope.run(() => {
+ const parent = createTextParent()
+ const source = ref(0)
+ if (oldPath) {
+ const text = txt(parent) as TextNodeWithCache
+ renderEffect(() => setText(text, toDisplayString(source.value)))
+ } else {
+ setTextBinding(parent, () => toDisplayString(source.value))
+ }
+ })
+ scope.stop()
+}
+
+async function updateText(oldPath: boolean): Promise {
+ const scope = effectScope()
+ const source = ref(0)
+ try {
+ scope.run(() => {
+ const parent = createTextParent()
+ if (oldPath) {
+ const text = txt(parent) as TextNodeWithCache
+ renderEffect(() => setText(text, toDisplayString(source.value)))
+ } else {
+ setTextBinding(parent, () => toDisplayString(source.value))
+ }
+ })
+ for (let i = 1; i <= TEXT_UPDATE_BATCH; i++) {
+ source.value = i
+ await nextTick()
+ }
+ } finally {
+ scope.stop()
+ }
+}
+
+async function updateManyText(oldPath: boolean): Promise {
+ const scope = effectScope()
+ const source = ref(0)
+ try {
+ scope.run(() => {
+ for (let i = 0; i < MANY_BINDINGS; i++) {
+ const parent = createTextParent()
+ if (oldPath) {
+ const text = txt(parent) as TextNodeWithCache
+ renderEffect(() => setText(text, toDisplayString(source.value + i)))
+ } else {
+ setTextBinding(parent, () => toDisplayString(source.value + i))
+ }
+ }
+ })
+ for (let i = 1; i <= UPDATE_BATCH; i++) {
+ source.value = i
+ await nextTick()
+ }
+ } finally {
+ scope.stop()
+ }
+}
+
+describe('DOM binding effects', () => {
+ describe('setText', () => {
+ describe('update', () => {
+ bench('txt + renderEffect + setText', async () => {
+ await updateText(true)
+ })
+
+ bench('setTextBinding', async () => {
+ await updateText(false)
+ })
+ })
+
+ describe('update many bindings', () => {
+ bench('txt + renderEffect + setText', async () => {
+ await updateManyText(true)
+ })
+
+ bench('setTextBinding', async () => {
+ await updateManyText(false)
+ })
+ })
+
+ describe('init', () => {
+ bench('txt + renderEffect + setText', () => {
+ initText(true)
+ })
+
+ bench('setTextBinding', () => {
+ initText(false)
+ })
+ })
+ })
+
+ benchBinding(
+ 'setProp',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setProp(el, 'id', source.value))
+ },
+ (el, source) => {
+ setPropBinding(el, 'id', () => source.value)
+ },
+ )
+
+ benchBinding(
+ 'setAttr',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setAttr(el, 'data-id', source.value))
+ },
+ (el, source) => {
+ setAttrBinding(el, 'data-id', () => source.value)
+ },
+ )
+
+ benchBinding(
+ 'setDOMProp',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setDOMProp(el, 'title', source.value))
+ },
+ (el, source) => {
+ setDOMPropBinding(el, 'title', () => source.value)
+ },
+ )
+
+ benchBinding(
+ 'setValue',
+ createInput,
+ (el, source) => {
+ renderEffect(() => setValue(el, source.value))
+ },
+ (el, source) => {
+ setValueBinding(el, () => source.value)
+ },
+ )
+
+ benchBinding(
+ 'setClass',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setClass(el, source.value & 1 ? 'one' : 'two'))
+ },
+ (el, source) => {
+ setClassBinding(el, () => (source.value & 1 ? 'one' : 'two'))
+ },
+ )
+
+ benchBinding(
+ 'setClassName',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setClassName(el, source.value & 1 ? 1 : 0, 'one'))
+ },
+ (el, source) => {
+ setClassNameBinding(el, () => (source.value & 1 ? 1 : 0), 'one')
+ },
+ )
+
+ benchBinding(
+ 'setStyle',
+ createDiv,
+ (el, source) => {
+ renderEffect(() =>
+ setStyle(el, { color: source.value & 1 ? 'red' : 'blue' }),
+ )
+ },
+ (el, source) => {
+ setStyleBinding(el, () => ({
+ color: source.value & 1 ? 'red' : 'blue',
+ }))
+ },
+ )
+
+ benchBinding(
+ 'setHtml',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setHtml(el, `${source.value} `))
+ },
+ (el, source) => {
+ setHtmlBinding(el, () => `${source.value} `)
+ },
+ )
+
+ benchBinding(
+ 'setBlockText',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setBlockText(el, source.value))
+ },
+ (el, source) => {
+ setBlockTextBinding(el, () => source.value)
+ },
+ )
+
+ benchBinding(
+ 'setBlockHtml',
+ createDiv,
+ (el, source) => {
+ renderEffect(() => setBlockHtml(el, `${source.value} `))
+ },
+ (el, source) => {
+ setBlockHtmlBinding(el, () => `${source.value} `)
+ },
+ )
+
+ benchBinding(
+ 'setDynamicProps',
+ createDiv,
+ (el, source) => {
+ renderEffect(() =>
+ setDynamicProps(el, [
+ { id: `id-${source.value}`, class: source.value & 1 ? 'one' : 'two' },
+ ]),
+ )
+ },
+ (el, source) => {
+ setDynamicPropsBinding(el, () => [
+ { id: `id-${source.value}`, class: source.value & 1 ? 'one' : 'two' },
+ ])
+ },
+ )
+
+ describe('setMergedDynamicProps', () => {
+ const arrayGetterPath: Setup = (el, source) => {
+ setDynamicPropsBinding(el, () => [
+ { id: 'static-id' },
+ { title: `title-${source.value}` },
+ { class: 'static-class' },
+ ])
+ }
+ const mergedSourcesPath: Setup = (el, source) => {
+ setMergedDynamicPropsBinding(
+ el,
+ { id: 'static-id' },
+ () => ({ title: `title-${source.value}` }),
+ { class: 'static-class' },
+ )
+ }
+
+ describe('update', () => {
+ bench('array getter helper', async () => {
+ await update(createDiv, arrayGetterPath)
+ })
+
+ bench('merged sources helper', async () => {
+ await update(createDiv, mergedSourcesPath)
+ })
+ })
+
+ describe('init', () => {
+ bench('array getter helper', () => {
+ init(createDiv, arrayGetterPath)
+ })
+
+ bench('merged sources helper', () => {
+ init(createDiv, mergedSourcesPath)
+ })
+ })
+ })
+
+ benchBinding(
+ 'setEvent',
+ createDiv,
+ (el, source) => {
+ renderEffect(() =>
+ onBinding(el, source.value & 1 ? 'click' : 'mouseover', noop),
+ )
+ },
+ (el, source) => {
+ setEventBinding(
+ el,
+ () => (source.value & 1 ? 'click' : 'mouseover'),
+ noop,
+ )
+ },
+ )
+
+ benchBinding(
+ 'setDynamicEvents',
+ createDiv,
+ (el, source) => {
+ renderEffect(() =>
+ setDynamicEvents(el, {
+ [source.value & 1 ? 'click' : 'mouseover']: noop,
+ }),
+ )
+ },
+ (el, source) => {
+ setDynamicEventsBinding(el, () => ({
+ [source.value & 1 ? 'click' : 'mouseover']: noop,
+ }))
+ },
+ )
+})
diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
index f15bae72ba4..6f8b68da110 100644
--- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
@@ -14,7 +14,9 @@ import {
prepend,
remove,
renderEffect,
+ setDynamicEventsBinding,
setInsertionState,
+ setMergedDynamicPropsBinding,
template,
txt,
vaporInteropPlugin,
@@ -34,7 +36,7 @@ import {
toDisplayString,
useSlots,
} from '@vue/runtime-dom'
-import { makeRender } from './_utils'
+import { compileToVaporRender, makeRender } from './_utils'
import type { DynamicSlot } from '../src/componentSlots'
import { setElementText, setText } from '../src/dom/prop'
import { type Block, type BlockFn, isValidBlock } from '../src/block'
@@ -1799,18 +1801,77 @@ describe('component: slots', () => {
const n3 = template('
')() as any
const x3 = txt(n3) as any
renderEffect(() => setText(x3, toDisplayString(count.value)))
+ setMergedDynamicPropsBinding(
+ n3,
+ { id: 'static-id' },
+ () => ({
+ title: `title-${count.value}`,
+ class: `count-${count.value}`,
+ }),
+ { class: 'after' },
+ )
return n3
},
})
},
}).render()
- expect(html()).toBe('0
')
+ expect(html()).toBe(
+ '0
',
+ )
// expect no changes due to v-once
count.value++
await nextTick()
- expect(html()).toBe('0
')
+ expect(html()).toBe(
+ '0
',
+ )
+ })
+
+ test('v-on in v-once slot should not warn or update events', async () => {
+ const Child = defineVaporComponent({
+ render: compileToVaporRender(` `, {
+ bindingMetadata: {},
+ }),
+ })
+
+ const calls: string[] = []
+ const events = ref void>>({
+ click: () => {
+ calls.push('click')
+ },
+ })
+ let button!: HTMLButtonElement
+
+ define({
+ setup() {
+ return createComponent(Child, null, {
+ default: () => {
+ button = template(' ')() as HTMLButtonElement
+ setDynamicEventsBinding(button, () => events.value)
+ return button
+ },
+ })
+ },
+ }).render()
+
+ expect(
+ `onEffectCleanup() was called when there was no active effect`,
+ ).not.toHaveBeenWarned()
+
+ button.click()
+ expect(calls).toEqual(['click'])
+
+ events.value = {
+ mouseover: () => {
+ calls.push('mouseover')
+ },
+ }
+ await nextTick()
+
+ button.dispatchEvent(new Event('mouseover'))
+ button.click()
+ expect(calls).toEqual(['click', 'click'])
})
})
diff --git a/packages/runtime-vapor/__tests__/renderEffect.spec.ts b/packages/runtime-vapor/__tests__/renderEffect.spec.ts
index 68ea0c125f3..b05fbadd869 100644
--- a/packages/runtime-vapor/__tests__/renderEffect.spec.ts
+++ b/packages/runtime-vapor/__tests__/renderEffect.spec.ts
@@ -11,7 +11,25 @@ import {
watchPostEffect,
watchSyncEffect,
} from '@vue/runtime-dom'
-import { renderEffect, template } from '../src'
+import {
+ renderEffect,
+ setAttrBinding,
+ setBlockHtmlBinding,
+ setBlockTextBinding,
+ setClassBinding,
+ setClassNameBinding,
+ setDOMPropBinding,
+ setDynamicEventsBinding,
+ setDynamicPropsBinding,
+ setEventBinding,
+ setHtmlBinding,
+ setMergedDynamicPropsBinding,
+ setPropBinding,
+ setStyleBinding,
+ setTextBinding,
+ setValueBinding,
+ template,
+} from '../src'
import { RenderEffect } from '../src/renderEffect'
import { onEffectCleanup } from '@vue/reactivity'
import { makeRender } from './_utils'
@@ -137,6 +155,311 @@ describe('renderEffect', () => {
expect(dummy).toBe(3)
})
+ test('setTextBinding updates text with render lifecycle', async () => {
+ const calls: string[] = []
+ const { instance, html } = define({
+ setup() {
+ const source = ref('one')
+ const update = () => (source.value = 'two')
+ onBeforeUpdate(() => calls.push(`beforeUpdate ${source.value}`))
+ onUpdated(() => calls.push(`updated ${source.value}`))
+ return { source, update }
+ },
+ render(ctx: any) {
+ const t0 = template('
', 1)
+ const n0 = t0() as ParentNode
+ setTextBinding(n0, () => ctx.source)
+ return n0
+ },
+ }).render()
+
+ expect(html()).toBe('one
')
+ expect(calls).toEqual([])
+
+ const { update } = instance?.setupState as any
+ update()
+ await nextTick()
+
+ expect(html()).toBe('two
')
+ expect(calls).toEqual(['beforeUpdate two', 'updated two'])
+ })
+
+ test('setTextBinding getter runs with current instance and scope', async () => {
+ const source = ref('one')
+ const scope = new EffectScope()
+ let instanceSnap: GenericComponentInstance | null = null
+ let scopeSnap: EffectScope | undefined = undefined
+ const { instance, html } = define(() => {
+ const t0 = template('
', 1)
+ const n0 = t0() as ParentNode
+ scope.run(() => {
+ setTextBinding(n0, () => {
+ instanceSnap = currentInstance
+ scopeSnap = getCurrentScope()
+ return source.value
+ })
+ })
+ return n0
+ }).render()
+
+ expect(html()).toBe('one
')
+ expect(instanceSnap).toBe(instance)
+ expect(scopeSnap).toBe(scope)
+
+ source.value = 'two'
+ await nextTick()
+ expect(html()).toBe('two
')
+ expect(instanceSnap).toBe(instance)
+ expect(scopeSnap).toBe(scope)
+ scope.stop()
+ })
+
+ test('DOM binding helpers update with their source values', async () => {
+ let input!: HTMLInputElement
+ let eventTarget!: HTMLButtonElement
+ let dynamicEventTarget!: HTMLButtonElement
+ const eventCalls: string[] = []
+ const { instance, html } = define({
+ setup() {
+ const source = ref('one')
+ const active = ref(true)
+ const color = ref('red')
+ const eventName = ref('click')
+ const events = ref void>>({
+ click: () => eventCalls.push(`dynamic ${source.value}`),
+ })
+ const update = () => {
+ source.value = 'two'
+ active.value = false
+ color.value = 'blue'
+ eventName.value = 'mouseover'
+ events.value = {
+ mouseover: () => eventCalls.push(`dynamic ${source.value}`),
+ }
+ }
+ return { source, active, color, eventName, events, update }
+ },
+ render(ctx: any) {
+ const root = document.createElement('div')
+ const attr = document.createElement('div')
+ const prop = document.createElement('div')
+ const domProp = document.createElement('div')
+ input = document.createElement('input')
+ const cls = document.createElement('div')
+ const clsName = document.createElement('div')
+ const style = document.createElement('div')
+ const html = document.createElement('div')
+ const blockText = document.createElement('div')
+ const blockHtml = document.createElement('div')
+ const dynamic = document.createElement('div')
+ const mergedDynamic = document.createElement('div')
+ eventTarget = document.createElement('button')
+ dynamicEventTarget = document.createElement('button')
+
+ root.append(
+ attr,
+ prop,
+ domProp,
+ input,
+ cls,
+ clsName,
+ style,
+ html,
+ blockText,
+ blockHtml,
+ dynamic,
+ mergedDynamic,
+ eventTarget,
+ dynamicEventTarget,
+ )
+
+ setAttrBinding(attr, 'data-test', () => ctx.source)
+ setPropBinding(prop, 'id', () => ctx.source)
+ setDOMPropBinding(domProp, 'title', () => ctx.source)
+ setValueBinding(input, () => ctx.source)
+ setClassBinding(cls, () => ctx.source)
+ setClassNameBinding(clsName, () => (ctx.active ? 1 : 0), 'active')
+ setStyleBinding(style, () => ({ color: ctx.color }))
+ setHtmlBinding(html, () => `${ctx.source} `)
+ setBlockTextBinding(blockText, () => ctx.source)
+ setBlockHtmlBinding(blockHtml, () => `${ctx.source} `)
+ setDynamicPropsBinding(dynamic, () => [
+ { id: ctx.source, class: ctx.source },
+ ])
+ setMergedDynamicPropsBinding(
+ mergedDynamic,
+ { id: 'static-id' },
+ () => ({ title: ctx.source, class: ctx.source }),
+ { class: 'static-class' },
+ )
+ setEventBinding(
+ eventTarget,
+ () => ctx.eventName,
+ () => eventCalls.push(`event ${ctx.source}`),
+ )
+ setDynamicEventsBinding(dynamicEventTarget, () => ctx.events)
+
+ return root
+ },
+ }).render()
+
+ expect(html()).toBe(
+ '',
+ )
+ expect(input.value).toBe('one')
+ eventTarget.dispatchEvent(new Event('click'))
+ dynamicEventTarget.dispatchEvent(new Event('click'))
+ expect(eventCalls).toEqual(['event one', 'dynamic one'])
+
+ const { update } = instance?.setupState as any
+ update()
+ await nextTick()
+
+ expect(html()).toBe(
+ '',
+ )
+ expect(input.value).toBe('two')
+ eventTarget.dispatchEvent(new Event('click'))
+ dynamicEventTarget.dispatchEvent(new Event('click'))
+ eventTarget.dispatchEvent(new Event('mouseover'))
+ dynamicEventTarget.dispatchEvent(new Event('mouseover'))
+ expect(eventCalls).toEqual([
+ 'event one',
+ 'dynamic one',
+ 'event two',
+ 'dynamic two',
+ ])
+ })
+
+ test('setMergedDynamicPropsBinding handles nullish source updates', async () => {
+ let el!: HTMLElement
+ const { instance } = define({
+ setup() {
+ const mode = ref<'value' | 'null' | 'undefined'>('value')
+ const setNull = () => {
+ mode.value = 'null'
+ }
+ const setValue = () => {
+ mode.value = 'value'
+ }
+ const setUndefined = () => {
+ mode.value = 'undefined'
+ }
+ return { mode, setNull, setValue, setUndefined }
+ },
+ render(ctx: any) {
+ el = document.createElement('div')
+ setMergedDynamicPropsBinding(
+ el,
+ { id: 'static-id', class: 'before', style: { color: 'red' } },
+ () =>
+ ctx.mode === 'value'
+ ? {
+ title: 'live',
+ 'data-dyn': 'yes',
+ class: 'dynamic',
+ style: { backgroundColor: 'blue' },
+ }
+ : ctx.mode === 'null'
+ ? null
+ : undefined,
+ { class: 'after', style: { fontSize: '12px' } },
+ )
+ return el
+ },
+ }).render()
+
+ expect(el.id).toBe('static-id')
+ expect(el.title).toBe('live')
+ expect(el.dataset.dyn).toBe('yes')
+ expect(el.className).toBe('before dynamic after')
+ expect(el.style.color).toBe('red')
+ expect(el.style.backgroundColor).toBe('blue')
+ expect(el.style.fontSize).toBe('12px')
+
+ const { setNull, setValue, setUndefined } = instance?.setupState as any
+ setNull()
+ await nextTick()
+
+ expect(el.id).toBe('static-id')
+ expect(el.title).toBe('')
+ expect(el.dataset.dyn).toBe(undefined)
+ expect(el.className).toBe('before after')
+ expect(el.style.color).toBe('red')
+ expect(el.style.backgroundColor).toBe('')
+ expect(el.style.fontSize).toBe('12px')
+
+ setValue()
+ await nextTick()
+
+ expect(el.title).toBe('live')
+ expect(el.dataset.dyn).toBe('yes')
+ expect(el.className).toBe('before dynamic after')
+ expect(el.style.backgroundColor).toBe('blue')
+
+ setUndefined()
+ await nextTick()
+
+ expect(el.id).toBe('static-id')
+ expect(el.title).toBe('')
+ expect(el.dataset.dyn).toBe(undefined)
+ expect(el.className).toBe('before after')
+ expect(el.style.color).toBe('red')
+ expect(el.style.backgroundColor).toBe('')
+ expect(el.style.fontSize).toBe('12px')
+ })
+
+ test('setEventBinding preserves listener options across event name updates', async () => {
+ let button!: HTMLButtonElement
+ const calls: string[] = []
+ const { instance } = define({
+ setup() {
+ const eventName = ref('click')
+ const update = () => {
+ eventName.value = 'mouseover'
+ }
+ return { eventName, update }
+ },
+ render(ctx: any) {
+ button = document.createElement('button')
+ setEventBinding(
+ button,
+ () => ctx.eventName,
+ () => calls.push(ctx.eventName),
+ { once: true },
+ )
+ return button
+ },
+ }).render()
+
+ const { update } = instance?.setupState as any
+ update()
+ await nextTick()
+
+ button.dispatchEvent(new Event('click'))
+ button.dispatchEvent(new Event('mouseover'))
+ button.dispatchEvent(new Event('mouseover'))
+ expect(calls).toEqual(['mouseover'])
+ })
+
+ test('setEventBinding does not mutate listener options', () => {
+ const options = { once: true }
+ const button = document.createElement('button')
+ const scope = new EffectScope()
+
+ scope.run(() => {
+ setEventBinding(
+ button,
+ () => 'click',
+ () => {},
+ options,
+ )
+ })
+ scope.stop()
+
+ expect(options).toEqual({ once: true })
+ })
+
test('should run with the scheduling order', async () => {
const calls: string[] = []
diff --git a/packages/runtime-vapor/src/dom/bindingEffect.ts b/packages/runtime-vapor/src/dom/bindingEffect.ts
new file mode 100644
index 00000000000..416d50dc9f2
--- /dev/null
+++ b/packages/runtime-vapor/src/dom/bindingEffect.ts
@@ -0,0 +1,152 @@
+import { inOnceSlot } from '../componentSlots'
+import { renderEffect } from '../renderEffect'
+import { on, onBinding, setDynamicEvents } from './event'
+import { txt } from './node'
+import {
+ setAttr,
+ setBlockHtml,
+ setBlockText,
+ setClass,
+ setClassName,
+ setDOMProp,
+ setDynamicProps,
+ setHtml,
+ setProp,
+ setStyle,
+ setText,
+ setValue,
+} from './prop'
+
+type TextNodeWithCache = Text & { $txt?: string }
+type MergedDynamicPropsSource = Record | null | undefined
+type DynamicPropsGetter = () => MergedDynamicPropsSource
+
+export function setTextBinding(parent: ParentNode, getter: () => string): void {
+ const text = txt(parent) as TextNodeWithCache
+ renderEffect(() => setText(text, getter()))
+}
+
+export function setHtmlBinding(el: any, getter: () => any): void {
+ renderEffect(() => setHtml(el, getter()))
+}
+
+export function setBlockHtmlBinding(block: any, getter: () => any): void {
+ renderEffect(() => setBlockHtml(block, getter()))
+}
+
+export function setBlockTextBinding(block: any, getter: () => any): void {
+ renderEffect(() => setBlockText(block, getter()))
+}
+
+export function setClassBinding(
+ el: any,
+ getter: () => any,
+ isSVG: boolean = false,
+): void {
+ renderEffect(() => setClass(el, getter(), isSVG))
+}
+
+export function setClassNameBinding(
+ el: any,
+ getter: () => number,
+ cls: string | string[],
+ prefix: string = '',
+ suffix: string = '',
+): void {
+ renderEffect(() => setClassName(el, getter(), cls, prefix, suffix))
+}
+
+export function setStyleBinding(el: any, getter: () => any): void {
+ renderEffect(() => setStyle(el, getter()))
+}
+
+export function setValueBinding(el: any, getter: () => any): void {
+ renderEffect(() => setValue(el, getter()))
+}
+
+export function setAttrBinding(
+ el: any,
+ key: string,
+ getter: () => any,
+ isSVG: boolean = false,
+): void {
+ renderEffect(() => setAttr(el, key, getter(), isSVG))
+}
+
+export function setPropBinding(el: any, key: string, getter: () => any): void {
+ renderEffect(() => setProp(el, key, getter()))
+}
+
+export function setDOMPropBinding(
+ el: any,
+ key: string,
+ getter: () => any,
+): void {
+ renderEffect(() => setDOMProp(el, key, getter()))
+}
+
+export function setDynamicPropsBinding(
+ el: any,
+ getter: () => any[],
+ isSVG: boolean = false,
+): void {
+ renderEffect(() => setDynamicProps(el, getter(), isSVG))
+}
+
+export function setMergedDynamicPropsBinding(
+ el: any,
+ before: MergedDynamicPropsSource,
+ getter: DynamicPropsGetter,
+ after?: MergedDynamicPropsSource,
+ isSVG?: boolean,
+): void {
+ const values = createMergedDynamicPropsValues(before, undefined, after)
+ const index = before != null ? 1 : 0
+ renderEffect(() => {
+ values[index] = getter()
+ setDynamicProps(el, values, isSVG === true)
+ })
+}
+
+function createMergedDynamicPropsValues(
+ before: MergedDynamicPropsSource,
+ value: MergedDynamicPropsSource,
+ after: MergedDynamicPropsSource,
+): any[] {
+ return before != null
+ ? after != null
+ ? [before, value, after]
+ : [before, value]
+ : after != null
+ ? [value, after]
+ : [value]
+}
+
+export function setEventBinding(
+ el: Element,
+ getter: () => string,
+ handler: (e: Event) => any | ((e: Event) => any)[],
+ options?: AddEventListenerOptions,
+): void {
+ if (inOnceSlot) {
+ on(el, getter(), handler, options)
+ return
+ }
+
+ renderEffect(() => onBinding(el, getter(), handler, options))
+}
+
+export function setDynamicEventsBinding(
+ el: HTMLElement,
+ getter: () => Record any>,
+): void {
+ if (inOnceSlot) {
+ const events = getter()
+ for (const name in events) {
+ on(el, name, events[name])
+ }
+ return
+ }
+
+ renderEffect(() => setDynamicEvents(el, getter()))
+}
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
index 52bbda96129..d10b3345640 100644
--- a/packages/runtime-vapor/src/index.ts
+++ b/packages/runtime-vapor/src/index.ts
@@ -34,6 +34,23 @@ export { renderEffect } from './renderEffect'
export { createSlot, withVaporCtx } from './componentSlots'
export { template } from './dom/template'
export { createTextNode, child, nthChild, next, txt } from './dom/node'
+export {
+ setAttrBinding,
+ setBlockHtmlBinding,
+ setBlockTextBinding,
+ setClassBinding,
+ setClassNameBinding,
+ setDOMPropBinding,
+ setDynamicEventsBinding,
+ setDynamicPropsBinding,
+ setEventBinding,
+ setHtmlBinding,
+ setMergedDynamicPropsBinding,
+ setPropBinding,
+ setStyleBinding,
+ setTextBinding,
+ setValueBinding,
+} from './dom/bindingEffect'
export {
setText,
setBlockText,