Skip to content

Commit d7fda0e

Browse files
authored
Merge pull request #48 from Giwayume/feat/setup
Implement setup() for consuming composables
2 parents d1ada43 + 0c98179 commit d7fda0e

File tree

12 files changed

+231
-3
lines changed

12 files changed

+231
-3
lines changed

docs/en/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- [Quick start](/en/quick-start/quick-start.md)
33
- Class component
44
- [Component](/en/class-component/component/component.md)
5+
- [Setup](/en/class-component/setup/setup.md)
56
- [Property](/en/class-component/property/property.md)
67
- [Method](/en/class-component/method/method.md)
78
- [Hooks](/en/class-component/hooks/hooks.md)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import ref from 'vue'
2+
import { Component, Vue, setup } from 'vue-facing-decorator'
3+
4+
@Component({
5+
setup(props, ctx) {
6+
const message = ref('hello world')
7+
return { message }
8+
}
9+
})
10+
class MyComponent extends Vue {
11+
private data = setup(() => 'hello world') // This setup() function will no longer work!
12+
13+
mounted() {
14+
console.log(this.message) // Undefined!
15+
}
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Component, Vue, setup } from 'vue-facing-decorator'
2+
import { useRouter } from 'vue-router'
3+
4+
@Component
5+
class MyComponent extends Vue {
6+
private router = setup(() => useRouter())
7+
8+
mounted() {
9+
console.log(this.router.getRoutes())
10+
}
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Usage
2+
3+
Use the `setup` function exported from `'vue-facing-decorator'` to inject [composables](https://vuejs.org/guide/reusability/composables.html) into your component's class as data.
4+
5+
[](./code-usage-base.ts ':include :type=code typescript')
6+
7+
## Options
8+
9+
You can also provide your own custom setup function as part of the component options, but be aware that if you do this, the `setup()` function from `'vue-facing-decorator'` will no longer work to inject data, and any properties you return from that setup will only be accessible in the component's template or render function.
10+
11+
[](./code-option-setup.ts ':include :type=code typescript')
12+

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"vue component decorator"
1717
],
1818
"scripts": {
19-
"test-build": "npm run test && npm run build",
19+
"test-build": "npm run build && npm run test",
2020
"test": "mocha -r ts-node/register test/test.ts",
2121
"build": "npm run build:cjs && npm run build:esm",
2222
"build:cjs": "./node_modules/.bin/tsc",

src/component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineComponent, type ComponentCustomOptions } from 'vue';
22
import { obtainSlot, getSuperSlot } from './utils'
3+
import { build as optionSetup } from './option/setup'
34
import { build as optionComputed } from './option/computed'
45
import { build as optionData } from './option/data'
56
import { build as optionMethodsAndHooks } from './option/methodsAndHooks'
@@ -10,6 +11,7 @@ import { build as optionInject } from './option/inject'
1011
import { build as optionEmit } from './option/emit'
1112
import { build as optionVModel } from './option/vmodel'
1213
import { build as optionAccessor } from './option/accessor'
14+
import type { SetupContext, RenderFunction } from 'vue';
1315
import type { OptionBuilder } from './optionBuilder'
1416
import type { VueCons } from './index'
1517
export type Cons = VueCons
@@ -18,6 +20,7 @@ export type Cons = VueCons
1820
function ComponentOption(cons: Cons, extend?: any) {
1921

2022
const optionBuilder: OptionBuilder = {}
23+
const setupData = optionSetup(cons, optionBuilder)
2124
optionVModel(cons, optionBuilder)
2225
optionComputed(cons, optionBuilder)//after VModel
2326
optionWatch(cons, optionBuilder)
@@ -28,9 +31,13 @@ function ComponentOption(cons: Cons, extend?: any) {
2831
optionMethodsAndHooks(cons, optionBuilder)//after Ref Computed
2932
optionAccessor(cons, optionBuilder)
3033
const raw = {
34+
setup: optionBuilder.setup,
3135
data() {
3236
delete optionBuilder.data
3337
optionData(cons, optionBuilder, this)
38+
if (optionBuilder.data && Object.keys(setupData).length > 0) {
39+
Object.assign(optionBuilder.data, setupData)
40+
}
3441
return optionBuilder.data ?? {}
3542
},
3643
methods: optionBuilder.methods,
@@ -46,6 +53,7 @@ function ComponentOption(cons: Cons, extend?: any) {
4653

4754
type ComponentOption = {
4855
name?: string
56+
setup?: (this: void, props: Readonly<any>, ctx: SetupContext<any>) => Promise<any> | any | RenderFunction | void,
4957
emits?: string[]
5058
provide?: Record<string, any> | Function
5159
components?: Record<string, any>

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { Component, ComponentBase } from './component'
2+
export { setup } from './option/setup'
23
export { decorator as Ref } from './option/ref'
34
export { decorator as Watch } from './option/watch'
45
export { decorator as Prop } from './option/props'

src/option/setup.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { Ref, SetupContext, ShallowUnwrapRef } from 'vue';
2+
import type { Cons } from '../component'
3+
import type { OptionBuilder } from '../optionBuilder'
4+
import { getValidNames } from '../utils'
5+
6+
const isPromise = (v: any) => typeof v === 'object' && typeof v.then === 'function'
7+
8+
export function build(cons: Cons, optionBuilder: OptionBuilder): Record<string, any> {
9+
const setupData: Record<string, any> = {};
10+
11+
optionBuilder.setup = (props, ctx) => {
12+
const sample = new cons(optionBuilder, ctx)
13+
14+
let promise: Promise<any> | null = null;
15+
16+
const names = getValidNames(sample, (des) => {
17+
return !!des.enumerable
18+
})
19+
for (const name of names) {
20+
const value = (sample as any)[name]
21+
const keys = Object.keys(value ?? {})
22+
if (keys.length === 1 && keys[0] === '__vueFacingDecoratorDeferredSetup') {
23+
const setupState = value.__vueFacingDecoratorDeferredSetup(props, ctx)
24+
if (isPromise(setupState)) {
25+
if (!promise) {
26+
promise = Promise.resolve(setupState)
27+
}
28+
promise = promise.then(() => {
29+
return setupState.then((value: any) => {
30+
setupData[name] = value
31+
})
32+
})
33+
} else {
34+
setupData[name] = setupState
35+
}
36+
}
37+
}
38+
return promise ?? {};
39+
}
40+
return setupData;
41+
}
42+
43+
export type UnwrapSetupValue<T> = T extends Ref<infer R>
44+
? R
45+
: ShallowUnwrapRef<T>
46+
47+
export type UnwrapPromise<T> = T extends Promise<infer R> ? R : T
48+
49+
export function setup<R = any>(setupFn: (this: void, props: Readonly<any>, ctx: SetupContext<any>) => R): UnwrapSetupValue<UnwrapPromise<R>> {
50+
return {
51+
__vueFacingDecoratorDeferredSetup: setupFn
52+
} as UnwrapSetupValue<UnwrapPromise<R>>
53+
}

src/optionBuilder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import type { RenderFunction, SetupContext } from 'vue';
12
import type { WatchConfig } from './option/watch'
23
import type { PropsConfig } from './option/props'
34
import type { InjectConfig } from './option/inject'
45
export interface OptionBuilder {
56
name?: string
7+
setup?: (this: void, props: Readonly<any>, ctx: SetupContext<any>) => Promise<any> | any | RenderFunction | void
68
data?: Record<string, any>
79
methods?: Record<string, Function>
810
hooks?: Record<string, Function>

test/option/setup.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
2+
import { inject } from 'vue'
3+
import { mount } from '@vue/test-utils'
4+
import { expect } from 'chai'
5+
import { mountSuspense } from '../utils'
6+
import 'mocha'
7+
import { Component, Base, Prop, setup } from '../../dist'
8+
9+
const SETUP_AXIOM = 'setup is working to allow composition API usage'
10+
const DATA_AXIOM = 'data is injected into the template'
11+
const injectionKey = Symbol('injection test key')
12+
13+
function useInjectedValue() {
14+
return inject(injectionKey)
15+
}
16+
17+
@Component({
18+
render() { return [] }
19+
})
20+
export class SyncComp extends Base {
21+
22+
injectedValue = setup(() => useInjectedValue())
23+
24+
}
25+
26+
const SyncCompContext = SyncComp as any
27+
28+
@Component({
29+
render() { return [] }
30+
})
31+
export class AsyncComp extends Base {
32+
33+
injectedValue = setup(() => {
34+
const value = useInjectedValue()
35+
return new Promise((resolve) => {
36+
setTimeout(() => {
37+
resolve(value)
38+
}, 1)
39+
})
40+
})
41+
42+
}
43+
44+
const AsyncCompContext = AsyncComp as any
45+
46+
@Component({
47+
setup() {
48+
const injectedValue = useInjectedValue()
49+
return { injectedValue }
50+
},
51+
template: '{{ injectedValue }} {{ dataValue }}'
52+
})
53+
export class SetupComp extends Base {
54+
dataValue = DATA_AXIOM
55+
}
56+
57+
const SetupCompContext = SetupComp as any
58+
59+
describe('setup function', () => {
60+
describe('synchronous setup', () => {
61+
const wrapper = mount(SyncCompContext, {
62+
global: {
63+
provide: {
64+
[injectionKey]: SETUP_AXIOM
65+
}
66+
}
67+
})
68+
const vm = wrapper.vm
69+
it('injects the value provided to the component via composition API', () => {
70+
expect(vm.injectedValue).to.equal(SETUP_AXIOM)
71+
})
72+
})
73+
74+
describe('asynchronous setup', () => {
75+
it('injects the value provided to the component via composition API', async () => {
76+
const wrapper = await mountSuspense(AsyncCompContext, {
77+
global: {
78+
provide: {
79+
[injectionKey]: SETUP_AXIOM
80+
}
81+
}
82+
})
83+
const vm = wrapper.findComponent(AsyncCompContext).vm
84+
expect(vm.injectedValue).to.equal(SETUP_AXIOM)
85+
})
86+
})
87+
})
88+
89+
describe('setup option', () => {
90+
const wrapper = mount(SetupCompContext, {
91+
global: {
92+
provide: {
93+
[injectionKey]: SETUP_AXIOM
94+
}
95+
}
96+
})
97+
it('can inject variables into the template', () => {
98+
expect(wrapper.text()).to.contain(SETUP_AXIOM)
99+
expect(wrapper.text()).to.contain(DATA_AXIOM)
100+
})
101+
})
102+
103+
export default {}

0 commit comments

Comments
 (0)