Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime-vapor): component attrs #124

Merged
merged 3 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions packages/runtime-vapor/__tests__/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Data, isFunction } from '@vue/shared'
import type { Data } from '@vue/shared'
import {
type ComponentInternalInstance,
type ObjectComponent,
Expand All @@ -24,13 +24,7 @@ export function makeRender<Component = ObjectComponent | SetupFn>(
})

const define = (comp: Component) => {
const component = defineComponent(
isFunction(comp)
? {
setup: comp,
}
: comp,
)
const component = defineComponent(comp)
let instance: ComponentInternalInstance
const render = (
props: Data = {},
Expand Down
98 changes: 84 additions & 14 deletions packages/runtime-vapor/__tests__/componentProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,47 +22,72 @@ const define = makeRender<any>()
describe('component props (vapor)', () => {
test('stateful', () => {
let props: any
// TODO: attrs
let attrs: any

const { render } = define({
props: ['fooBar', 'barBaz'],
render() {
const instance = getCurrentInstance()!
props = instance.props
attrs = instance.attrs
},
})

render({
get fooBar() {
return 1
},
get bar() {
return 2
},
})
expect(props.fooBar).toEqual(1)
expect(attrs.bar).toEqual(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)

// 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({})
render({
get qux() {
return 5
},
})
expect(props.fooBar).toBeUndefined()
expect(props.barBaz).toBeUndefined()
// expect(props.qux).toEqual(5) // TODO: attrs
expect(attrs.qux).toEqual(5)
})

test.todo('stateful with setup', () => {
Expand All @@ -71,59 +96,78 @@ describe('component props (vapor)', () => {

test('functional with declaration', () => {
let props: any
// TODO: attrs
let attrs: any

const { component: Comp, render } = define((_props: any) => {
const instance = getCurrentInstance()!
props = instance.props
attrs = instance.attrs
return {}
})
Comp.props = ['foo']
Comp.render = (() => {}) as any

render({
get foo() {
return 1
},
get bar() {
return 2
},
})
expect(props.foo).toEqual(1)
expect(attrs.bar).toEqual(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({})
render({
get qux() {
return 5
},
})
expect(props.foo).toBeUndefined()
expect(attrs.qux).toEqual(5)
})

// FIXME:
test('functional without declaration', () => {
let props: any
// TODO: attrs
let attrs: any

const { component: Comp, render } = define((_props: any) => {
const { render } = define((_props: any, { attrs: _attrs }: any) => {
const instance = getCurrentInstance()!
props = instance.props
attrs = instance.attrs
return {}
})
Comp.props = undefined as any
Comp.render = (() => {}) as any

render({
get foo() {
return 1
},
})
expect(props.foo).toBeUndefined()
expect(props.foo).toEqual(1)
expect(attrs.foo).toEqual(1)

render({
get foo() {
return 2
},
})
expect(props.foo).toBeUndefined()
expect(props.foo).toEqual(2)
expect(attrs.foo).toEqual(2)
})

test('boolean casting', () => {
Expand Down Expand Up @@ -490,8 +534,34 @@ describe('component props (vapor)', () => {
})

// #5016
test.todo('handling attr with undefined value', () => {
// TODO: attrs
test('handling attr with undefined value', () => {
const { render, host } = define({
render() {
const instance = getCurrentInstance()!
const t0 = template('<div></div>')
const n0 = t0()
const n1 = children(n0, 0)
watchEffect(() => {
setText(
n1,
JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
)
})
return n0
},
})

let attrs: any = {
get foo() {
return undefined
},
}

render(attrs)

expect(host.innerHTML).toBe(
`<div>${JSON.stringify(attrs) + Object.keys(attrs)}</div>`,
)
})

// #6915
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface ComponentInternalInstance {

// state
props: Data
attrs: Data
setupState: Data
emit: EmitFn
emitted: Record<string, boolean> | null
Expand Down Expand Up @@ -179,6 +180,7 @@ export const createComponentInstance = (

// state
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
refs: EMPTY_OBJ,
metadata: new WeakMap(),
Expand Down
34 changes: 31 additions & 3 deletions packages/runtime-vapor/src/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type ComponentInternalInstance,
setCurrentInstance,
} from './component'
import { isEmitListener } from './componentEmits'

export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
Expand Down Expand Up @@ -74,10 +75,13 @@ export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: boolean,
) {
const props: Data = {}
const attrs: Data = {}

const [options, needCastKeys] = instance.propsOptions
let hasAttrsChanged = false
let rawCastValues: Data | undefined
if (rawProps) {
for (let key in rawProps) {
Expand All @@ -96,6 +100,7 @@ export function initProps(
get() {
return valueGetter()
},
enumerable: true,
})
} else {
// NOTE: must getter
Expand All @@ -105,10 +110,22 @@ export function initProps(
get() {
return valueGetter()
},
enumerable: true,
})
}
} else {
// TODO:
} 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
}
}
}
}
Expand Down Expand Up @@ -148,7 +165,18 @@ export function initProps(
validateProps(rawProps || {}, props, instance)
}

instance.props = shallowReactive(props)
if (isStateful) {
instance.props = shallowReactive(props)
} else {
if (instance.propsOptions === EMPTY_ARR) {
instance.props = attrs
} else {
instance.props = props
}
}
instance.attrs = attrs

return hasAttrsChanged
}

function resolvePropValue(
Expand Down
17 changes: 11 additions & 6 deletions packages/runtime-vapor/src/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { proxyRefs } from '@vue/reactivity'
import { type Data, invokeArrayFns, isArray, isObject } from '@vue/shared'
import {
type Data,
invokeArrayFns,
isArray,
isFunction,
isObject,
} from '@vue/shared'
import {
type Component,
type ComponentInternalInstance,
Expand Down Expand Up @@ -28,7 +34,7 @@ export function render(
container: string | ParentNode,
): ComponentInternalInstance {
const instance = createComponentInstance(comp, props)
initProps(instance, props)
initProps(instance, props, !isFunction(instance.component))
return mountComponent(instance, (container = normalizeContainer(container)))
}

Expand All @@ -46,11 +52,10 @@ export function mountComponent(

const reset = setCurrentInstance(instance)
const block = instance.scope.run(() => {
const { component, props, emit } = instance
const ctx = { expose: () => {}, emit }
const { component, props, emit, attrs } = instance
const ctx = { expose: () => {}, emit, attrs }

const setupFn =
typeof component === 'function' ? component : component.setup
const setupFn = isFunction(component) ? component : component.setup
const stateOrNode = setupFn && setupFn(props, ctx)

let block: Block | undefined
Expand Down
Loading