mirror of https://github.com/vuejs/core.git
feat: create component & component lifecycle/props/attrs (#151)
This commit is contained in:
parent
5d15314c4e
commit
463b47e83d
|
@ -120,18 +120,19 @@ export function render(_ctx) {
|
|||
`;
|
||||
|
||||
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
|
||||
"import { createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
|
||||
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
|
||||
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
|
||||
const t1 = _template("<div><Comp></Comp></div>")
|
||||
const t1 = _template("<div></div>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const n0 = t0()
|
||||
const n2 = t1()
|
||||
const n1 = _createTextNode()
|
||||
_insert(n1, n2)
|
||||
_renderEffect(() => _setText(n1, _ctx.bar))
|
||||
_renderEffect(() => _setDynamicProp(n2, "id", _ctx.foo))
|
||||
return [n0, n2]
|
||||
const n3 = t1()
|
||||
const n1 = _createComponent(_resolveComponent("Comp"))
|
||||
const n2 = _createTextNode()
|
||||
_insert([n1, n2], n3)
|
||||
_renderEffect(() => _setText(n2, _ctx.bar))
|
||||
_renderEffect(() => _setDynamicProp(n3, "id", _ctx.foo))
|
||||
return [n0, n3]
|
||||
}"
|
||||
`;
|
||||
|
||||
|
|
|
@ -72,7 +72,6 @@ describe('compile', () => {
|
|||
expect(code).not.contains('effect')
|
||||
})
|
||||
|
||||
// TODO: support multiple root nodes and components
|
||||
test('should not affect siblings after it', () => {
|
||||
const code = compile(
|
||||
`<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { isArray } from '@vue/shared'
|
||||
import type { CodegenContext } from '../generate'
|
||||
import type { CreateComponentIRNode, IRProp } from '../ir'
|
||||
import {
|
||||
type CodeFragment,
|
||||
INDENT_END,
|
||||
INDENT_START,
|
||||
NEWLINE,
|
||||
genCall,
|
||||
genMulti,
|
||||
} from './utils'
|
||||
import { genExpression } from './expression'
|
||||
import { genPropKey } from './prop'
|
||||
|
||||
export function genCreateComponent(
|
||||
oper: CreateComponentIRNode,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const { vaporHelper } = context
|
||||
|
||||
const tag = oper.resolve
|
||||
? genCall(vaporHelper('resolveComponent'), JSON.stringify(oper.tag))
|
||||
: [oper.tag]
|
||||
|
||||
return [
|
||||
NEWLINE,
|
||||
`const n${oper.id} = `,
|
||||
...genCall(vaporHelper('createComponent'), tag, genProps()),
|
||||
]
|
||||
|
||||
function genProps() {
|
||||
const props = oper.props
|
||||
.map(props => {
|
||||
if (isArray(props)) {
|
||||
if (!props.length) return undefined
|
||||
return genStaticProps(props)
|
||||
} else {
|
||||
return ['() => (', ...genExpression(props, context), ')']
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
if (props.length) {
|
||||
return genMulti(['[', ']', ', '], ...props)
|
||||
}
|
||||
}
|
||||
|
||||
function genStaticProps(props: IRProp[]) {
|
||||
return genMulti(
|
||||
[
|
||||
['{', INDENT_START, NEWLINE],
|
||||
[INDENT_END, NEWLINE, '}'],
|
||||
[', ', NEWLINE],
|
||||
],
|
||||
...props.map(prop => {
|
||||
return [
|
||||
...genPropKey(prop, context),
|
||||
': () => (',
|
||||
...genExpression(prop.values[0], context),
|
||||
')',
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import {
|
|||
NEWLINE,
|
||||
buildCodeFragment,
|
||||
} from './utils'
|
||||
import { genCreateComponent } from './component'
|
||||
|
||||
export function genOperations(opers: OperationNode[], context: CodegenContext) {
|
||||
const [frag, push] = buildCodeFragment()
|
||||
|
@ -56,6 +57,8 @@ export function genOperation(
|
|||
return genIf(oper, context)
|
||||
case IRNodeTypes.FOR:
|
||||
return genFor(oper, context)
|
||||
case IRNodeTypes.CREATE_COMPONENT_NODE:
|
||||
return genCreateComponent(oper, context)
|
||||
}
|
||||
|
||||
return []
|
||||
|
|
|
@ -78,14 +78,14 @@ function genLiteralObjectProps(
|
|||
return genMulti(
|
||||
['{ ', ' }', ', '],
|
||||
...props.map(prop => [
|
||||
...genPropertyKey(prop, context),
|
||||
...genPropKey(prop, context),
|
||||
`: `,
|
||||
...genPropValue(prop.values, context),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function genPropertyKey(
|
||||
export function genPropKey(
|
||||
{ key: node, runtimeCamelize, modifier }: IRProp,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
|
|
|
@ -29,6 +29,7 @@ export enum IRNodeTypes {
|
|||
INSERT_NODE,
|
||||
PREPEND_NODE,
|
||||
CREATE_TEXT_NODE,
|
||||
CREATE_COMPONENT_NODE,
|
||||
|
||||
WITH_DIRECTIVE,
|
||||
|
||||
|
@ -173,6 +174,16 @@ export interface WithDirectiveIRNode extends BaseIRNode {
|
|||
builtin?: VaporHelper
|
||||
}
|
||||
|
||||
export interface CreateComponentIRNode extends BaseIRNode {
|
||||
type: IRNodeTypes.CREATE_COMPONENT_NODE
|
||||
id: number
|
||||
tag: string
|
||||
props: IRProps[]
|
||||
// TODO slots
|
||||
|
||||
resolve: boolean
|
||||
}
|
||||
|
||||
export type IRNode = OperationNode | RootIRNode
|
||||
export type OperationNode =
|
||||
| SetPropIRNode
|
||||
|
@ -189,6 +200,7 @@ export type OperationNode =
|
|||
| WithDirectiveIRNode
|
||||
| IfIRNode
|
||||
| ForIRNode
|
||||
| CreateComponentIRNode
|
||||
|
||||
export enum DynamicFlag {
|
||||
NONE = 0,
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
TransformContext,
|
||||
} from '../transform'
|
||||
import {
|
||||
DynamicFlag,
|
||||
IRNodeTypes,
|
||||
type IRProp,
|
||||
type IRProps,
|
||||
|
@ -29,8 +30,7 @@ export const isReservedProp = /*#__PURE__*/ makeMap(
|
|||
|
||||
export const transformElement: NodeTransform = (node, context) => {
|
||||
return function postTransformElement() {
|
||||
node = context.node
|
||||
|
||||
;({ node } = context)
|
||||
if (
|
||||
!(
|
||||
node.type === NodeTypes.ELEMENT &&
|
||||
|
@ -41,37 +41,94 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||
return
|
||||
}
|
||||
|
||||
const { tag, props } = node
|
||||
const isComponent = node.tagType === ElementTypes.COMPONENT
|
||||
const { tag, tagType } = node
|
||||
const isComponent = tagType === ElementTypes.COMPONENT
|
||||
const propsResult = buildProps(
|
||||
node,
|
||||
context as TransformContext<ElementNode>,
|
||||
)
|
||||
|
||||
context.template += `<${tag}`
|
||||
if (props.length) {
|
||||
buildProps(
|
||||
node,
|
||||
context as TransformContext<ElementNode>,
|
||||
undefined,
|
||||
isComponent,
|
||||
)
|
||||
}
|
||||
const { scopeId } = context.options
|
||||
if (scopeId) {
|
||||
context.template += ` ${scopeId}`
|
||||
}
|
||||
context.template += `>` + context.childrenTemplate.join('')
|
||||
|
||||
// TODO remove unnecessary close tag, e.g. if it's the last element of the template
|
||||
if (!isVoidTag(tag)) {
|
||||
context.template += `</${tag}>`
|
||||
}
|
||||
;(isComponent ? transformComponentElement : transformNativeElement)(
|
||||
tag,
|
||||
propsResult,
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function transformComponentElement(
|
||||
tag: string,
|
||||
propsResult: PropsResult,
|
||||
context: TransformContext,
|
||||
) {
|
||||
const { bindingMetadata } = context.options
|
||||
const resolve = !bindingMetadata[tag]
|
||||
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
|
||||
|
||||
context.registerOperation({
|
||||
type: IRNodeTypes.CREATE_COMPONENT_NODE,
|
||||
id: context.reference(),
|
||||
tag,
|
||||
props: propsResult[0] ? propsResult[1] : [propsResult[1]],
|
||||
resolve,
|
||||
})
|
||||
}
|
||||
|
||||
function transformNativeElement(
|
||||
tag: string,
|
||||
propsResult: ReturnType<typeof buildProps>,
|
||||
context: TransformContext,
|
||||
) {
|
||||
const { scopeId } = context.options
|
||||
|
||||
context.template += `<${tag}`
|
||||
if (scopeId) context.template += ` ${scopeId}`
|
||||
|
||||
if (propsResult[0] /* dynamic props */) {
|
||||
const [, dynamicArgs, expressions] = propsResult
|
||||
context.registerEffect(expressions, [
|
||||
{
|
||||
type: IRNodeTypes.SET_DYNAMIC_PROPS,
|
||||
element: context.reference(),
|
||||
props: dynamicArgs,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
for (const prop of propsResult[1]) {
|
||||
const { key, values } = prop
|
||||
if (key.isStatic && values.length === 1 && values[0].isStatic) {
|
||||
context.template += ` ${key.content}`
|
||||
if (values[0].content) context.template += `="${values[0].content}"`
|
||||
} else {
|
||||
context.registerEffect(values, [
|
||||
{
|
||||
type: IRNodeTypes.SET_PROP,
|
||||
element: context.reference(),
|
||||
prop,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.template += `>` + context.childrenTemplate.join('')
|
||||
// TODO remove unnecessary close tag, e.g. if it's the last element of the template
|
||||
if (!isVoidTag(tag)) {
|
||||
context.template += `</${tag}>`
|
||||
}
|
||||
}
|
||||
|
||||
export type PropsResult =
|
||||
| [dynamic: true, props: IRProps[], expressions: SimpleExpressionNode[]]
|
||||
| [dynamic: false, props: IRProp[]]
|
||||
|
||||
function buildProps(
|
||||
node: ElementNode,
|
||||
context: TransformContext<ElementNode>,
|
||||
props: (VaporDirectiveNode | AttributeNode)[] = node.props as any,
|
||||
isComponent: boolean,
|
||||
) {
|
||||
): PropsResult {
|
||||
const props = node.props as (VaporDirectiveNode | AttributeNode)[]
|
||||
if (props.length === 0) return [false, []]
|
||||
|
||||
const dynamicArgs: IRProps[] = []
|
||||
const dynamicExpr: SimpleExpressionNode[] = []
|
||||
let results: DirectiveTransformResult[] = []
|
||||
|
@ -112,31 +169,11 @@ function buildProps(
|
|||
if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) {
|
||||
// take rest of props as dynamic props
|
||||
pushMergeArg()
|
||||
context.registerEffect(dynamicExpr, [
|
||||
{
|
||||
type: IRNodeTypes.SET_DYNAMIC_PROPS,
|
||||
element: context.reference(),
|
||||
props: dynamicArgs,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
const irProps = dedupeProperties(results)
|
||||
for (const prop of irProps) {
|
||||
const { key, values } = prop
|
||||
if (key.isStatic && values.length === 1 && values[0].isStatic) {
|
||||
context.template += ` ${key.content}`
|
||||
if (values[0].content) context.template += `="${values[0].content}"`
|
||||
} else {
|
||||
context.registerEffect(values, [
|
||||
{
|
||||
type: IRNodeTypes.SET_PROP,
|
||||
element: context.reference(),
|
||||
prop,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
return [true, dynamicArgs, dynamicExpr]
|
||||
}
|
||||
|
||||
const irProps = dedupeProperties(results)
|
||||
return [false, irProps]
|
||||
}
|
||||
|
||||
function transformProp(
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import type { Data } from '@vue/shared'
|
||||
import {
|
||||
type App,
|
||||
type ComponentInternalInstance,
|
||||
type ObjectComponent,
|
||||
type SetupFn,
|
||||
render as _render,
|
||||
createComponentInstance,
|
||||
createVaporApp,
|
||||
defineComponent,
|
||||
} from '../src'
|
||||
import type { RawProps } from '../src/componentProps'
|
||||
|
||||
export function makeRender<Component = ObjectComponent | SetupFn>(
|
||||
initHost = () => {
|
||||
|
@ -27,18 +27,20 @@ export function makeRender<Component = ObjectComponent | SetupFn>(
|
|||
const define = (comp: Component) => {
|
||||
const component = defineComponent(comp as any)
|
||||
let instance: ComponentInternalInstance
|
||||
let app: App
|
||||
const render = (
|
||||
props: Data = {},
|
||||
props: RawProps = {},
|
||||
container: string | ParentNode = '#host',
|
||||
) => {
|
||||
instance = createComponentInstance(component, props)
|
||||
_render(instance, container)
|
||||
app = createVaporApp(component, props)
|
||||
instance = app.mount(container)
|
||||
return res()
|
||||
}
|
||||
const res = () => ({
|
||||
component,
|
||||
host,
|
||||
instance,
|
||||
app,
|
||||
render,
|
||||
})
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ref, setText, template, unmountComponent, watchEffect } from '../src'
|
||||
import { ref, setText, template, watchEffect } from '../src'
|
||||
import { describe, expect } from 'vitest'
|
||||
import { makeRender } from './_utils'
|
||||
|
||||
|
@ -6,7 +6,7 @@ const define = makeRender()
|
|||
|
||||
describe('component', () => {
|
||||
test('unmountComponent', async () => {
|
||||
const { host, instance } = define(() => {
|
||||
const { host, app } = define(() => {
|
||||
const count = ref(0)
|
||||
const t0 = template('<div></div>')
|
||||
const n0 = t0()
|
||||
|
@ -16,7 +16,7 @@ describe('component', () => {
|
|||
return n0
|
||||
}).render()
|
||||
expect(host.innerHTML).toBe('<div>0</div>')
|
||||
unmountComponent(instance)
|
||||
app.unmount()
|
||||
expect(host.innerHTML).toBe('')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
// Note: emits and listener fallthrough is tested in
|
||||
// ./rendererAttrsFallthrough.spec.ts.
|
||||
|
||||
import { nextTick, onBeforeUnmount, unmountComponent } from '../src'
|
||||
import { nextTick, onBeforeUnmount } from '../src'
|
||||
import { isEmitListener } from '../src/componentEmits'
|
||||
import { makeRender } from './_utils'
|
||||
|
||||
const define = makeRender<any>()
|
||||
|
||||
describe('component: emit', () => {
|
||||
describe.todo('component: emit', () => {
|
||||
test('trigger handlers', () => {
|
||||
const { render } = define({
|
||||
render() {},
|
||||
|
@ -137,9 +137,7 @@ describe('component: emit', () => {
|
|||
const fn2 = vi.fn()
|
||||
|
||||
render({
|
||||
get onFoo() {
|
||||
return [fn1, fn2]
|
||||
},
|
||||
onFoo: () => [fn1, fn2],
|
||||
})
|
||||
expect(fn1).toHaveBeenCalledTimes(1)
|
||||
expect(fn1).toHaveBeenCalledWith(1)
|
||||
|
@ -246,22 +244,22 @@ describe('component: emit', () => {
|
|||
const fn1 = vi.fn()
|
||||
const fn2 = vi.fn()
|
||||
render({
|
||||
get modelValue() {
|
||||
modelValue() {
|
||||
return null
|
||||
},
|
||||
get modelModifiers() {
|
||||
modelModifiers() {
|
||||
return { number: true }
|
||||
},
|
||||
get ['onUpdate:modelValue']() {
|
||||
['onUpdate:modelValue']() {
|
||||
return fn1
|
||||
},
|
||||
get foo() {
|
||||
foo() {
|
||||
return null
|
||||
},
|
||||
get fooModifiers() {
|
||||
fooModifiers() {
|
||||
return { number: true }
|
||||
},
|
||||
get ['onUpdate:foo']() {
|
||||
['onUpdate:foo']() {
|
||||
return fn2
|
||||
},
|
||||
})
|
||||
|
@ -282,22 +280,22 @@ describe('component: emit', () => {
|
|||
const fn1 = vi.fn()
|
||||
const fn2 = vi.fn()
|
||||
render({
|
||||
get modelValue() {
|
||||
modelValue() {
|
||||
return null
|
||||
},
|
||||
get modelModifiers() {
|
||||
modelModifiers() {
|
||||
return { trim: true }
|
||||
},
|
||||
get ['onUpdate:modelValue']() {
|
||||
['onUpdate:modelValue']() {
|
||||
return fn1
|
||||
},
|
||||
get foo() {
|
||||
foo() {
|
||||
return null
|
||||
},
|
||||
get fooModifiers() {
|
||||
fooModifiers() {
|
||||
return { trim: true }
|
||||
},
|
||||
get 'onUpdate:foo'() {
|
||||
'onUpdate:foo'() {
|
||||
return fn2
|
||||
},
|
||||
})
|
||||
|
@ -318,22 +316,22 @@ describe('component: emit', () => {
|
|||
const fn1 = vi.fn()
|
||||
const fn2 = vi.fn()
|
||||
render({
|
||||
get modelValue() {
|
||||
modelValue() {
|
||||
return null
|
||||
},
|
||||
get modelModifiers() {
|
||||
modelModifiers() {
|
||||
return { trim: true, number: true }
|
||||
},
|
||||
get ['onUpdate:modelValue']() {
|
||||
['onUpdate:modelValue']() {
|
||||
return fn1
|
||||
},
|
||||
get foo() {
|
||||
foo() {
|
||||
return null
|
||||
},
|
||||
get fooModifiers() {
|
||||
fooModifiers() {
|
||||
return { trim: true, number: true }
|
||||
},
|
||||
get ['onUpdate:foo']() {
|
||||
['onUpdate:foo']() {
|
||||
return fn2
|
||||
},
|
||||
})
|
||||
|
@ -352,13 +350,13 @@ describe('component: emit', () => {
|
|||
})
|
||||
const fn = vi.fn()
|
||||
render({
|
||||
get modelValue() {
|
||||
modelValue() {
|
||||
return null
|
||||
},
|
||||
get modelModifiers() {
|
||||
modelModifiers() {
|
||||
return { trim: true }
|
||||
},
|
||||
get ['onUpdate:modelValue']() {
|
||||
['onUpdate:modelValue']() {
|
||||
return fn
|
||||
},
|
||||
})
|
||||
|
@ -397,7 +395,7 @@ describe('component: emit', () => {
|
|||
|
||||
test('does not emit after unmount', async () => {
|
||||
const fn = vi.fn()
|
||||
const { instance } = define({
|
||||
const { app } = define({
|
||||
emits: ['closing'],
|
||||
setup(_: any, { emit }: any) {
|
||||
onBeforeUnmount(async () => {
|
||||
|
@ -412,7 +410,7 @@ describe('component: emit', () => {
|
|||
},
|
||||
})
|
||||
await nextTick()
|
||||
unmountComponent(instance)
|
||||
app.unmount()
|
||||
await nextTick()
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
|
||||
|
||||
// NOTE: not supported
|
||||
// mixins
|
||||
// caching
|
||||
|
||||
import { setCurrentInstance } from '../src/component'
|
||||
import {
|
||||
createComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
ref,
|
||||
setText,
|
||||
template,
|
||||
toRefs,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from '../src'
|
||||
import { makeRender } from './_utils'
|
||||
|
||||
const define = makeRender<any>()
|
||||
|
||||
describe('component props (vapor)', () => {
|
||||
describe('component: props', () => {
|
||||
// NOTE: no proxy
|
||||
test('stateful', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
|
@ -32,65 +32,51 @@ describe('component props (vapor)', () => {
|
|||
},
|
||||
})
|
||||
|
||||
render({
|
||||
get fooBar() {
|
||||
return 1
|
||||
},
|
||||
get bar() {
|
||||
return 2
|
||||
},
|
||||
})
|
||||
expect(props.fooBar).toEqual(1)
|
||||
expect(attrs.bar).toEqual(2)
|
||||
render({ fooBar: () => 1, bar: () => 2 })
|
||||
expect(props).toEqual({ fooBar: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
// test passing kebab-case and resolving to camelCase
|
||||
render({
|
||||
get ['foo-bar']() {
|
||||
return 2
|
||||
},
|
||||
get bar() {
|
||||
return 3
|
||||
},
|
||||
get baz() {
|
||||
return 4
|
||||
},
|
||||
})
|
||||
expect(props.fooBar).toEqual(2)
|
||||
expect(attrs.bar).toEqual(3)
|
||||
expect(attrs.baz).toEqual(4)
|
||||
render({ 'foo-bar': () => 2, bar: () => 3, baz: () => 4 })
|
||||
expect(props).toEqual({ fooBar: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
// test updating kebab-case should not delete it (#955)
|
||||
render({
|
||||
get ['foo-bar']() {
|
||||
return 3
|
||||
},
|
||||
get bar() {
|
||||
return 3
|
||||
},
|
||||
get baz() {
|
||||
return 4
|
||||
},
|
||||
get barBaz() {
|
||||
return 5
|
||||
},
|
||||
})
|
||||
expect(props.fooBar).toEqual(3)
|
||||
expect(props.barBaz).toEqual(5)
|
||||
expect(attrs.bar).toEqual(3)
|
||||
expect(attrs.baz).toEqual(4)
|
||||
render({ 'foo-bar': () => 3, bar: () => 3, baz: () => 4, barBaz: () => 5 })
|
||||
expect(props).toEqual({ fooBar: 3, barBaz: 5 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render({
|
||||
get qux() {
|
||||
return 5
|
||||
},
|
||||
})
|
||||
expect(props.fooBar).toBeUndefined()
|
||||
expect(props.barBaz).toBeUndefined()
|
||||
expect(attrs.qux).toEqual(5)
|
||||
// remove the props with camelCase key (#1412)
|
||||
render({ qux: () => 5 })
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test.todo('stateful with setup', () => {
|
||||
// TODO:
|
||||
test.fails('stateful with setup', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
|
||||
const { render } = define({
|
||||
props: ['foo'],
|
||||
setup(_props: any, { attrs: _attrs }: any) {
|
||||
return () => {
|
||||
props = _props
|
||||
attrs = _attrs
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
render({ foo: () => 1, bar: () => 2 })
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render({ foo: () => 2, bar: () => 3, baz: () => 4 })
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render({ qux: () => 5 })
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
test('functional with declaration', () => {
|
||||
|
@ -105,42 +91,19 @@ describe('component props (vapor)', () => {
|
|||
})
|
||||
Comp.props = ['foo']
|
||||
|
||||
render({
|
||||
get foo() {
|
||||
return 1
|
||||
},
|
||||
get bar() {
|
||||
return 2
|
||||
},
|
||||
})
|
||||
expect(props.foo).toEqual(1)
|
||||
expect(attrs.bar).toEqual(2)
|
||||
render({ foo: () => 1, bar: () => 2 })
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
|
||||
render({
|
||||
get foo() {
|
||||
return 2
|
||||
},
|
||||
get bar() {
|
||||
return 3
|
||||
},
|
||||
get baz() {
|
||||
return 4
|
||||
},
|
||||
})
|
||||
expect(props.foo).toEqual(2)
|
||||
expect(attrs.bar).toEqual(3)
|
||||
expect(attrs.baz).toEqual(4)
|
||||
render({ foo: () => 2, bar: () => 3, baz: () => 4 })
|
||||
expect(props).toEqual({ foo: 2 })
|
||||
expect(attrs).toEqual({ bar: 3, baz: 4 })
|
||||
|
||||
render({
|
||||
get qux() {
|
||||
return 5
|
||||
},
|
||||
})
|
||||
expect(props.foo).toBeUndefined()
|
||||
expect(attrs.qux).toEqual(5)
|
||||
render({ qux: () => 5 })
|
||||
expect(props).toEqual({})
|
||||
expect(attrs).toEqual({ qux: 5 })
|
||||
})
|
||||
|
||||
// FIXME:
|
||||
test('functional without declaration', () => {
|
||||
let props: any
|
||||
let attrs: any
|
||||
|
@ -152,21 +115,15 @@ describe('component props (vapor)', () => {
|
|||
return {}
|
||||
})
|
||||
|
||||
render({
|
||||
get foo() {
|
||||
return 1
|
||||
},
|
||||
})
|
||||
expect(props.foo).toEqual(1)
|
||||
expect(attrs.foo).toEqual(1)
|
||||
render({ foo: () => 1 })
|
||||
expect(props).toEqual({ foo: 1 })
|
||||
expect(attrs).toEqual({ foo: 1 })
|
||||
expect(props).toBe(attrs)
|
||||
|
||||
render({
|
||||
get foo() {
|
||||
return 2
|
||||
},
|
||||
})
|
||||
expect(props.foo).toEqual(2)
|
||||
expect(attrs.foo).toEqual(2)
|
||||
render({ bar: () => 2 })
|
||||
expect(props).toEqual({ bar: 2 })
|
||||
expect(attrs).toEqual({ bar: 2 })
|
||||
expect(props).toBe(attrs)
|
||||
})
|
||||
|
||||
test('boolean casting', () => {
|
||||
|
@ -186,15 +143,16 @@ describe('component props (vapor)', () => {
|
|||
|
||||
render({
|
||||
// absent should cast to false
|
||||
bar: '', // empty string should cast to true
|
||||
baz: 'baz', // same string should cast to true
|
||||
qux: 'ok', // other values should be left in-tact (but raise warning)
|
||||
bar: () => '', // empty string should cast to true
|
||||
baz: () => 'baz', // same string should cast to true
|
||||
qux: () => 'ok', // other values should be left in-tact (but raise warning)
|
||||
})
|
||||
|
||||
expect(props.foo).toBe(false)
|
||||
expect(props.bar).toBe(true)
|
||||
expect(props.baz).toBe(true)
|
||||
expect(props.qux).toBe('ok')
|
||||
// expect('type check failed for prop "qux"').toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('default value', () => {
|
||||
|
@ -221,82 +179,57 @@ describe('component props (vapor)', () => {
|
|||
},
|
||||
})
|
||||
|
||||
render({
|
||||
get foo() {
|
||||
return 2
|
||||
},
|
||||
})
|
||||
render({ foo: () => 2 })
|
||||
expect(props.foo).toBe(2)
|
||||
// const prevBar = props.bar
|
||||
props.bar
|
||||
expect(props.bar).toEqual({ a: 1 })
|
||||
expect(props.baz).toEqual(defaultBaz)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: (caching is not supported)
|
||||
expect(defaultFn).toHaveBeenCalledTimes(3)
|
||||
expect(defaultFn).toHaveBeenCalledTimes(1)
|
||||
expect(defaultBaz).toHaveBeenCalledTimes(0)
|
||||
|
||||
// #999: updates should not cause default factory of unchanged prop to be
|
||||
// called again
|
||||
render({
|
||||
get foo() {
|
||||
return 3
|
||||
},
|
||||
})
|
||||
render({ foo: () => 3 })
|
||||
|
||||
expect(props.foo).toBe(3)
|
||||
expect(props.bar).toEqual({ a: 1 })
|
||||
// expect(props.bar).toBe(prevBar) // failed: (caching is not supported)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
|
||||
|
||||
render({
|
||||
get bar() {
|
||||
return { b: 2 }
|
||||
},
|
||||
})
|
||||
render({ bar: () => ({ b: 2 }) })
|
||||
expect(props.foo).toBe(1)
|
||||
expect(props.bar).toEqual({ b: 2 })
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
|
||||
|
||||
render({
|
||||
get foo() {
|
||||
return 3
|
||||
},
|
||||
get bar() {
|
||||
return { b: 3 }
|
||||
},
|
||||
foo: () => 3,
|
||||
bar: () => ({ b: 3 }),
|
||||
})
|
||||
expect(props.foo).toBe(3)
|
||||
expect(props.bar).toEqual({ b: 3 })
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
|
||||
|
||||
render({
|
||||
get bar() {
|
||||
return { b: 4 }
|
||||
},
|
||||
})
|
||||
render({ bar: () => ({ b: 4 }) })
|
||||
expect(props.foo).toBe(1)
|
||||
expect(props.bar).toEqual({ b: 4 })
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
|
||||
})
|
||||
|
||||
test.todo('using inject in default value factory', () => {
|
||||
// TODO: impl inject
|
||||
})
|
||||
|
||||
// NOTE: maybe it's unnecessary
|
||||
// https://github.com/vuejs/core-vapor/pull/99#discussion_r1472647377
|
||||
test('optimized props updates', async () => {
|
||||
const renderChild = define({
|
||||
const t0 = template('<div>')
|
||||
const { component: Child } = define({
|
||||
props: ['foo'],
|
||||
render() {
|
||||
const instance = getCurrentInstance()!
|
||||
const t0 = template('<div></div>')
|
||||
const n0 = t0()
|
||||
watchEffect(() => {
|
||||
setText(n0, instance.props.foo)
|
||||
})
|
||||
watchEffect(() => setText(n0, instance.props.foo))
|
||||
return n0
|
||||
},
|
||||
}).render
|
||||
})
|
||||
|
||||
const foo = ref(1)
|
||||
const id = ref('a')
|
||||
|
@ -305,54 +238,36 @@ describe('component props (vapor)', () => {
|
|||
return { foo, id }
|
||||
},
|
||||
render(_ctx: Record<string, any>) {
|
||||
const t0 = template('<div>')
|
||||
const n0 = t0()
|
||||
renderChild(
|
||||
{
|
||||
get foo() {
|
||||
return _ctx.foo
|
||||
},
|
||||
get id() {
|
||||
return _ctx.id
|
||||
},
|
||||
},
|
||||
n0 as HTMLDivElement,
|
||||
)
|
||||
return n0
|
||||
return createComponent(Child, {
|
||||
foo: () => _ctx.foo,
|
||||
id: () => _ctx.id,
|
||||
})
|
||||
},
|
||||
}).render()
|
||||
const reset = setCurrentInstance(instance)
|
||||
// expect(host.innerHTML).toBe('<div id="a">1</div>') // TODO: Fallthrough Attributes
|
||||
expect(host.innerHTML).toBe('<div><div>1</div></div>')
|
||||
expect(host.innerHTML).toBe('<div>1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
// expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
|
||||
expect(host.innerHTML).toBe('<div><div>2</div></div>')
|
||||
expect(host.innerHTML).toBe('<div>2</div>')
|
||||
|
||||
// id.value = 'b'
|
||||
// await nextTick()
|
||||
id.value = 'b'
|
||||
await nextTick()
|
||||
// expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
|
||||
reset()
|
||||
})
|
||||
|
||||
describe('validator', () => {
|
||||
test('validator should be called with two arguments', () => {
|
||||
let args: any
|
||||
const mockFn = vi.fn((..._args: any[]) => {
|
||||
args = _args
|
||||
return true
|
||||
})
|
||||
|
||||
const mockFn = vi.fn((...args: any[]) => true)
|
||||
const props = {
|
||||
get foo() {
|
||||
return 1
|
||||
},
|
||||
get bar() {
|
||||
return 2
|
||||
},
|
||||
foo: () => 1,
|
||||
bar: () => 2,
|
||||
}
|
||||
|
||||
const t0 = template('<div/>')
|
||||
define({
|
||||
props: {
|
||||
foo: {
|
||||
|
@ -364,19 +279,11 @@ describe('component props (vapor)', () => {
|
|||
},
|
||||
},
|
||||
render() {
|
||||
const t0 = template('<div/>')
|
||||
const n0 = t0()
|
||||
return n0
|
||||
return t0()
|
||||
},
|
||||
}).render(props)
|
||||
|
||||
expect(mockFn).toHaveBeenCalled()
|
||||
// NOTE: Vapor Component props defined by getter. So, `props` not Equal to `{ foo: 1, bar: 2 }`
|
||||
// expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
|
||||
expect(args.length).toBe(2)
|
||||
expect(args[0]).toBe(1)
|
||||
expect(args[1].foo).toEqual(1)
|
||||
expect(args[1].bar).toEqual(2)
|
||||
expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
|
||||
})
|
||||
|
||||
// TODO: impl setter and warnner
|
||||
|
@ -401,16 +308,16 @@ describe('component props (vapor)', () => {
|
|||
return n0
|
||||
},
|
||||
}).render!({
|
||||
get foo() {
|
||||
foo() {
|
||||
return 1
|
||||
},
|
||||
get bar() {
|
||||
bar() {
|
||||
return 2
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
`Set operation on key "bar" failed: target is readonly.`,
|
||||
`Set operation on key "bar" failed: taris readonly.`,
|
||||
).toHaveBeenWarnedLast()
|
||||
expect(mockFn).toHaveBeenCalledWith(2)
|
||||
},
|
||||
|
@ -450,70 +357,71 @@ describe('component props (vapor)', () => {
|
|||
return () => null
|
||||
},
|
||||
}).render({
|
||||
get ['foo-bar']() {
|
||||
return 'hello'
|
||||
},
|
||||
['foo-bar']: () => 'hello',
|
||||
})
|
||||
expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('props type support BigInt', () => {
|
||||
const t0 = template('<div>')
|
||||
const { host } = define({
|
||||
props: {
|
||||
foo: BigInt,
|
||||
},
|
||||
render() {
|
||||
const instance = getCurrentInstance()!
|
||||
const t0 = template('<div></div>')
|
||||
const n0 = t0()
|
||||
watchEffect(() => {
|
||||
setText(n0, instance.props.foo)
|
||||
})
|
||||
watchEffect(() => setText(n0, instance.props.foo))
|
||||
return n0
|
||||
},
|
||||
}).render({
|
||||
get foo() {
|
||||
return BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000)
|
||||
},
|
||||
foo: () =>
|
||||
BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000),
|
||||
})
|
||||
expect(host.innerHTML).toBe('<div>60000000100000111</div>')
|
||||
})
|
||||
|
||||
// #3288
|
||||
// #3474
|
||||
test.todo(
|
||||
'declared prop key should be present even if not passed',
|
||||
async () => {
|
||||
// let initialKeys: string[] = []
|
||||
// const changeSpy = vi.fn()
|
||||
// const passFoo = ref(false)
|
||||
// const Comp = {
|
||||
// props: ['foo'],
|
||||
// setup() {
|
||||
// const instance = getCurrentInstance()!
|
||||
// initialKeys = Object.keys(instance.props)
|
||||
// watchEffect(changeSpy)
|
||||
// return {}
|
||||
// },
|
||||
// render() {
|
||||
// return {}
|
||||
// },
|
||||
// }
|
||||
// const Parent = createIf(
|
||||
// () => passFoo.value,
|
||||
// () => {
|
||||
// return render(Comp , { foo: 1 }, host) // TODO: createComponent fn
|
||||
// },
|
||||
// )
|
||||
// // expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||
},
|
||||
'should cache the value returned from the default factory to avoid unnecessary watcher trigger',
|
||||
() => {},
|
||||
)
|
||||
|
||||
// #3288
|
||||
test('declared prop key should be present even if not passed', async () => {
|
||||
let initialKeys: string[] = []
|
||||
const changeSpy = vi.fn()
|
||||
const passFoo = ref(false)
|
||||
|
||||
const Comp: any = {
|
||||
render() {},
|
||||
props: {
|
||||
foo: String,
|
||||
},
|
||||
setup(props: any) {
|
||||
initialKeys = Object.keys(props)
|
||||
const { foo } = toRefs(props)
|
||||
watch(foo, changeSpy)
|
||||
},
|
||||
}
|
||||
|
||||
define(() =>
|
||||
createComponent(Comp, [() => (passFoo.value ? { foo: () => 'ok' } : {})]),
|
||||
).render()
|
||||
|
||||
expect(initialKeys).toMatchObject(['foo'])
|
||||
passFoo.value = true
|
||||
await nextTick()
|
||||
expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// #3371
|
||||
test.todo(`avoid double-setting props when casting`, async () => {
|
||||
// TODO: proide, slots
|
||||
})
|
||||
|
||||
test('support null in required + multiple-type declarations', () => {
|
||||
// NOTE: type check is not supported
|
||||
test.todo('support null in required + multiple-type declarations', () => {
|
||||
const { render } = define({
|
||||
props: {
|
||||
foo: { type: [Function, null], required: true },
|
||||
|
@ -522,11 +430,11 @@ describe('component props (vapor)', () => {
|
|||
})
|
||||
|
||||
expect(() => {
|
||||
render({ foo: () => {} })
|
||||
render({ foo: () => () => {} })
|
||||
}).not.toThrow()
|
||||
|
||||
expect(() => {
|
||||
render({ foo: null })
|
||||
render({ foo: () => null })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
|
@ -537,22 +445,17 @@ describe('component props (vapor)', () => {
|
|||
const instance = getCurrentInstance()!
|
||||
const t0 = template('<div></div>')
|
||||
const n0 = t0()
|
||||
watchEffect(() => {
|
||||
watchEffect(() =>
|
||||
setText(
|
||||
n0,
|
||||
JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
|
||||
)
|
||||
})
|
||||
),
|
||||
)
|
||||
return n0
|
||||
},
|
||||
})
|
||||
|
||||
let attrs: any = {
|
||||
get foo() {
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
|
||||
const attrs: any = { foo: () => undefined }
|
||||
render(attrs)
|
||||
|
||||
expect(host.innerHTML).toBe(
|
||||
|
@ -567,8 +470,20 @@ describe('component props (vapor)', () => {
|
|||
type: String,
|
||||
},
|
||||
}
|
||||
define({ props, render() {} }).render({ msg: 'test' })
|
||||
define({ props, render() {} }).render({ msg: () => 'test' })
|
||||
|
||||
expect(Object.keys(props.msg).length).toBe(1)
|
||||
})
|
||||
|
||||
test('should warn against reserved prop names', () => {
|
||||
const { render } = define({
|
||||
props: {
|
||||
$foo: String,
|
||||
},
|
||||
render() {},
|
||||
})
|
||||
|
||||
render({ msg: () => 'test' })
|
||||
expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { insert, normalizeBlock, prepend, remove } from '../../src/dom/element'
|
||||
import { fragmentKey } from '../../src/render'
|
||||
import { fragmentKey } from '../../src/apiRender'
|
||||
|
||||
const node1 = document.createTextNode('node1')
|
||||
const node2 = document.createTextNode('node2')
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
type Component,
|
||||
createComponentInstance,
|
||||
currentInstance,
|
||||
} from './component'
|
||||
import { setupComponent } from './apiRender'
|
||||
import type { RawProps } from './componentProps'
|
||||
|
||||
export function createComponent(comp: Component, rawProps: RawProps = null) {
|
||||
const current = currentInstance!
|
||||
const instance = createComponentInstance(comp, rawProps)
|
||||
setupComponent(instance)
|
||||
|
||||
// register sub-component with current component for lifecycle management
|
||||
current.comps.add(instance)
|
||||
|
||||
return instance.block
|
||||
}
|
|
@ -2,7 +2,7 @@ import { type EffectScope, effectScope, isReactive } from '@vue/reactivity'
|
|||
import { isArray, isObject, isString } from '@vue/shared'
|
||||
import { createComment, createTextNode, insert, remove } from './dom/element'
|
||||
import { renderEffect } from './renderWatch'
|
||||
import { type Block, type Fragment, fragmentKey } from './render'
|
||||
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
||||
import { warn } from './warning'
|
||||
|
||||
interface ForBlock extends Fragment {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { renderWatch } from './renderWatch'
|
||||
import { type Block, type Fragment, fragmentKey } from './render'
|
||||
import { type Block, type Fragment, fragmentKey } from './apiRender'
|
||||
import { type EffectScope, effectScope } from '@vue/reactivity'
|
||||
import { createComment, createTextNode, insert, remove } from './dom/element'
|
||||
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import { isObject } from '@vue/shared'
|
||||
import {
|
||||
type Component,
|
||||
type ComponentInternalInstance,
|
||||
createComponentInstance,
|
||||
} from './component'
|
||||
import { warn } from './warning'
|
||||
import { version } from '.'
|
||||
import { render, setupComponent, unmountComponent } from './apiRender'
|
||||
import type { RawProps } from './componentProps'
|
||||
|
||||
export function createVaporApp(
|
||||
rootComponent: Component,
|
||||
rootProps: RawProps | null = null,
|
||||
): App {
|
||||
if (rootProps != null && !isObject(rootProps)) {
|
||||
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
|
||||
rootProps = null
|
||||
}
|
||||
|
||||
const context = createAppContext()
|
||||
let instance: ComponentInternalInstance
|
||||
|
||||
const app: App = {
|
||||
version,
|
||||
|
||||
get config() {
|
||||
return context.config
|
||||
},
|
||||
|
||||
set config(v) {
|
||||
if (__DEV__) {
|
||||
warn(
|
||||
`app.config cannot be replaced. Modify individual options instead.`,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
mount(rootContainer): any {
|
||||
if (!instance) {
|
||||
instance = createComponentInstance(rootComponent, rootProps)
|
||||
setupComponent(instance)
|
||||
render(instance, rootContainer)
|
||||
return instance
|
||||
} else if (__DEV__) {
|
||||
warn(
|
||||
`App has already been mounted.\n` +
|
||||
`If you want to remount the same app, move your app creation logic ` +
|
||||
`into a factory function and create fresh app instances for each ` +
|
||||
`mount - e.g. \`const createMyApp = () => createApp(App)\``,
|
||||
)
|
||||
}
|
||||
},
|
||||
unmount() {
|
||||
if (instance) {
|
||||
unmountComponent(instance)
|
||||
} else if (__DEV__) {
|
||||
warn(`Cannot unmount an app that is not mounted.`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
function createAppContext(): AppContext {
|
||||
return {
|
||||
app: null as any,
|
||||
config: {
|
||||
errorHandler: undefined,
|
||||
warnHandler: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export interface App {
|
||||
version: string
|
||||
config: AppConfig
|
||||
|
||||
mount(
|
||||
rootContainer: ParentNode | string,
|
||||
isHydrate?: boolean,
|
||||
): ComponentInternalInstance
|
||||
unmount(): void
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
errorHandler?: (
|
||||
err: unknown,
|
||||
instance: ComponentInternalInstance | null,
|
||||
info: string,
|
||||
) => void
|
||||
warnHandler?: (
|
||||
msg: string,
|
||||
instance: ComponentInternalInstance | null,
|
||||
trace: string,
|
||||
) => void
|
||||
}
|
||||
|
||||
export interface AppContext {
|
||||
app: App // for devtools
|
||||
config: AppConfig
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
import { proxyRefs } from '@vue/reactivity'
|
||||
import { invokeArrayFns, isArray, isFunction, isObject } from '@vue/shared'
|
||||
import {
|
||||
type ComponentInternalInstance,
|
||||
setCurrentInstance,
|
||||
unsetCurrentInstance,
|
||||
} from './component'
|
||||
import { invokeDirectiveHook } from './directives'
|
||||
import { isArray, isFunction, isObject } from '@vue/shared'
|
||||
import { type ComponentInternalInstance, setCurrentInstance } from './component'
|
||||
import { insert, querySelector, remove } from './dom/element'
|
||||
import { flushPostFlushCbs, queuePostRenderEffect } from './scheduler'
|
||||
import { proxyRefs } from '@vue/reactivity'
|
||||
import { invokeLifecycle } from './componentLifecycle'
|
||||
import { VaporLifecycleHooks } from './apiLifecycle'
|
||||
|
||||
export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)
|
||||
|
||||
|
@ -18,28 +15,9 @@ export type Fragment = {
|
|||
[fragmentKey]: true
|
||||
}
|
||||
|
||||
export function render(
|
||||
instance: ComponentInternalInstance,
|
||||
container: string | ParentNode,
|
||||
): void {
|
||||
mountComponent(instance, (container = normalizeContainer(container)))
|
||||
flushPostFlushCbs()
|
||||
}
|
||||
|
||||
function normalizeContainer(container: string | ParentNode): ParentNode {
|
||||
return typeof container === 'string'
|
||||
? (querySelector(container) as ParentNode)
|
||||
: container
|
||||
}
|
||||
|
||||
function mountComponent(
|
||||
instance: ComponentInternalInstance,
|
||||
container: ParentNode,
|
||||
) {
|
||||
instance.container = container
|
||||
|
||||
export function setupComponent(instance: ComponentInternalInstance): void {
|
||||
const reset = setCurrentInstance(instance)
|
||||
const block = instance.scope.run(() => {
|
||||
instance.scope.run(() => {
|
||||
const { component, props, emit, attrs } = instance
|
||||
const ctx = { expose: () => {}, emit, attrs }
|
||||
|
||||
|
@ -70,40 +48,52 @@ function mountComponent(
|
|||
block = []
|
||||
}
|
||||
return (instance.block = block)
|
||||
})!
|
||||
const { bm, m } = instance
|
||||
})
|
||||
reset()
|
||||
}
|
||||
|
||||
export function render(
|
||||
instance: ComponentInternalInstance,
|
||||
container: string | ParentNode,
|
||||
): void {
|
||||
mountComponent(instance, (container = normalizeContainer(container)))
|
||||
flushPostFlushCbs()
|
||||
}
|
||||
|
||||
function normalizeContainer(container: string | ParentNode): ParentNode {
|
||||
return typeof container === 'string'
|
||||
? (querySelector(container) as ParentNode)
|
||||
: container
|
||||
}
|
||||
|
||||
function mountComponent(
|
||||
instance: ComponentInternalInstance,
|
||||
container: ParentNode,
|
||||
) {
|
||||
instance.container = container
|
||||
|
||||
// hook: beforeMount
|
||||
bm && invokeArrayFns(bm)
|
||||
invokeDirectiveHook(instance, 'beforeMount')
|
||||
invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_MOUNT, 'beforeMount')
|
||||
|
||||
insert(block, instance.container)
|
||||
insert(instance.block!, instance.container)
|
||||
instance.isMounted = true
|
||||
|
||||
// hook: mounted
|
||||
queuePostRenderEffect(() => {
|
||||
invokeDirectiveHook(instance, 'mounted')
|
||||
m && invokeArrayFns(m)
|
||||
})
|
||||
reset()
|
||||
invokeLifecycle(instance, VaporLifecycleHooks.MOUNTED, 'mounted', true)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
export function unmountComponent(instance: ComponentInternalInstance) {
|
||||
const { container, block, scope, um, bum } = instance
|
||||
const { container, block, scope } = instance
|
||||
|
||||
// hook: beforeUnmount
|
||||
bum && invokeArrayFns(bum)
|
||||
invokeDirectiveHook(instance, 'beforeUnmount')
|
||||
invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_UNMOUNT, 'beforeUnmount')
|
||||
|
||||
scope.stop()
|
||||
block && remove(block, container)
|
||||
instance.isMounted = false
|
||||
instance.isUnmounted = true
|
||||
|
||||
// hook: unmounted
|
||||
invokeDirectiveHook(instance, 'unmounted')
|
||||
um && invokeArrayFns(um)
|
||||
unsetCurrentInstance()
|
||||
invokeLifecycle(instance, VaporLifecycleHooks.UNMOUNTED, 'unmounted', true)
|
||||
queuePostRenderEffect(() => (instance.isUnmounted = true))
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import { EffectScope } from '@vue/reactivity'
|
||||
|
||||
import { EMPTY_OBJ, isFunction } from '@vue/shared'
|
||||
import type { Block } from './render'
|
||||
import type { Block } from './apiRender'
|
||||
import type { DirectiveBinding } from './directives'
|
||||
import {
|
||||
type ComponentPropsOptions,
|
||||
type NormalizedPropsOptions,
|
||||
type NormalizedRawProps,
|
||||
type RawProps,
|
||||
initProps,
|
||||
normalizePropsOptions,
|
||||
} from './componentProps'
|
||||
|
@ -37,33 +38,29 @@ type LifecycleHook<TFn = Function> = TFn[] | null
|
|||
|
||||
export interface ComponentInternalInstance {
|
||||
uid: number
|
||||
container: ParentNode
|
||||
vapor: true
|
||||
|
||||
block: Block | null
|
||||
container: ParentNode
|
||||
parent: ComponentInternalInstance | null
|
||||
|
||||
scope: EffectScope
|
||||
component: FunctionalComponent | ObjectComponent
|
||||
comps: Set<ComponentInternalInstance>
|
||||
dirs: Map<Node, DirectiveBinding[]>
|
||||
|
||||
// TODO: ExtraProps: key, ref, ...
|
||||
rawProps: { [key: string]: any }
|
||||
|
||||
// normalized options
|
||||
rawProps: NormalizedRawProps
|
||||
propsOptions: NormalizedPropsOptions
|
||||
emitsOptions: ObjectEmitsOptions | null
|
||||
|
||||
parent: ComponentInternalInstance | null
|
||||
|
||||
// state
|
||||
props: Data
|
||||
attrs: Data
|
||||
setupState: Data
|
||||
props: Data
|
||||
emit: EmitFn
|
||||
emitted: Record<string, boolean> | null
|
||||
attrs: Data
|
||||
refs: Data
|
||||
|
||||
vapor: true
|
||||
|
||||
/** directives */
|
||||
dirs: Map<Node, DirectiveBinding[]>
|
||||
|
||||
// lifecycle
|
||||
isMounted: boolean
|
||||
isUnmounted: boolean
|
||||
|
@ -141,37 +138,37 @@ export const unsetCurrentInstance = () => {
|
|||
}
|
||||
|
||||
let uid = 0
|
||||
export const createComponentInstance = (
|
||||
export function createComponentInstance(
|
||||
component: ObjectComponent | FunctionalComponent,
|
||||
rawProps: Data,
|
||||
): ComponentInternalInstance => {
|
||||
rawProps: RawProps | null,
|
||||
): ComponentInternalInstance {
|
||||
const instance: ComponentInternalInstance = {
|
||||
uid: uid++,
|
||||
block: null,
|
||||
container: null!, // set on mountComponent
|
||||
scope: new EffectScope(true /* detached */)!,
|
||||
component,
|
||||
rawProps,
|
||||
vapor: true,
|
||||
|
||||
// TODO: registory of parent
|
||||
block: null,
|
||||
container: null!,
|
||||
|
||||
// TODO
|
||||
parent: null,
|
||||
|
||||
scope: new EffectScope(true /* detached */)!,
|
||||
component,
|
||||
comps: new Set(),
|
||||
dirs: new Map(),
|
||||
|
||||
// resolved props and emits options
|
||||
rawProps: null!, // set later
|
||||
propsOptions: normalizePropsOptions(component),
|
||||
emitsOptions: normalizeEmitsOptions(component),
|
||||
|
||||
// emit
|
||||
emit: null!, // to be set immediately
|
||||
emitted: null,
|
||||
|
||||
// state
|
||||
props: EMPTY_OBJ,
|
||||
attrs: EMPTY_OBJ,
|
||||
setupState: EMPTY_OBJ,
|
||||
props: EMPTY_OBJ,
|
||||
emit: null!,
|
||||
emitted: null,
|
||||
attrs: EMPTY_OBJ,
|
||||
refs: EMPTY_OBJ,
|
||||
vapor: true,
|
||||
|
||||
dirs: new Map(),
|
||||
|
||||
// lifecycle
|
||||
isMounted: false,
|
||||
|
@ -227,8 +224,6 @@ export const createComponentInstance = (
|
|||
*/
|
||||
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
|
||||
}
|
||||
|
||||
// TODO init first
|
||||
initProps(instance, rawProps, !isFunction(component))
|
||||
instance.emit = emit.bind(null, instance)
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { camelize, isFunction } from '@vue/shared'
|
||||
import type { ComponentInternalInstance } from './component'
|
||||
import { isEmitListener } from './componentEmits'
|
||||
|
||||
export function patchAttrs(instance: ComponentInternalInstance) {
|
||||
const attrs = instance.attrs
|
||||
const options = instance.propsOptions[0]
|
||||
|
||||
const keys = new Set<string>()
|
||||
if (instance.rawProps.length)
|
||||
for (const props of Array.from(instance.rawProps).reverse()) {
|
||||
if (isFunction(props)) {
|
||||
const resolved = props()
|
||||
for (const rawKey in resolved) {
|
||||
registerAttr(rawKey, () => resolved[rawKey])
|
||||
}
|
||||
} else {
|
||||
for (const rawKey in props) {
|
||||
registerAttr(rawKey, props[rawKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in attrs) {
|
||||
if (!keys.has(key)) {
|
||||
delete attrs[key]
|
||||
}
|
||||
}
|
||||
|
||||
function registerAttr(key: string, getter: () => unknown) {
|
||||
if (
|
||||
(!options || !(camelize(key) in options)) &&
|
||||
!isEmitListener(instance.emitsOptions, key)
|
||||
) {
|
||||
keys.add(key)
|
||||
if (key in attrs) return
|
||||
Object.defineProperty(attrs, key, {
|
||||
get: getter,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
// NOTE: runtime-core/src/componentEmits.ts
|
||||
|
||||
// TODO WIP
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
type UnionToIntersection,
|
||||
|
@ -45,6 +48,8 @@ export function emit(
|
|||
...rawArgs: any[]
|
||||
) {
|
||||
if (instance.isUnmounted) return
|
||||
// TODO
|
||||
// @ts-expect-error
|
||||
const { rawProps } = instance
|
||||
|
||||
let args = rawArgs
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { invokeArrayFns } from '@vue/shared'
|
||||
import type { VaporLifecycleHooks } from './apiLifecycle'
|
||||
import { type ComponentInternalInstance, setCurrentInstance } from './component'
|
||||
import { queuePostRenderEffect } from './scheduler'
|
||||
import { type DirectiveHookName, invokeDirectiveHook } from './directives'
|
||||
|
||||
export function invokeLifecycle(
|
||||
instance: ComponentInternalInstance,
|
||||
lifecycle: VaporLifecycleHooks,
|
||||
directive: DirectiveHookName,
|
||||
post?: boolean,
|
||||
) {
|
||||
invokeArrayFns(post ? [invokeSub, invokeCurrent] : [invokeCurrent, invokeSub])
|
||||
|
||||
function invokeCurrent() {
|
||||
const hooks = instance[lifecycle]
|
||||
if (hooks) {
|
||||
const fn = () => {
|
||||
const reset = setCurrentInstance(instance)
|
||||
invokeArrayFns(hooks)
|
||||
reset()
|
||||
}
|
||||
post ? queuePostRenderEffect(fn) : fn()
|
||||
}
|
||||
|
||||
invokeDirectiveHook(instance, directive)
|
||||
}
|
||||
|
||||
function invokeSub() {
|
||||
instance.comps.forEach(comp =>
|
||||
invokeLifecycle(comp, lifecycle, directive, post),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
// NOTE: runtime-core/src/componentProps.ts
|
||||
|
||||
import {
|
||||
type Data,
|
||||
EMPTY_ARR,
|
||||
|
@ -11,14 +9,15 @@ import {
|
|||
isArray,
|
||||
isFunction,
|
||||
} from '@vue/shared'
|
||||
import { shallowReactive, shallowReadonly, toRaw } from '@vue/reactivity'
|
||||
import { baseWatch, shallowReactive } from '@vue/reactivity'
|
||||
import { warn } from './warning'
|
||||
import {
|
||||
type Component,
|
||||
type ComponentInternalInstance,
|
||||
setCurrentInstance,
|
||||
} from './component'
|
||||
import { isEmitListener } from './componentEmits'
|
||||
import { patchAttrs } from './componentAttrs'
|
||||
import { createVaporPreScheduler } from './scheduler'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> =
|
||||
| ComponentObjectPropsOptions<P>
|
||||
|
@ -69,108 +68,129 @@ type NormalizedProp =
|
|||
})
|
||||
|
||||
export type NormalizedProps = Record<string, NormalizedProp>
|
||||
export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
|
||||
export type NormalizedPropsOptions =
|
||||
| [props: NormalizedProps, needCastKeys: string[]]
|
||||
| []
|
||||
|
||||
type StaticProps = Record<string, () => unknown>
|
||||
type DynamicProps = () => Data
|
||||
export type NormalizedRawProps = Array<StaticProps | DynamicProps>
|
||||
export type RawProps = NormalizedRawProps | StaticProps | null
|
||||
|
||||
export function initProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
rawProps: RawProps,
|
||||
isStateful: boolean,
|
||||
) {
|
||||
const props: Data = {}
|
||||
const attrs: Data = {}
|
||||
const attrs = (instance.attrs = shallowReactive<Data>({}))
|
||||
|
||||
const [options, needCastKeys] = instance.propsOptions
|
||||
let hasAttrsChanged = false
|
||||
let rawCastValues: Data | undefined
|
||||
if (rawProps) {
|
||||
for (let key in rawProps) {
|
||||
const valueGetter = () => rawProps[key]
|
||||
let camelKey
|
||||
if (options && hasOwn(options, (camelKey = camelize(key)))) {
|
||||
if (!needCastKeys || !needCastKeys.includes(camelKey)) {
|
||||
// NOTE: must getter
|
||||
// props[camelKey] = value
|
||||
Object.defineProperty(props, camelKey, {
|
||||
get() {
|
||||
return valueGetter()
|
||||
},
|
||||
enumerable: true,
|
||||
})
|
||||
if (!rawProps) rawProps = []
|
||||
else if (!isArray(rawProps)) rawProps = [rawProps]
|
||||
instance.rawProps = rawProps
|
||||
|
||||
const [options] = instance.propsOptions
|
||||
|
||||
const hasDynamicProps = rawProps.some(isFunction)
|
||||
if (options) {
|
||||
if (hasDynamicProps) {
|
||||
for (const key in options) {
|
||||
const getter = () =>
|
||||
getDynamicPropValue(rawProps as NormalizedRawProps, key)
|
||||
registerProp(instance, props, key, getter, true)
|
||||
}
|
||||
} else {
|
||||
for (const key in options) {
|
||||
const rawKey = rawProps[0] && getRawKey(rawProps[0] as StaticProps, key)
|
||||
if (rawKey) {
|
||||
registerProp(
|
||||
instance,
|
||||
props,
|
||||
key,
|
||||
(rawProps[0] as StaticProps)[rawKey],
|
||||
)
|
||||
} else {
|
||||
// NOTE: must getter
|
||||
// ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
|
||||
rawCastValues || (rawCastValues = {})
|
||||
Object.defineProperty(rawCastValues, camelKey, {
|
||||
get() {
|
||||
return valueGetter()
|
||||
},
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
} else if (!isEmitListener(instance.emitsOptions, key)) {
|
||||
// if (!(key in attrs) || value !== attrs[key]) {
|
||||
if (!(key in attrs)) {
|
||||
// NOTE: must getter
|
||||
// attrs[key] = value
|
||||
Object.defineProperty(attrs, key, {
|
||||
get() {
|
||||
return valueGetter()
|
||||
},
|
||||
enumerable: true,
|
||||
})
|
||||
hasAttrsChanged = true
|
||||
registerProp(instance, props, key, undefined, false, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needCastKeys) {
|
||||
const rawCurrentProps = toRaw(props)
|
||||
const castValues = rawCastValues || EMPTY_OBJ
|
||||
for (let i = 0; i < needCastKeys.length; i++) {
|
||||
const key = needCastKeys[i]
|
||||
|
||||
// NOTE: must getter
|
||||
// props[key] = resolvePropValue(
|
||||
// options!,
|
||||
// rawCurrentProps,
|
||||
// key,
|
||||
// castValues[key],
|
||||
// instance,
|
||||
// !hasOwn(castValues, key),
|
||||
// )
|
||||
Object.defineProperty(props, key, {
|
||||
get() {
|
||||
return resolvePropValue(
|
||||
options!,
|
||||
rawCurrentProps,
|
||||
key,
|
||||
castValues[key],
|
||||
instance,
|
||||
!hasOwn(castValues, key),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// validation
|
||||
if (__DEV__) {
|
||||
validateProps(rawProps || {}, props, instance)
|
||||
validateProps(rawProps, props, options || {})
|
||||
}
|
||||
|
||||
if (hasDynamicProps) {
|
||||
baseWatch(() => patchAttrs(instance), undefined, {
|
||||
scheduler: createVaporPreScheduler(instance),
|
||||
})
|
||||
} else {
|
||||
patchAttrs(instance)
|
||||
}
|
||||
|
||||
if (isStateful) {
|
||||
instance.props = shallowReactive(props)
|
||||
instance.props = /* isSSR ? props : */ shallowReactive(props)
|
||||
} else {
|
||||
if (instance.propsOptions === EMPTY_ARR) {
|
||||
instance.props = attrs
|
||||
// functional w/ optional props, props === attrs
|
||||
instance.props = instance.propsOptions === EMPTY_ARR ? attrs : props
|
||||
}
|
||||
}
|
||||
|
||||
function registerProp(
|
||||
instance: ComponentInternalInstance,
|
||||
props: Data,
|
||||
rawKey: string,
|
||||
getter?: (() => unknown) | (() => DynamicPropResult),
|
||||
isDynamic?: boolean,
|
||||
isAbsent?: boolean,
|
||||
) {
|
||||
const key = camelize(rawKey)
|
||||
if (key in props) return
|
||||
|
||||
const [options, needCastKeys] = instance.propsOptions
|
||||
const needCast = needCastKeys && needCastKeys.includes(key)
|
||||
const withCast = (value: unknown, absent?: boolean) =>
|
||||
resolvePropValue(options!, props, key, value, instance, absent)
|
||||
|
||||
if (isAbsent) {
|
||||
props[key] = needCast ? withCast(undefined, true) : undefined
|
||||
} else {
|
||||
const get: () => unknown = isDynamic
|
||||
? needCast
|
||||
? () => withCast(...(getter!() as DynamicPropResult))
|
||||
: () => (getter!() as DynamicPropResult)[0]
|
||||
: needCast
|
||||
? () => withCast(getter!())
|
||||
: getter!
|
||||
|
||||
Object.defineProperty(props, key, {
|
||||
get,
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getRawKey(obj: Data, key: string) {
|
||||
return Object.keys(obj).find(k => camelize(k) === key)
|
||||
}
|
||||
|
||||
type DynamicPropResult = [value: unknown, absent: boolean]
|
||||
function getDynamicPropValue(
|
||||
rawProps: NormalizedRawProps,
|
||||
key: string,
|
||||
): DynamicPropResult {
|
||||
for (const props of Array.from(rawProps).reverse()) {
|
||||
if (isFunction(props)) {
|
||||
const resolved = props()
|
||||
const rawKey = getRawKey(resolved, key)
|
||||
if (rawKey) return [resolved[rawKey], false]
|
||||
} else {
|
||||
instance.props = props
|
||||
const rawKey = getRawKey(props, key)
|
||||
if (rawKey) return [props[rawKey](), false]
|
||||
}
|
||||
}
|
||||
instance.attrs = attrs
|
||||
|
||||
return hasAttrsChanged
|
||||
return [undefined, true]
|
||||
}
|
||||
|
||||
function resolvePropValue(
|
||||
|
@ -179,7 +199,7 @@ function resolvePropValue(
|
|||
key: string,
|
||||
value: unknown,
|
||||
instance: ComponentInternalInstance,
|
||||
isAbsent: boolean,
|
||||
isAbsent?: boolean,
|
||||
) {
|
||||
const opt = options[key]
|
||||
if (opt != null) {
|
||||
|
@ -223,12 +243,12 @@ function resolvePropValue(
|
|||
export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
|
||||
// TODO: cahching?
|
||||
|
||||
const raw = comp.props as any
|
||||
const normalized: NormalizedPropsOptions[0] = {}
|
||||
const raw = comp.props
|
||||
const normalized: NormalizedProps | undefined = {}
|
||||
const needCastKeys: NormalizedPropsOptions[1] = []
|
||||
|
||||
if (!raw) {
|
||||
return EMPTY_ARR as any
|
||||
return EMPTY_ARR as []
|
||||
}
|
||||
|
||||
if (isArray(raw)) {
|
||||
|
@ -238,7 +258,7 @@ export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
|
|||
normalized[normalizedKey] = EMPTY_OBJ
|
||||
}
|
||||
}
|
||||
} else if (raw) {
|
||||
} else {
|
||||
for (const key in raw) {
|
||||
const normalizedKey = camelize(key)
|
||||
if (validatePropName(normalizedKey)) {
|
||||
|
@ -267,6 +287,8 @@ export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
|
|||
function validatePropName(key: string) {
|
||||
if (key[0] !== '$') {
|
||||
return true
|
||||
} else if (__DEV__) {
|
||||
warn(`Invalid prop name: "${key}" is a reserved property.`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -296,22 +318,25 @@ function getTypeIndex(
|
|||
* dev only
|
||||
*/
|
||||
function validateProps(
|
||||
rawProps: Data,
|
||||
rawProps: NormalizedRawProps,
|
||||
props: Data,
|
||||
instance: ComponentInternalInstance,
|
||||
options: NormalizedProps,
|
||||
) {
|
||||
const resolvedValues = toRaw(props)
|
||||
const options = instance.propsOptions[0]
|
||||
const presentKeys: string[] = []
|
||||
for (const props of rawProps) {
|
||||
presentKeys.push(...Object.keys(isFunction(props) ? props() : props))
|
||||
}
|
||||
|
||||
for (const key in options) {
|
||||
let opt = options[key]
|
||||
if (opt == null) continue
|
||||
validateProp(
|
||||
key,
|
||||
resolvedValues[key],
|
||||
opt,
|
||||
__DEV__ ? shallowReadonly(resolvedValues) : resolvedValues,
|
||||
!hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key)),
|
||||
)
|
||||
const opt = options[key]
|
||||
if (opt != null)
|
||||
validateProp(
|
||||
key,
|
||||
props[key],
|
||||
opt,
|
||||
props,
|
||||
!presentKeys.some(k => camelize(k) === key),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,11 +346,11 @@ function validateProps(
|
|||
function validateProp(
|
||||
name: string,
|
||||
value: unknown,
|
||||
prop: PropOptions,
|
||||
option: PropOptions,
|
||||
props: Data,
|
||||
isAbsent: boolean,
|
||||
) {
|
||||
const { required, validator } = prop
|
||||
const { required, validator } = option
|
||||
// required!
|
||||
if (required && isAbsent) {
|
||||
warn('Missing required prop: "' + name + '"')
|
||||
|
|
|
@ -145,7 +145,3 @@ function callDirectiveHook(
|
|||
])
|
||||
resetTracking()
|
||||
}
|
||||
|
||||
export function resolveDirective() {
|
||||
// TODO
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { isArray, toDisplayString } from '@vue/shared'
|
||||
import type { Block } from '../render'
|
||||
import type { Block } from '../apiRender'
|
||||
|
||||
/*! #__NO_SIDE_EFFECTS__ */
|
||||
export function normalizeBlock(block: Block): Node[] {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export function resolveComponent() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export function resolveDirective() {
|
||||
// TODO
|
||||
}
|
|
@ -46,7 +46,6 @@ export {
|
|||
type FunctionalComponent,
|
||||
type SetupFn,
|
||||
} from './component'
|
||||
export { render, unmountComponent } from './render'
|
||||
export { renderEffect, renderWatch } from './renderWatch'
|
||||
export {
|
||||
watch,
|
||||
|
@ -62,7 +61,6 @@ export {
|
|||
} from './apiWatch'
|
||||
export {
|
||||
withDirectives,
|
||||
resolveDirective,
|
||||
type Directive,
|
||||
type DirectiveBinding,
|
||||
type DirectiveHook,
|
||||
|
@ -88,7 +86,6 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
|
|||
export { setRef } from './dom/templateRef'
|
||||
|
||||
export { defineComponent } from './apiDefineComponent'
|
||||
export { createComponentInstance } from './component'
|
||||
export {
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
|
@ -103,8 +100,17 @@ export {
|
|||
onErrorCaptured,
|
||||
// onServerPrefetch,
|
||||
} from './apiLifecycle'
|
||||
export {
|
||||
createVaporApp,
|
||||
type App,
|
||||
type AppConfig,
|
||||
type AppContext,
|
||||
} from './apiCreateVaporApp'
|
||||
export { createIf } from './apiCreateIf'
|
||||
export { createFor } from './apiCreateFor'
|
||||
export { createComponent } from './apiCreateComponent'
|
||||
|
||||
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
|
||||
|
||||
// **Internal** DOM-only runtime directive helpers
|
||||
export {
|
||||
|
|
|
@ -120,9 +120,9 @@ watch(
|
|||
files.value['src/index.html'] = new File(
|
||||
'src/index.html',
|
||||
`<script type="module">
|
||||
import { render } from 'vue/vapor'
|
||||
import { createVaporApp } from 'vue/vapor'
|
||||
import App from './App.vue'
|
||||
render(App, {}, '#app')` +
|
||||
createVaporApp(App).mount('#app')` +
|
||||
'<' +
|
||||
'/script>' +
|
||||
`<div id="app"></div>`,
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
onUnmounted,
|
||||
ref,
|
||||
} from 'vue/vapor'
|
||||
import SubComp from './sub-comp.vue'
|
||||
|
||||
const bar = ref('update')
|
||||
const id = ref('id')
|
||||
const p = ref<any>({
|
||||
bar,
|
||||
id: 'not id',
|
||||
test: 100,
|
||||
})
|
||||
|
||||
function update() {
|
||||
bar.value = 'updated'
|
||||
p.value.foo = 'updated foo'
|
||||
p.value.newAttr = 'new attr'
|
||||
id.value = 'updated id'
|
||||
}
|
||||
|
||||
function update2() {
|
||||
delete p.value.test
|
||||
}
|
||||
|
||||
onBeforeMount(() => console.log('root: before mount'))
|
||||
onMounted(() => console.log('root: mounted'))
|
||||
onBeforeUnmount(() => console.log('root: before unmount'))
|
||||
onUnmounted(() => console.log('root: unmounted'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
root comp
|
||||
<button @click="update">update</button>
|
||||
<button @click="update2">update2</button>
|
||||
<SubComp foo="foo" v-bind="p" :baz="'baz'" :id />
|
||||
</div>
|
||||
</template>
|
|
@ -1,4 +1,4 @@
|
|||
import { createComponentInstance, render, unmountComponent } from 'vue/vapor'
|
||||
import { createVaporApp } from 'vue/vapor'
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
|
||||
|
@ -6,14 +6,11 @@ const modules = import.meta.glob<any>('./**/*.(vue|js)')
|
|||
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
|
||||
|
||||
mod.then(({ default: mod }) => {
|
||||
if (mod.vapor) {
|
||||
const instance = createComponentInstance(mod, {})
|
||||
render(instance, '#app')
|
||||
// @ts-expect-error
|
||||
globalThis.unmount = () => {
|
||||
unmountComponent(instance)
|
||||
}
|
||||
} else {
|
||||
createApp(mod).mount('#app')
|
||||
const app = (mod.vapor ? createVaporApp : createApp)(mod)
|
||||
app.mount('#app')
|
||||
|
||||
// @ts-expect-error
|
||||
globalThis.unmount = () => {
|
||||
app.unmount()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,69 +1,53 @@
|
|||
// @ts-check
|
||||
import {
|
||||
children,
|
||||
createComponent,
|
||||
defineComponent,
|
||||
on,
|
||||
reactive,
|
||||
ref,
|
||||
render as renderComponent,
|
||||
renderEffect,
|
||||
setText,
|
||||
template,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from '@vue/vapor'
|
||||
|
||||
const t0 = template('<button></button>')
|
||||
|
||||
export default defineComponent({
|
||||
vapor: true,
|
||||
props: undefined,
|
||||
|
||||
setup(_, {}) {
|
||||
setup() {
|
||||
const count = ref(1)
|
||||
const props = reactive({
|
||||
a: 'b',
|
||||
'foo-bar': 100,
|
||||
})
|
||||
const handleClick = () => {
|
||||
count.value++
|
||||
props['foo-bar']++
|
||||
// @ts-expect-error
|
||||
props.boolean = true
|
||||
console.log(count)
|
||||
}
|
||||
|
||||
const __returned__ = { count, handleClick }
|
||||
|
||||
Object.defineProperty(__returned__, '__isScriptSetup', {
|
||||
enumerable: false,
|
||||
value: true,
|
||||
})
|
||||
|
||||
return __returned__
|
||||
},
|
||||
|
||||
render(_ctx) {
|
||||
const n0 = t0()
|
||||
const n1 = /** @type {HTMLButtonElement} */ (children(n0, 0))
|
||||
on(n1, 'click', () => _ctx.handleClick)
|
||||
watchEffect(() => {
|
||||
setText(n1, _ctx.count)
|
||||
})
|
||||
|
||||
// TODO: create component fn?
|
||||
// const c0 = createComponent(...)
|
||||
// insert(n0, c0)
|
||||
renderComponent(
|
||||
/** @type {any} */ (child),
|
||||
|
||||
// TODO: proxy??
|
||||
{
|
||||
/* <Comp :count="count" /> */
|
||||
get count() {
|
||||
return _ctx.count
|
||||
return (() => {
|
||||
const n0 = /** @type {HTMLButtonElement} */ (t0())
|
||||
on(n0, 'click', () => handleClick)
|
||||
renderEffect(() => setText(n0, count.value))
|
||||
/** @type {any} */
|
||||
const n1 = createComponent(child, [
|
||||
{
|
||||
/* <Comp :count="count" /> */
|
||||
count: () => {
|
||||
// console.trace('access')
|
||||
return count.value
|
||||
},
|
||||
/* <Comp :inline-double="count * 2" /> */
|
||||
inlineDouble: () => count.value * 2,
|
||||
id: () => 'hello',
|
||||
},
|
||||
|
||||
/* <Comp :inline-double="count * 2" /> */
|
||||
get inlineDouble() {
|
||||
return _ctx.count * 2
|
||||
},
|
||||
},
|
||||
// @ts-expect-error TODO
|
||||
n0[0],
|
||||
)
|
||||
|
||||
return n0
|
||||
() => props,
|
||||
])
|
||||
return [n0, n1]
|
||||
})()
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -74,25 +58,22 @@ const child = defineComponent({
|
|||
props: {
|
||||
count: { type: Number, default: 1 },
|
||||
inlineDouble: { type: Number, default: 2 },
|
||||
fooBar: { type: Number, required: true },
|
||||
boolean: { type: Boolean },
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
watch(
|
||||
() => props.count,
|
||||
v => console.log('count changed', v),
|
||||
)
|
||||
watch(
|
||||
() => props.inlineDouble,
|
||||
v => console.log('inlineDouble changed', v),
|
||||
)
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
console.log(props, { ...props })
|
||||
console.log(attrs, { ...attrs })
|
||||
|
||||
render(_ctx) {
|
||||
const n0 = t1()
|
||||
const n1 = children(n0, 0)
|
||||
watchEffect(() => {
|
||||
setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble)
|
||||
})
|
||||
return n0
|
||||
return (() => {
|
||||
const n0 = /** @type {HTMLParagraphElement} */ (t1())
|
||||
renderEffect(() =>
|
||||
setText(n0, props.count + ' * 2 = ' + props.inlineDouble),
|
||||
)
|
||||
const n1 = /** @type {HTMLParagraphElement} */ (t1())
|
||||
renderEffect(() => setText(n1, props.fooBar, ', ', props.boolean))
|
||||
return [n0, n1]
|
||||
})()
|
||||
},
|
||||
})
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
getCurrentInstance,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
watchEffect,
|
||||
} from 'vue/vapor'
|
||||
|
||||
const props = defineProps<{
|
||||
foo: string
|
||||
bar: string
|
||||
baz: string
|
||||
}>()
|
||||
|
||||
const attrs = getCurrentInstance()?.attrs
|
||||
|
||||
watchEffect(() => {
|
||||
console.log({ ...attrs })
|
||||
})
|
||||
|
||||
const keys = Object.keys
|
||||
|
||||
onBeforeMount(() => console.log('sub: before mount'))
|
||||
onMounted(() => console.log('sub: mounted'))
|
||||
onBeforeUnmount(() => console.log('sub: before unmount'))
|
||||
onUnmounted(() => console.log('sub: unmounted'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>sub-comp</div>
|
||||
{{ props }}
|
||||
{{ attrs }}
|
||||
{{ keys(attrs) }}
|
||||
</template>
|
Loading…
Reference in New Issue