diff --git a/README.md b/README.md index 3f7569ed3..93bfdc129 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ See the To-do list below or `// TODO` comments in code (`compiler-vapor` and `ru - [ ] `v-if` / `v-else` / `v-else-if` - [ ] `v-for` - [ ] `v-once` - - [ ] `v-html` + - [x] `v-html` - [ ] `v-text` - [ ] `v-show` - [ ] `v-pre` diff --git a/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap index efb1dbb7b..90e3c5a8c 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/basic.test.ts.snap @@ -3,10 +3,11 @@ exports[`basic 1`] = ` "import { defineComponent as _defineComponent } from 'vue' import { watchEffect } from 'vue' -import { template, insert, setText, on } from 'vue/vapor' -const t0 = template(\`

Counter

Count:

Double:

\`) +import { template, insert, setText, on, setHtml } from 'vue/vapor' +const t0 = template(\`

Counter

Count:

Double:

\`) import { ref, computed } from 'vue' +const html = 'HTML' export default /*#__PURE__*/_defineComponent({ setup(__props) { @@ -16,6 +17,7 @@ const double = computed(() => count.value * 2) const increment = () => count.value++ + return (() => { const root = t0() const n1 = document.createTextNode(count.value) @@ -31,6 +33,9 @@ setText(n3, undefined, double.value) watchEffect(() => { on(n4, \\"click\\", increment) }) +watchEffect(() => { +setHtml(n5, undefined, html) +}) return root })(); } diff --git a/packages/compiler-vapor/__tests__/fixtures/counter.vue b/packages/compiler-vapor/__tests__/fixtures/counter.vue index 9415168d7..9ef7a9784 100644 --- a/packages/compiler-vapor/__tests__/fixtures/counter.vue +++ b/packages/compiler-vapor/__tests__/fixtures/counter.vue @@ -5,6 +5,8 @@ const count = ref(0) const double = computed(() => count.value * 2) const increment = () => count.value++ + +const html = 'HTML' diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 0a64b732d..c24040f72 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -1,6 +1,9 @@ import type { CodegenOptions, CodegenResult } from '@vue/compiler-dom' import { type DynamicChildren, type RootIRNode, IRNodeTypes } from './ir' +// remove when stable +function checkNever(x: never): void {} + // IR -> JS codegen export function generate( ir: RootIRNode, @@ -74,6 +77,13 @@ export function generate( vaporHelpers.add('on') break } + case IRNodeTypes.SET_HTML: { + scope += `setHtml(n${effect.element}, undefined, ${expr})\n` + vaporHelpers.add('setHtml') + break + } + default: + checkNever(effect) } } scope += '})\n' diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index c515797b9..9e45b062b 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -6,6 +6,7 @@ export const enum IRNodeTypes { SET_PROP, SET_TEXT, SET_EVENT, + SET_HTML, INSERT_NODE, TEXT_NODE, @@ -48,6 +49,17 @@ export interface SetEventIRNode extends IRNode { name: string } +export interface SetHtmlIRNode extends IRNode { + type: IRNodeTypes.SET_HTML + element: number +} + +export type EffectNode = + | SetPropIRNode + | SetTextIRNode + | SetEventIRNode + | SetHtmlIRNode + export interface TextNodeIRNode extends IRNode { type: IRNodeTypes.TEXT_NODE id: number @@ -61,7 +73,6 @@ export interface InsertNodeIRNode extends IRNode { anchor: number | 'first' | 'last' } -export type EffectNode = SetPropIRNode | SetTextIRNode | SetEventIRNode export type OprationNode = TextNodeIRNode | InsertNodeIRNode export interface DynamicChild { diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index e2fe911a5..8436092c7 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -134,20 +134,26 @@ export function transform( vaporHelpers: new Set([]), } const ctx = createRootContext(ir, root, options) - transformChildren(ctx) - ctx.registerTemplate() + + // TODO: transform presets, see packages/compiler-core/src/transforms + transformChildren(ctx, true) ir.children = ctx.children return ir } -function transformChildren(ctx: TransformContext) { +function transformChildren( + ctx: TransformContext, + root?: boolean, +) { const { node: { children }, } = ctx let index = 0 children.forEach((child, i) => walkNode(child, i)) + if (root) ctx.registerTemplate() + function walkNode(node: TemplateChildNode, i: number) { const child = createContext(node, ctx, index) const isFirst = i === 0 @@ -298,30 +304,56 @@ function transformProp( } else if (node.exp.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) { // TODO: CompoundExpressionNode: :foo="count + 1" return - } else if (!node.arg) { - // TODO support v-bind="{}" - return - } else if (node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) { - // TODO support :[foo]="bar" - return } - const expr = processExpression(ctx, node.exp.content) ctx.store = true - if (name === 'bind') { - ctx.registerEffect(expr, { - type: IRNodeTypes.SET_PROP, - loc: node.loc, - element: ctx.getElementId(), - name: node.arg.content, - }) - } else if (name === 'on') { - ctx.registerEffect(expr, { - type: IRNodeTypes.SET_EVENT, - loc: node.loc, - element: ctx.getElementId(), - name: node.arg.content, - }) + const expr = processExpression(ctx, node.exp.content) + switch (name) { + case 'bind': { + if (!node.arg) { + // TODO support v-bind="{}" + return + } else if ( + node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION) + ) { + // TODO support :[foo]="bar" + return + } + + ctx.registerEffect(expr, { + type: IRNodeTypes.SET_PROP, + loc: node.loc, + element: ctx.getElementId(), + name: node.arg.content, + }) + break + } + case 'on': { + if (!node.arg) { + // TODO support v-on="{}" + return + } else if ( + node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION) + ) { + // TODO support @[foo]="bar" + return + } + + ctx.registerEffect(expr, { + type: IRNodeTypes.SET_EVENT, + loc: node.loc, + element: ctx.getElementId(), + name: node.arg.content, + }) + break + } + case 'html': + ctx.registerEffect(expr, { + type: IRNodeTypes.SET_HTML, + loc: node.loc, + element: ctx.getElementId(), + }) + break } } diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index aad51f3ab..b155f8b09 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -74,6 +74,12 @@ export function setText(el: Element, oldVal: any, newVal: any) { } } +export function setHtml(el: Element, oldVal: any, newVal: any) { + if (newVal !== oldVal) { + el.innerHTML = newVal + } +} + export function setClass(el: Element, oldVal: any, newVal: any) { if ((newVal = normalizeClass(newVal)) !== oldVal && (newVal || oldVal)) { el.className = newVal diff --git a/playground/src/App.vue b/playground/src/App.vue index 9497d5bdb..cb0887d99 100644 --- a/playground/src/App.vue +++ b/playground/src/App.vue @@ -3,6 +3,7 @@ import { ref, computed } from 'vue' const count = ref(0) const double = computed(() => count.value * 2) +const html = computed(() => ``) const inc = () => count.value++ const dec = () => count.value-- @@ -15,6 +16,8 @@ globalThis.double = double globalThis.inc = inc // @ts-expect-error globalThis.dec = dec +// @ts-expect-error +globalThis.html = html