--- /dev/null
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-for > basic v-for 1`] = `
+"import { template as _template, fragment as _fragment, children as _children, on as _on, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, append as _append } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div></div>")
+ const t1 = _fragment()
+ const n0 = t1()
+ const n1 = _createFor(() => (_ctx.items), (_block) => {
+ const n2 = t0()
+ const { 0: [n3],} = _children(n2)
+ _on(n3, "click", $event => (_ctx.remove(_block.s[0])))
+ const _updateEffect = () => {
+ const [item] = _block.s
+ _setText(n3, item)
+ }
+ _renderEffect(_updateEffect)
+ return [n2, _updateEffect]
+ })
+ _append(n0, n1)
+ return n0
+}"
+`;
+
+exports[`compiler: v-for > basic v-for 2`] = `
+"import { template as _template, fragment as _fragment, createFor as _createFor, append as _append } from 'vue/vapor';
+
+export function render(_ctx) {
+ const t0 = _template("<div>item</div>")
+ const t1 = _fragment()
+ const n0 = t1()
+ const n1 = _createFor(() => (_ctx.items), (_block) => {
+ const n2 = t0()
+ return [n2, () => {}]
+ })
+ _append(n0, n1)
+ return n0
+}"
+`;
--- /dev/null
+import { makeCompile } from './_utils'
+import {
+ type ForIRNode,
+ IRNodeTypes,
+ transformElement,
+ transformInterpolation,
+ transformVFor,
+ transformVOn,
+} from '../../src'
+import { NodeTypes } from '@vue/compiler-dom'
+
+const compileWithVFor = makeCompile({
+ nodeTransforms: [transformInterpolation, transformVFor, transformElement],
+ directiveTransforms: { on: transformVOn },
+})
+
+describe('compiler: v-for', () => {
+ test('basic v-for', () => {
+ const { code, ir, vaporHelpers, helpers } = compileWithVFor(
+ `<div v-for="item of items" @click="remove(item)">{{ item }}</div>`,
+ )
+
+ expect(code).matchSnapshot()
+ expect(vaporHelpers).contains('createFor')
+ expect(helpers.size).toBe(0)
+ expect(ir.template).lengthOf(2)
+ expect(ir.template).toMatchObject([
+ {
+ template: '<div></div>',
+ type: IRNodeTypes.TEMPLATE_FACTORY,
+ },
+ {
+ type: IRNodeTypes.FRAGMENT_FACTORY,
+ },
+ ])
+ expect(ir.operation).toMatchObject([
+ {
+ type: IRNodeTypes.FOR,
+ id: 1,
+ source: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'items',
+ },
+ value: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'item',
+ },
+ key: undefined,
+ index: undefined,
+ render: {
+ type: IRNodeTypes.BLOCK_FUNCTION,
+ templateIndex: 0,
+ },
+ },
+ {
+ type: IRNodeTypes.APPEND_NODE,
+ elements: [1],
+ parent: 0,
+ },
+ ])
+ expect(ir.dynamic).toMatchObject({
+ id: 0,
+ children: { 0: { id: 1 } },
+ })
+ expect(ir.effect).toEqual([])
+ expect((ir.operation[0] as ForIRNode).render.effect).lengthOf(1)
+ })
+
+ test('basic v-for', () => {
+ const { code } = compileWithVFor(`<div v-for=" of items">item</div>`)
+ expect(code).matchSnapshot()
+ })
+})
import type { HackOptions } from './ir'
import { transformVModel } from './transforms/vModel'
import { transformVIf } from './transforms/vIf'
+import { transformVFor } from './transforms/vFor'
export type CompilerOptions = HackOptions<BaseCompilerOptions>
transformRef,
transformInterpolation,
transformVIf,
+ transformVFor,
transformElement,
],
{
advancePositionWithMutation,
locStub,
} from '@vue/compiler-dom'
-import {
- IRNodeTypes,
- type OperationNode,
- type RootIRNode,
- type VaporHelper,
-} from './ir'
+import type { IREffect, RootIRNode, VaporHelper } from './ir'
import { SourceMapGenerator } from 'source-map-js'
-import { extend, isString } from '@vue/shared'
+import { extend, isString, remove } from '@vue/shared'
import type { ParserPlugin } from '@babel/parser'
-import { genSetProp } from './generators/prop'
-import { genCreateTextNode, genSetText } from './generators/text'
-import { genSetEvent } from './generators/event'
-import { genSetHtml } from './generators/html'
-import { genSetRef } from './generators/ref'
-import { genSetModelValue } from './generators/modelValue'
-import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom'
-import { genIf } from './generators/if'
import { genTemplate } from './generators/template'
import { genBlockFunctionContent } from './generators/block'
return `_${name}`
}
+ identifiers: Record<string, string[]> = Object.create(null)
+ withId = <T>(fn: () => T, map: Record<string, string | null>): T => {
+ const { identifiers } = this
+ const ids = Object.keys(map)
+
+ for (const id of ids) {
+ identifiers[id] ||= []
+ identifiers[id].unshift(map[id] || id)
+ }
+
+ const ret = fn()
+ ids.forEach(id => remove(identifiers[id], map[id] || id))
+
+ return ret
+ }
+ genEffect?: (effects: IREffect[]) => CodeFragment[]
+
constructor(ir: RootIRNode, options: CodegenOptions) {
const defaultOptions = {
mode: 'function',
const push = frag.push.bind(frag)
return [frag, push] as const
}
-
-export function genOperation(
- oper: OperationNode,
- context: CodegenContext,
-): CodeFragment[] {
- // TODO: cache old value
- switch (oper.type) {
- case IRNodeTypes.SET_PROP:
- return genSetProp(oper, context)
- case IRNodeTypes.SET_TEXT:
- return genSetText(oper, context)
- case IRNodeTypes.SET_EVENT:
- return genSetEvent(oper, context)
- case IRNodeTypes.SET_HTML:
- return genSetHtml(oper, context)
- case IRNodeTypes.SET_REF:
- return genSetRef(oper, context)
- case IRNodeTypes.SET_MODEL_VALUE:
- return genSetModelValue(oper, context)
- case IRNodeTypes.CREATE_TEXT_NODE:
- return genCreateTextNode(oper, context)
- case IRNodeTypes.INSERT_NODE:
- return genInsertNode(oper, context)
- case IRNodeTypes.PREPEND_NODE:
- return genPrependNode(oper, context)
- case IRNodeTypes.APPEND_NODE:
- return genAppendNode(oper, context)
- case IRNodeTypes.IF:
- return genIf(oper, context)
- case IRNodeTypes.WITH_DIRECTIVE:
- // generated, skip
- break
- default:
- return checkNever(oper)
- }
-
- return []
-}
-
-// remove when stable
-// @ts-expect-error
-function checkNever(x: never): never {}
type CodeFragment,
type CodegenContext,
buildCodeFragment,
- genOperation,
} from '../generate'
import { genWithDirective } from './directive'
+import { genEffects, genOperations } from './operation'
export function genBlockFunction(
oper: BlockFunctionIRNode,
context: CodegenContext,
+ args: CodeFragment[] = [],
+ returnValue?: () => CodeFragment[],
): CodeFragment[] {
const { newline, withIndent } = context
return [
- '() => {',
- ...withIndent(() => genBlockFunctionContent(oper, context)),
+ '(',
+ ...args,
+ ') => {',
+ ...withIndent(() => genBlockFunctionContent(oper, context, returnValue)),
newline(),
'}',
]
export function genBlockFunctionContent(
ir: BlockFunctionIRNode | RootIRNode,
ctx: CodegenContext,
+ returnValue?: () => CodeFragment[],
): CodeFragment[] {
- const { newline, withIndent, vaporHelper } = ctx
+ const { newline, vaporHelper } = ctx
const [frag, push] = buildCodeFragment()
push(newline(), `const n${ir.dynamic.id} = t${ir.templateIndex}()`)
push(...genWithDirective(directives, ctx))
}
- for (const operation of ir.operation) {
- push(...genOperation(operation, ctx))
- }
-
- for (const { operations } of ir.effect) {
- push(newline(), `${vaporHelper('renderEffect')}(() => {`)
- withIndent(() => {
- operations.forEach(op => push(...genOperation(op, ctx)))
- })
- push(newline(), '})')
- }
+ push(...genOperations(ir.operation, ctx))
+ push(...genEffects(ir.effect, ctx))
- push(newline(), `return n${ir.dynamic.id}`)
+ push(
+ newline(),
+ 'return ',
+ ...(returnValue ? returnValue() : [`n${ir.dynamic.id}`]),
+ )
return frag
}
const hasMultipleStatements = exp.content.includes(`;`)
if (isInlineStatement) {
- const knownIds = Object.create(null)
- knownIds['$event'] = 1
-
+ const expr = context.withId(() => genExpression(exp, context), {
+ $event: null,
+ })
return [
'$event => ',
hasMultipleStatements ? '{' : '(',
- ...genExpression(exp, context, knownIds),
+ ...expr,
hasMultipleStatements ? '}' : ')',
]
} else {
export function genExpression(
node: IRExpression,
context: CodegenContext,
- knownIds: Record<string, number> = Object.create(null),
): CodeFragment[] {
const {
options: { prefixIdentifiers },
return [[rawExpr, NewlineType.None, loc]]
}
+ // the expression is a simple identifier
if (ast === null) {
- // the expression is a simple identifier
return [genIdentifier(rawExpr, context, loc)]
}
const ids: Identifier[] = []
- walkIdentifiers(
- ast!,
- (id, parent, parentStack, isReference, isLocal) => {
- if (isLocal) return
- ids.push(id)
- },
- false,
- [],
- knownIds,
- )
+ walkIdentifiers(ast!, id => ids.push(id))
if (ids.length) {
ids.sort((a, b) => a.start! - b.start!)
const [frag, push] = buildCodeFragment()
function genIdentifier(
id: string,
- { options, vaporHelper }: CodegenContext,
+ { options, vaporHelper, identifiers }: CodegenContext,
loc?: SourceLocation,
): CodeFragment {
const { inline, bindingMetadata } = options
let name: string | undefined = id
+
+ const idMap = identifiers[id]
+ if (idMap && idMap.length) {
+ return [idMap[0], NewlineType.None, loc]
+ }
+
if (inline) {
switch (bindingMetadata[id]) {
case BindingTypes.SETUP_REF:
--- /dev/null
+import { genBlockFunction } from './block'
+import { genExpression } from './expression'
+import {
+ type CodeFragment,
+ type CodegenContext,
+ buildCodeFragment,
+} from '../generate'
+import type { ForIRNode, IREffect } from '../ir'
+import { genOperations } from './operation'
+import { NewlineType } from '@vue/compiler-dom'
+
+export function genFor(
+ oper: ForIRNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ const { newline, call, vaporHelper } = context
+ const { source, value, key, render } = oper
+
+ const rawValue = value && value.content
+ const rawKey = key && key.content
+
+ const sourceExpr = ['() => (', ...genExpression(source, context), ')']
+ let updateFn = '_updateEffect'
+ context.genEffect = genEffectInFor
+
+ const idMap: Record<string, string> = {}
+ if (rawValue) idMap[rawValue] = `_block.s[0]`
+ if (rawKey) idMap[rawKey] = `_block.s[1]`
+
+ const blockRet = (): CodeFragment[] => [
+ `[n${render.dynamic.id!}, ${updateFn}]`,
+ ]
+
+ const blockFn = context.withId(
+ () => genBlockFunction(render, context, ['_block'], blockRet),
+ idMap,
+ )
+
+ context.genEffect = undefined
+
+ return [
+ newline(),
+ `const n${oper.id} = `,
+ ...call(vaporHelper('createFor'), sourceExpr, blockFn),
+ ]
+
+ function genEffectInFor(effects: IREffect[]) {
+ if (!effects.length) {
+ updateFn = '() => {}'
+ return []
+ }
+
+ const [frag, push] = buildCodeFragment()
+
+ context.withIndent(() => {
+ if (rawValue || rawKey) {
+ push(
+ newline(),
+ 'const ',
+ '[',
+ rawValue && [rawValue, NewlineType.None, value.loc],
+ rawKey && ', ',
+ rawKey && [rawKey, NewlineType.None, key.loc],
+ '] = _block.s',
+ )
+ }
+
+ const idMap: Record<string, string | null> = {}
+ if (value) idMap[value.content] = null
+ if (key) idMap[key.content] = null
+
+ context.withId(() => {
+ effects.forEach(effect =>
+ push(...genOperations(effect.operations, context)),
+ )
+ }, idMap)
+ })
+
+ return [
+ newline(),
+ `const ${updateFn} = () => {`,
+ ...frag,
+ newline(),
+ '}',
+ newline(),
+ `${vaporHelper('renderEffect')}(${updateFn})`,
+ ]
+ }
+}
--- /dev/null
+import { type IREffect, IRNodeTypes, type OperationNode } from '../ir'
+import {
+ type CodeFragment,
+ type CodegenContext,
+ buildCodeFragment,
+} from '../generate'
+import { genAppendNode, genInsertNode, genPrependNode } from './dom'
+import { genSetEvent } from './event'
+import { genFor } from './for'
+import { genSetHtml } from './html'
+import { genIf } from './if'
+import { genSetModelValue } from './modelValue'
+import { genSetProp } from './prop'
+import { genSetRef } from './ref'
+import { genCreateTextNode, genSetText } from './text'
+
+export function genOperations(opers: OperationNode[], ctx: CodegenContext) {
+ const [frag, push] = buildCodeFragment()
+ for (const operation of opers) {
+ push(...genOperation(operation, ctx))
+ }
+ return frag
+}
+
+function genOperation(
+ oper: OperationNode,
+ context: CodegenContext,
+): CodeFragment[] {
+ switch (oper.type) {
+ case IRNodeTypes.SET_PROP:
+ return genSetProp(oper, context)
+ case IRNodeTypes.SET_TEXT:
+ return genSetText(oper, context)
+ case IRNodeTypes.SET_EVENT:
+ return genSetEvent(oper, context)
+ case IRNodeTypes.SET_HTML:
+ return genSetHtml(oper, context)
+ case IRNodeTypes.SET_REF:
+ return genSetRef(oper, context)
+ case IRNodeTypes.SET_MODEL_VALUE:
+ return genSetModelValue(oper, context)
+ case IRNodeTypes.CREATE_TEXT_NODE:
+ return genCreateTextNode(oper, context)
+ case IRNodeTypes.INSERT_NODE:
+ return genInsertNode(oper, context)
+ case IRNodeTypes.PREPEND_NODE:
+ return genPrependNode(oper, context)
+ case IRNodeTypes.APPEND_NODE:
+ return genAppendNode(oper, context)
+ case IRNodeTypes.IF:
+ return genIf(oper, context)
+ case IRNodeTypes.FOR:
+ return genFor(oper, context)
+ case IRNodeTypes.WITH_DIRECTIVE:
+ // TODO remove this after remove checkNever
+ // generated, skip
+ break
+ default:
+ return checkNever(oper)
+ }
+
+ return []
+}
+
+export function genEffects(effects: IREffect[], context: CodegenContext) {
+ if (context.genEffect) {
+ return context.genEffect(effects)
+ }
+ const [frag, push] = buildCodeFragment()
+ for (const effect of effects) {
+ push(...genEffect(effect, context))
+ }
+ return frag
+}
+
+function genEffect({ operations }: IREffect, context: CodegenContext) {
+ const { newline, withIndent, vaporHelper } = context
+ const [frag, push] = buildCodeFragment()
+ push(newline(), `${vaporHelper('renderEffect')}(() => {`)
+ withIndent(() => {
+ operations.forEach(op => push(...genOperation(op, context)))
+ })
+ push(newline(), '})')
+ return frag
+}
+
+// remove when stable
+// @ts-expect-error
+function checkNever(x: never): never {}
export { transformVShow } from './transforms/vShow'
export { transformVText } from './transforms/vText'
export { transformVIf } from './transforms/vIf'
+export { transformVFor } from './transforms/vFor'
export enum IRNodeTypes {
ROOT,
+ BLOCK_FUNCTION,
+
TEMPLATE_FACTORY,
FRAGMENT_FACTORY,
WITH_DIRECTIVE,
IF,
- BLOCK_FUNCTION,
+ FOR,
}
export interface BaseIRNode {
negative?: BlockFunctionIRNode | IfIRNode
}
+export interface ForIRNode extends BaseIRNode {
+ type: IRNodeTypes.FOR
+ id: number
+ source: IRExpression
+ value?: SimpleExpressionNode
+ key?: SimpleExpressionNode
+ index?: SimpleExpressionNode
+ render: BlockFunctionIRNode
+}
+
export interface TemplateFactoryIRNode extends BaseIRNode {
type: IRNodeTypes.TEMPLATE_FACTORY
template: string
| AppendNodeIRNode
| WithDirectiveIRNode
| IfIRNode
+ | ForIRNode
export type BlockIRNode = RootIRNode | BlockFunctionIRNode
import {
type AllNode,
+ type AttributeNode,
type TransformOptions as BaseTransformOptions,
type CompilerCompatOptions,
+ type DirectiveNode,
type ElementNode,
ElementTypes,
NodeTypes,
type ParentNode,
type RootNode,
type TemplateChildNode,
+ type TemplateNode,
defaultOnError,
defaultOnWarn,
isVSlot,
}
}
}
+
+export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
+ if (node.tagType === ElementTypes.TEMPLATE) {
+ return node
+ }
+
+ const reserved: Array<AttributeNode | DirectiveNode> = []
+ const pass: Array<AttributeNode | DirectiveNode> = []
+ node.props.forEach(prop => {
+ if (prop.type === NodeTypes.DIRECTIVE && dirs.includes(prop.name)) {
+ reserved.push(prop)
+ } else {
+ pass.push(prop)
+ }
+ })
+
+ return extend({}, node, {
+ type: NodeTypes.ELEMENT,
+ tag: 'template',
+ props: reserved,
+ tagType: ElementTypes.TEMPLATE,
+ children: [extend({}, node, { props: pass } as TemplateChildNode)],
+ } as Partial<TemplateNode>)
+}
--- /dev/null
+import {
+ type ElementNode,
+ ErrorCodes,
+ type SimpleExpressionNode,
+ createCompilerError,
+} from '@vue/compiler-dom'
+import {
+ type TransformContext,
+ createStructuralDirectiveTransform,
+ genDefaultDynamic,
+ wrapTemplate,
+} from '../transform'
+import {
+ type BlockFunctionIRNode,
+ DynamicFlag,
+ type IRDynamicInfo,
+ IRNodeTypes,
+ type VaporDirectiveNode,
+} from '../ir'
+import { extend } from '@vue/shared'
+
+export const transformVFor = createStructuralDirectiveTransform(
+ 'for',
+ processFor,
+)
+
+export function processFor(
+ node: ElementNode,
+ dir: VaporDirectiveNode,
+ context: TransformContext<ElementNode>,
+) {
+ if (!dir.exp) {
+ context.options.onError(
+ createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc),
+ )
+ return
+ }
+ const parseResult = dir.forParseResult
+ if (!parseResult) {
+ context.options.onError(
+ createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc),
+ )
+ return
+ }
+
+ const { source, value, key, index } = parseResult
+
+ context.node = node = wrapTemplate(node, ['for'])
+ context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
+ const id = context.reference()
+ const render: BlockFunctionIRNode = {
+ type: IRNodeTypes.BLOCK_FUNCTION,
+ loc: node.loc,
+ node,
+ templateIndex: -1,
+ dynamic: extend(genDefaultDynamic(), {
+ flags: DynamicFlag.REFERENCED,
+ } satisfies Partial<IRDynamicInfo>),
+ effect: [],
+ operation: [],
+ }
+ const exitBlock = context.enterBlock(render)
+ context.reference()
+
+ return () => {
+ context.template += context.childrenTemplate.filter(Boolean).join('')
+ context.registerTemplate()
+ exitBlock()
+ context.registerOperation({
+ type: IRNodeTypes.FOR,
+ id,
+ loc: dir.loc,
+ source: source as SimpleExpressionNode,
+ value: value as SimpleExpressionNode | undefined,
+ key: key as SimpleExpressionNode | undefined,
+ index: index as SimpleExpressionNode | undefined,
+ render,
+ })
+ }
+}
import {
type ElementNode,
- ElementTypes,
ErrorCodes,
NodeTypes,
type TemplateChildNode,
- type TemplateNode,
createCompilerError,
createSimpleExpression,
} from '@vue/compiler-dom'
type TransformContext,
createStructuralDirectiveTransform,
genDefaultDynamic,
+ wrapTemplate,
} from '../transform'
import {
type BlockFunctionIRNode,
// TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
if (__DEV__ && comments.length) {
- node = wrapTemplate(node)
+ node = wrapTemplate(node, ['else-if', 'else'])
context.node = node = extend({}, node, {
children: [...comments, ...node.children],
})
node: ElementNode,
context: TransformContext<ElementNode>,
): [BlockFunctionIRNode, () => void] {
- context.node = node = wrapTemplate(node)
+ context.node = node = wrapTemplate(node, ['if', 'else-if', 'else'])
const branch: BlockFunctionIRNode = {
type: IRNodeTypes.BLOCK_FUNCTION,
}
return [branch, onExit]
}
-
-function wrapTemplate(node: ElementNode): TemplateNode {
- if (node.tagType === ElementTypes.TEMPLATE) {
- return node
- }
- return extend({}, node, {
- type: NodeTypes.ELEMENT,
- tag: 'template',
- props: [],
- tagType: ElementTypes.TEMPLATE,
- children: [
- extend({}, node, {
- props: node.props.filter(
- p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
- ),
- } as TemplateChildNode),
- ],
- } as Partial<TemplateNode>)
-}
getter = () => traverse(baseGetter())
}
+ const scope = getCurrentScope()
+
if (once) {
if (!cb) {
// onEffectCleanup need use effect as a key
- getCurrentScope()?.effects.push((effect = {} as any))
+ scope?.effects.push((effect = {} as any))
getter()
return
}
if (immediate) {
// onEffectCleanup need use effect as a key
- getCurrentScope()?.effects.push((effect = {} as any))
+ scope?.effects.push((effect = {} as any))
callWithAsyncErrorHandling(
cb,
onError,
let effectScheduler: EffectScheduler = () => scheduler(job, effect, false)
- effect = new ReactiveEffect(getter, NOOP, effectScheduler)
+ effect = new ReactiveEffect(getter, NOOP, effectScheduler, scope)
cleanup = effect.onStop = () => {
const cleanups = cleanupMap.get(effect)
public fn: () => T,
public trigger: () => void,
public scheduler?: EffectScheduler,
- scope?: EffectScope,
+ public scope?: EffectScope,
) {
recordEffectScope(this, scope)
}
-import { getCurrentEffect, onEffectCleanup } from '@vue/reactivity'
+import {
+ getCurrentEffect,
+ getCurrentScope,
+ onEffectCleanup,
+ onScopeDispose,
+} from '@vue/reactivity'
import { recordPropMetadata } from './patchProp'
import { toHandlerKey } from '@vue/shared'
) {
recordPropMetadata(el, toHandlerKey(event), handler)
el.addEventListener(event, handler, options)
- if (getCurrentEffect()) {
- onEffectCleanup(() => el.removeEventListener(event, handler, options))
+
+ const scope = getCurrentScope()
+ const effect = getCurrentEffect()
+
+ const cleanup = () => el.removeEventListener(event, handler, options)
+ if (effect && effect.scope === scope) {
+ onEffectCleanup(cleanup)
+ } else if (scope) {
+ onScopeDispose(cleanup)
}
}
--- /dev/null
+<script setup lang="ts">
+const name = 'click'
+</script>
+
+<template>
+ <button @[name]="">click me</button>
+</template>
-import { render } from 'vue/vapor'
+import { render, unmountComponent } from 'vue/vapor'
const modules = import.meta.glob<any>('./*.(vue|js)')
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
-mod.then(({ default: mod }) => render(mod, {}, '#app'))
+mod.then(({ default: mod }) => {
+ const instance = render(mod, {}, '#app')
+ // @ts-expect-error
+ globalThis.unmount = () => {
+ unmountComponent(instance)
+ }
+})
tasks.value = []
}
-function handleRemove(idx: number) {
+function handleRemove(idx: number, task: Task) {
+ console.log(task)
tasks.value.splice(idx, 1)
}
</script>
<template>
<h1>todos</h1>
<ul>
- <!-- TODO: v-for -->
- <li v-if="tasks[0]" :class="{ del: tasks[0]?.completed }">
+ <li
+ v-for="(task, index) of tasks"
+ :key="index"
+ :class="{ del: task.completed }"
+ >
<input
type="checkbox"
- :checked="tasks[0]?.completed"
- @change="handleComplete(0, $event)"
+ :checked="task.completed"
+ @change="handleComplete(index, $event)"
/>
- {{ tasks[0]?.title }}
- <button @click="handleRemove(0)">x</button>
- </li>
- <li v-if="tasks[1]" :class="{ del: tasks[1]?.completed }">
- <input
- type="checkbox"
- :checked="tasks[1]?.completed"
- @change="handleComplete(1, $event)"
- />
- {{ tasks[1]?.title }}
- <button @click="handleRemove(1)">x</button>
- </li>
- <li v-if="tasks[2]" :class="{ del: tasks[2]?.completed }">
- <input
- type="checkbox"
- :checked="tasks[2]?.completed"
- @change="handleComplete(2, $event)"
- />
- {{ tasks[2]?.title }}
- <button @click="handleRemove(2)">x</button>
- </li>
- <li v-if="tasks[3]" :class="{ del: tasks[3]?.completed }">
- <input
- type="checkbox"
- :checked="tasks[3]?.completed"
- @change="handleComplete(3, $event)"
- />
- {{ tasks[3]?.title }}
- <button @click="handleRemove(3)">x</button>
+ {{ task.title }}
+ <button @click="handleRemove(index, task)">x</button>
</li>
</ul>
<p>