- [ ] `v-model`
- [ ] `v-if` / `v-else` / `v-else-if`
- [ ] `v-for`
- - [ ] `v-once`
+ - [x] `v-once`
- [x] `v-html`
- [x] `v-text`
- [ ] `v-show`
"import { defineComponent as _defineComponent } from 'vue'
import { watchEffect } from 'vue'
import { template, insert, setText, on, setHtml } from 'vue/vapor'
-const t0 = template(\`<h1 id=\\"title\\">Counter</h1><p>Count: </p><p>Double: </p><button>Increment</button><div></div><input type=\\"text\\">\`)
+const t0 = template(\`<h1 id=\\"title\\">Counter</h1><p>Count: </p><p>Double: </p><button>Increment</button><div></div><input type=\\"text\\"><p>once: </p>\`)
import { ref, computed } from 'vue'
const html = '<b>HTML</b>'
insert(n1, n0)
const n3 = document.createTextNode(double.value)
insert(n3, n2)
+const n7 = document.createTextNode(count.value)
+insert(n7, n6)
+setText(n7, undefined, count.value)
watchEffect(() => {
setText(n1, undefined, count.value)
})
<button @click="increment">Increment</button>
<div v-html="html" />
<input type="text" />
+ <p v-once>once: {{ count }}</p>
</template>
import type { CodegenOptions, CodegenResult } from '@vue/compiler-dom'
-import { type DynamicChildren, type RootIRNode, IRNodeTypes } from './ir'
+import {
+ type DynamicChildren,
+ type RootIRNode,
+ IRNodeTypes,
+ OperationNode,
+} from './ir'
// remove when stable
function checkNever(x: never): void {}
}
for (const operation of ir.operation) {
- switch (operation.type) {
- case IRNodeTypes.TEXT_NODE: {
- // TODO handle by runtime: document.createTextNode
- code += `const n${operation.id} = document.createTextNode(${operation.content})\n`
- break
- }
-
- case IRNodeTypes.INSERT_NODE:
- {
- let anchor = ''
- if (typeof operation.anchor === 'number') {
- anchor = `, n${operation.anchor}`
- } else if (operation.anchor === 'first') {
- anchor = `, 0 /* InsertPosition.FIRST */`
- }
- code += `insert(n${operation.element}, n${operation.parent}${anchor})\n`
- vaporHelpers.add('insert')
- }
- break
- }
+ code += genOperation(operation)
}
- for (const [expr, effects] of Object.entries(ir.effect)) {
+ for (const [_expr, operations] of Object.entries(ir.effect)) {
// TODO don't use watchEffect from vue/core, implement `effect` function in runtime-vapor package
let scope = `watchEffect(() => {\n`
helpers.add('watchEffect')
- for (const effect of effects) {
- switch (effect.type) {
- case IRNodeTypes.SET_PROP: {
- scope += `setAttr(n${effect.element}, ${JSON.stringify(
- effect.name,
- )}, undefined, ${expr})\n`
- vaporHelpers.add('setAttr')
- break
- }
- case IRNodeTypes.SET_TEXT: {
- scope += `setText(n${effect.element}, undefined, ${expr})\n`
- vaporHelpers.add('setText')
- break
- }
- case IRNodeTypes.SET_EVENT: {
- scope += `on(n${effect.element}, ${JSON.stringify(
- effect.name,
- )}, ${expr})\n`
- vaporHelpers.add('on')
- break
- }
- case IRNodeTypes.SET_HTML: {
- scope += `setHtml(n${effect.element}, undefined, ${expr})\n`
- vaporHelpers.add('setHtml')
- break
- }
- default:
- checkNever(effect)
- }
+ for (const operation of operations) {
+ scope += genOperation(operation)
}
scope += '})\n'
code += scope
ast: ir as any,
preamble,
}
+
+ function genOperation(operation: OperationNode) {
+ let code = ''
+
+ switch (operation.type) {
+ case IRNodeTypes.SET_PROP: {
+ code = `setAttr(n${operation.element}, ${JSON.stringify(
+ operation.name,
+ )}, undefined, ${operation.value})\n`
+ vaporHelpers.add('setAttr')
+ break
+ }
+
+ case IRNodeTypes.SET_TEXT: {
+ code = `setText(n${operation.element}, undefined, ${operation.value})\n`
+ vaporHelpers.add('setText')
+ break
+ }
+
+ case IRNodeTypes.SET_EVENT: {
+ code = `on(n${operation.element}, ${JSON.stringify(operation.name)}, ${
+ operation.value
+ })\n`
+ vaporHelpers.add('on')
+ break
+ }
+
+ case IRNodeTypes.SET_HTML: {
+ code = `setHtml(n${operation.element}, undefined, ${operation.value})\n`
+ vaporHelpers.add('setHtml')
+ break
+ }
+
+ case IRNodeTypes.TEXT_NODE: {
+ // TODO handle by runtime: document.createTextNode
+ code = `const n${operation.id} = document.createTextNode(${operation.value})\n`
+ break
+ }
+
+ case IRNodeTypes.INSERT_NODE: {
+ let anchor = ''
+ if (typeof operation.anchor === 'number') {
+ anchor = `, n${operation.anchor}`
+ } else if (operation.anchor === 'first') {
+ anchor = `, 0 /* InsertPosition.FIRST */`
+ }
+ code = `insert(n${operation.element}, n${operation.parent}${anchor})\n`
+ vaporHelpers.add('insert')
+ break
+ }
+
+ default:
+ checkNever(operation)
+ }
+
+ return code
+ }
}
function genChildren(children: DynamicChildren) {
type: IRNodeTypes.ROOT
template: Array<TemplateGeneratorIRNode>
children: DynamicChildren
- effect: Record<string, EffectNode[]>
+ // TODO multi-expression effect
+ effect: Record<string /* expr */, OperationNode[]>
operation: OperationNode[]
helpers: Set<string>
vaporHelpers: Set<string>
type: IRNodeTypes.SET_PROP
element: number
name: string
+ value: string
}
export interface SetTextIRNode extends IRNode {
type: IRNodeTypes.SET_TEXT
element: number
+ value: string
}
export interface SetEventIRNode extends IRNode {
type: IRNodeTypes.SET_EVENT
element: number
name: string
+ value: string
}
export interface SetHtmlIRNode extends IRNode {
type: IRNodeTypes.SET_HTML
element: number
+ value: string
}
-export type EffectNode =
- | SetPropIRNode
- | SetTextIRNode
- | SetEventIRNode
- | SetHtmlIRNode
-
export interface TextNodeIRNode extends IRNode {
type: IRNodeTypes.TEXT_NODE
id: number
- content: string
+ value: string
}
export interface InsertNodeIRNode extends IRNode {
anchor: number | 'first' | 'last'
}
-export type OperationNode = TextNodeIRNode | InsertNodeIRNode
+export type OperationNode =
+ | SetPropIRNode
+ | SetTextIRNode
+ | SetEventIRNode
+ | SetHtmlIRNode
+ | TextNodeIRNode
+ | InsertNodeIRNode
export interface DynamicChild {
id: number | null
InterpolationNode,
TransformOptions,
DirectiveNode,
+ ExpressionNode,
} from '@vue/compiler-dom'
import {
type DynamicChildren,
- type EffectNode,
type OperationNode,
type RootIRNode,
IRNodeTypes,
children: DynamicChildren
store: boolean
ghost: boolean
+ once: boolean
getElementId(): number
registerTemplate(): number
- registerEffect(expr: string, effectNode: EffectNode): void
+ registerEffect(expr: string, operation: OperationNode): void
registerOpration(...oprations: OperationNode[]): void
helper(name: string): string
}
children: {},
store: false,
ghost: false,
+ once: false,
getElementId: () => i++,
- registerEffect(expr, effectNode) {
+ registerEffect(expr, operation) {
if (!effect[expr]) effect[expr] = []
- effect[expr].push(effectNode)
+ effect[expr].push(operation)
},
template: '',
children,
store: false,
+ registerEffect(expr, operation) {
+ if (ctx.once) {
+ return ctx.registerOpration(operation)
+ }
+ parent.registerEffect(expr, operation)
+ },
}
return ctx
}
const { node } = ctx
if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) {
- const expr = processExpression(ctx, node.content.content)
+ const expr = processExpression(ctx, node.content)!
const parent = ctx.parent!
const parentId = parent.getElementId()
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: parentId,
+ value: expr,
})
} else {
let id: number
type: IRNodeTypes.TEXT_NODE,
loc: node.loc,
id,
- content: expr,
+ value: expr,
},
{
type: IRNodeTypes.INSERT_NODE,
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: id,
+ value: expr,
})
}
return
return
}
- if (!node.exp) {
- // TODO: Vue 3.4 supported shorthand syntax
- // https://github.com/vuejs/core/pull/9451
- return
- } else if (node.exp.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
- // TODO: CompoundExpressionNode: :foo="count + 1"
- return
- }
-
ctx.store = true
- const expr = processExpression(ctx, node.exp.content)
+ const expr = processExpression(ctx, node.exp)
switch (name) {
case 'bind': {
- if (!node.arg) {
+ if (expr === null) {
+ // TODO: Vue 3.4 supported shorthand syntax
+ // https://github.com/vuejs/core/pull/9451
+ return
+ } else if (!node.arg) {
// TODO support v-bind="{}"
return
} else if (
loc: node.loc,
element: ctx.getElementId(),
name: node.arg.content,
+ value: expr,
})
break
}
) {
// TODO support @[foo]="bar"
return
+ } else if (expr === null) {
+ // TODO: support @foo
+ // https://github.com/vuejs/core/pull/9451
+ return
}
ctx.registerEffect(expr, {
loc: node.loc,
element: ctx.getElementId(),
name: node.arg.content,
+ value: expr,
})
break
}
- case 'html':
- ctx.registerEffect(expr, {
+ case 'html': {
+ const value = expr || '""'
+ ctx.registerEffect(value, {
type: IRNodeTypes.SET_HTML,
loc: node.loc,
element: ctx.getElementId(),
+ value,
})
break
- case 'text':
- ctx.registerEffect(expr, {
+ }
+ case 'text': {
+ const value = expr || '""'
+ ctx.registerEffect(value, {
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: ctx.getElementId(),
+ value,
})
break
+ }
+ case 'once': {
+ ctx.once = true
+ break
+ }
}
}
// TODO: reuse packages/compiler-core/src/transforms/transformExpression.ts
-function processExpression(ctx: TransformContext, expr: string) {
- if (ctx.options.bindingMetadata?.[expr] === 'setup-ref') {
- expr += '.value'
+function processExpression(
+ ctx: TransformContext,
+ expr: ExpressionNode | undefined,
+): string | null {
+ if (!expr) return null
+ if (expr.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
+ // TODO
+ return ''
+ }
+ const { content } = expr
+ if (ctx.options.bindingMetadata?.[content] === 'setup-ref') {
+ return content + '.value'
}
- return expr
+ return content
}
<script setup lang="ts">
import { ref, computed } from 'vue'
-const count = ref(0)
+const count = ref(1)
const double = computed(() => count.value * 2)
const html = computed(() => `<button>HTML! ${count.value}</button>`)
</div>
<div v-html="html" />
<div v-text="html" />
+ <div v-once>once: {{ count }}</div>
</div>
</template>