Преглед изворни кода

feat(runtime-vapor): component attrs (#124)

ubugeeei пре 2 година
родитељ
комит
52311fa7ae

+ 2 - 8
packages/runtime-vapor/__tests__/_utils.ts

@@ -1,4 +1,4 @@
-import { type Data, isFunction } from '@vue/shared'
+import type { Data } from '@vue/shared'
 import {
   type ComponentInternalInstance,
   type ObjectComponent,
@@ -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 = {},

+ 84 - 14
packages/runtime-vapor/__tests__/componentProps.spec.ts

@@ -22,13 +22,14 @@ 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
       },
     })
 
@@ -36,33 +37,57 @@ describe('component props (vapor)', () => {
       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', () => {
@@ -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', () => {
@@ -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

+ 2 - 0
packages/runtime-vapor/src/component.ts

@@ -58,6 +58,7 @@ export interface ComponentInternalInstance {
 
   // state
   props: Data
+  attrs: Data
   setupState: Data
   emit: EmitFn
   emitted: Record<string, boolean> | null
@@ -179,6 +180,7 @@ export const createComponentInstance = (
 
     // state
     props: EMPTY_OBJ,
+    attrs: EMPTY_OBJ,
     setupState: EMPTY_OBJ,
     refs: EMPTY_OBJ,
     metadata: new WeakMap(),

+ 31 - 3
packages/runtime-vapor/src/componentProps.ts

@@ -19,6 +19,7 @@ import {
   type ComponentInternalInstance,
   setCurrentInstance,
 } from './component'
+import { isEmitListener } from './componentEmits'
 
 export type ComponentPropsOptions<P = Data> =
   | ComponentObjectPropsOptions<P>
@@ -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) {
@@ -96,6 +100,7 @@ export function initProps(
             get() {
               return valueGetter()
             },
+            enumerable: true,
           })
         } else {
           // NOTE: must getter
@@ -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
+        }
       }
     }
   }
@@ -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(

+ 11 - 6
packages/runtime-vapor/src/render.ts

@@ -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,
@@ -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)))
 }
 
@@ -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