feat(compiler-vapor): v-for (#101)

This commit is contained in:
三咲智子 Kevin Deng 2024-01-31 17:00:19 +08:00 committed by GitHub
parent 7b036fd4c0
commit 681dc5d954
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 513 additions and 157 deletions

View File

@ -0,0 +1,40 @@
// 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
}"
`;

View File

@ -0,0 +1,73 @@
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()
})
})

View File

@ -25,6 +25,7 @@ import { transformInterpolation } from './transforms/transformInterpolation'
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>
@ -102,6 +103,7 @@ export function getBaseTransformPreset(
transformRef,
transformInterpolation,
transformVIf,
transformVFor,
transformElement,
],
{

View File

@ -7,23 +7,10 @@ import {
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'
@ -89,6 +76,23 @@ export class CodegenContext {
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',
@ -270,45 +274,3 @@ export function buildCodeFragment() {
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 {}

View File

@ -10,18 +10,22 @@ import {
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(),
'}',
]
@ -30,8 +34,9 @@ export function genBlockFunction(
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}()`)
@ -52,19 +57,14 @@ export function genBlockFunctionContent(
push(...genWithDirective(directives, ctx))
}
for (const operation of ir.operation) {
push(...genOperation(operation, ctx))
}
push(...genOperations(ir.operation, ctx))
push(...genEffects(ir.effect, ctx))
for (const { operations } of ir.effect) {
push(newline(), `${vaporHelper('renderEffect')}(() => {`)
withIndent(() => {
operations.forEach(op => push(...genOperation(op, ctx)))
})
push(newline(), '})')
}
push(newline(), `return n${ir.dynamic.id}`)
push(
newline(),
'return ',
...(returnValue ? returnValue() : [`n${ir.dynamic.id}`]),
)
return frag
}

View File

@ -45,13 +45,13 @@ export function genSetEvent(
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 {

View File

@ -17,7 +17,6 @@ import {
export function genExpression(
node: IRExpression,
context: CodegenContext,
knownIds: Record<string, number> = Object.create(null),
): CodeFragment[] {
const {
options: { prefixIdentifiers },
@ -41,22 +40,13 @@ export function genExpression(
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()
@ -92,11 +82,17 @@ const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
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:

View File

@ -0,0 +1,89 @@
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})`,
]
}
}

View File

@ -0,0 +1,89 @@
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 {}

View File

@ -13,3 +13,4 @@ export { transformOnce } from './transforms/vOnce'
export { transformVShow } from './transforms/vShow'
export { transformVText } from './transforms/vText'
export { transformVIf } from './transforms/vIf'
export { transformVFor } from './transforms/vFor'

View File

@ -12,6 +12,8 @@ import type { DirectiveTransform, NodeTransform } from './transform'
export enum IRNodeTypes {
ROOT,
BLOCK_FUNCTION,
TEMPLATE_FACTORY,
FRAGMENT_FACTORY,
@ -30,7 +32,7 @@ export enum IRNodeTypes {
WITH_DIRECTIVE,
IF,
BLOCK_FUNCTION,
FOR,
}
export interface BaseIRNode {
@ -65,6 +67,16 @@ export interface IfIRNode extends 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
@ -176,6 +188,7 @@ export type OperationNode =
| AppendNodeIRNode
| WithDirectiveIRNode
| IfIRNode
| ForIRNode
export type BlockIRNode = RootIRNode | BlockFunctionIRNode

View File

@ -1,13 +1,16 @@
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,
@ -403,3 +406,27 @@ export function createStructuralDirectiveTransform(
}
}
}
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>)
}

View File

@ -0,0 +1,80 @@
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,
})
}
}

View File

@ -1,10 +1,8 @@
import {
type ElementNode,
ElementTypes,
ErrorCodes,
NodeTypes,
type TemplateChildNode,
type TemplateNode,
createCompilerError,
createSimpleExpression,
} from '@vue/compiler-dom'
@ -12,6 +10,7 @@ import {
type TransformContext,
createStructuralDirectiveTransform,
genDefaultDynamic,
wrapTemplate,
} from '../transform'
import {
type BlockFunctionIRNode,
@ -117,7 +116,7 @@ export function processIf(
// 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],
})
@ -145,7 +144,7 @@ export function createIfBranch(
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,
@ -168,22 +167,3 @@ export function createIfBranch(
}
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>)
}

View File

@ -229,16 +229,18 @@ export function baseWatch(
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,
@ -317,7 +319,7 @@ export function baseWatch(
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)

View File

@ -70,7 +70,7 @@ export class ReactiveEffect<T = any> {
public fn: () => T,
public trigger: () => void,
public scheduler?: EffectScheduler,
scope?: EffectScope,
public scope?: EffectScope,
) {
recordEffectScope(this, scope)
}

View File

@ -1,4 +1,9 @@
import { getCurrentEffect, onEffectCleanup } from '@vue/reactivity'
import {
getCurrentEffect,
getCurrentScope,
onEffectCleanup,
onScopeDispose,
} from '@vue/reactivity'
import { recordPropMetadata } from './patchProp'
import { toHandlerKey } from '@vue/shared'
@ -10,7 +15,14 @@ export function on(
) {
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)
}
}

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
const name = 'click'
</script>
<template>
<button @[name]="">click me</button>
</template>

View File

@ -1,6 +1,12 @@
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)
}
})

View File

@ -34,7 +34,8 @@ function handleClearAll() {
tasks.value = []
}
function handleRemove(idx: number) {
function handleRemove(idx: number, task: Task) {
console.log(task)
tasks.value.splice(idx, 1)
}
</script>
@ -42,42 +43,18 @@ function handleRemove(idx: number) {
<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>