diff --git a/packages/core/createReusableTemplate/index.md b/packages/core/createReusableTemplate/index.md new file mode 100644 index 00000000000..d6b3b622a53 --- /dev/null +++ b/packages/core/createReusableTemplate/index.md @@ -0,0 +1,198 @@ +--- +category: Component +outline: deep +--- + +# createReusableTemplate + +Define and reuse template inside the component scope. + +## Motivation + +It's common to have the need to reuse some part of the template. For example: + +```html + +``` + +We'd like to reuse our code as much as possible. So normally we might need to extract those duplicated parts into a component. However, in a separated component you lose the ability to access the local bindings. Defining props and emits for them can be tedious sometime. + +So this function is made to provide a way for defining and reusing templates inside the component scope. + +## Usage + +In the previous example, we could refactor it to: + +```html + + + +``` + +- `` will register the template and renders nothing. +- `` will render the template provided by ``. +- `` must be used before ``. + +> **Note**: It's recommanded to extract as separate components whenever possible. Abusing this function might lead to bad practices for your codebase. + +### Passing Data + +You can also pass data to the template using slots: + +- Use `v-slot="..."` to access the data on `` +- Directly bind the data on `` to pass them to the template + +```html + + + +``` + +### TypeScript Support + +`createReusableTemplate` accepts a generic type to provide type support for the data passed to the template: + +```html + + + +``` + +Optionally, if you are not a fan of array destructuring, the following usages are also legal: + +```html + + + +``` + +```html + + + +``` + +::: info +Dot notation is not supported in Vue 2. +::: + +### Passing Slots + +It's also possible to pass slots back from ``. You can access the slots on `` from `$slots`: + +```html + + + +``` + +::: info +Passing slots does not work in Vue 2. +::: + +## References + +This function is migrated from [vue-reuse-template](https://github.com/antfu/vue-reuse-template). + +Existing Vue discussions/issues about reusing template: + +- [Discussion on Reusing Templates](https://github.com/vuejs/core/discussions/6898) + +Alternative Approaches: + +- [Vue Macros - `namedTemplate`](https://vue-macros.sxzz.moe/features/named-template.html) +- [`unplugin-@vueuse/core`](https://github.com/liulinboyi/unplugin-@vueuse/core) + diff --git a/packages/core/createReusableTemplate/index.test.ts b/packages/core/createReusableTemplate/index.test.ts new file mode 100644 index 00000000000..ea6593cd4f6 --- /dev/null +++ b/packages/core/createReusableTemplate/index.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import type { Slot } from 'vue-demi' +import { Fragment, defineComponent, h, isVue2, renderSlot } from 'vue-demi' +import { createReusableTemplate } from '.' + +describe.skipIf(isVue2)('createReusableTemplate', () => { + it('should work', () => { + const [DefineFoo, ReuseFoo] = createReusableTemplate() + const [DefineBar, ReuseBar] = createReusableTemplate() + const Zig = createReusableTemplate() + + const wrapper = mount({ + render() { + return h(Fragment, null, [ + h(DefineFoo, () => ['Foo']), + h(ReuseFoo), + + h(DefineBar, () => ['Bar']), + h(Zig.define, () => ['Zig']), + h(ReuseFoo), + h(ReuseBar), + h(Zig.reuse), + ]) + }, + }) + + expect(wrapper.text()).toBe('FooFooBarZig') + }) + + it('nested', () => { + const CompA = defineComponent((_, { slots }) => { + return () => renderSlot(slots, 'default') + }) + + const [DefineFoo, ReuseFoo] = createReusableTemplate() + + const wrapper = mount({ + render() { + return h(Fragment, null, [ + h(DefineFoo, () => ['Foo']), + h(CompA, () => h(ReuseFoo)), + ]) + }, + }) + + expect(wrapper.text()).toBe('Foo') + }) + + it('props', () => { + const [DefineFoo, ReuseFoo] = createReusableTemplate<{ msg: string }>() + + const wrapper = mount({ + render() { + return h(Fragment, null, [ + h(DefineFoo, ({ $slots, ...args }: any) => h('pre', JSON.stringify(args))), + + h(ReuseFoo, { msg: 'Foo' }), + h(ReuseFoo, { msg: 'Bar' }), + ]) + }, + }) + + expect(wrapper.text()).toBe('{"msg":"Foo"}{"msg":"Bar"}') + }) + + it('slots', () => { + const [DefineFoo, ReuseFoo] = createReusableTemplate<{ msg: string }, { default: Slot }>() + + const wrapper = mount({ + render() { + return h(Fragment, null, [ + h(DefineFoo, (args: any) => args.$slots.default?.()), + + h(ReuseFoo, () => h('div', 'Goodbye')), + h(ReuseFoo, () => h('div', 'Hi')), + ]) + }, + }) + + expect(wrapper.text()).toBe('GoodbyeHi') + }) +}) diff --git a/packages/core/createReusableTemplate/index.ts b/packages/core/createReusableTemplate/index.ts new file mode 100644 index 00000000000..ad17616e252 --- /dev/null +++ b/packages/core/createReusableTemplate/index.ts @@ -0,0 +1,57 @@ +import type { DefineComponent, Slot } from 'vue-demi' +import { defineComponent } from 'vue-demi' +import { __onlyVue27Plus, makeDestructurable } from '@vueuse/shared' + +export type DefineTemplateComponent< + Bindings extends object, + Slots extends Record, + Props = {}, +> = DefineComponent & { + new(): { $slots: { default(_: Bindings & { $slots: Slots }): any } } +} + +export type ReuseTemplateComponent< + Bindings extends object, + Slots extends Record, +> = DefineComponent & { + new(): { $slots: Slots } +} + +/** + * This function creates `define` and `reuse` components in pair, + * It also allow to pass a generic to bind with type. + * + * @see https://vueuse.org/createReusableTemplate + */ +export function createReusableTemplate< + Bindings extends object, + Slots extends Record = Record, +>(name?: string) { + __onlyVue27Plus() + + let render: Slot | undefined + + const define = defineComponent({ + setup(_, { slots }) { + return () => { + render = slots.default + } + }, + }) as DefineTemplateComponent + + const reuse = defineComponent({ + inheritAttrs: false, + setup(_, { attrs, slots }) { + return () => { + if (!render && process.env.NODE_ENV !== 'production') + throw new Error(`[VueUse] Failed to find the definition of template${name ? ` "${name}"` : ''}`) + return render?.({ ...attrs, $slots: slots }) + } + }, + }) as ReuseTemplateComponent + + return makeDestructurable( + { define, reuse }, + [define, reuse] as const, + ) +} diff --git a/packages/core/index.ts b/packages/core/index.ts index de779fcd90e..c0e59335e85 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -1,5 +1,6 @@ export * from './computedAsync' export * from './computedInject' +export * from './createReusableTemplate' export * from './createUnrefFn' export * from './onClickOutside' export * from './onKeyStroke'