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