Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(createReusableTemplate): new function (#2961)
- Loading branch information
Showing
4 changed files
with
339 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<template> | ||
<dialog v-if="showInDialog"> | ||
<!-- something complex --> | ||
</dialog> | ||
<div v-else> | ||
<!-- something complex --> | ||
</div> | ||
</template> | ||
``` | ||
|
||
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 | ||
<script setup> | ||
import { createReusableTemplate } from '@vueuse/core' | ||
const [DefineTemplate, ReuseTemplate] = createReusableTemplate() | ||
</script> | ||
|
||
<template> | ||
<DefineTemplate> | ||
<!-- something complex --> | ||
</DefineTemplate> | ||
|
||
<dialog v-if="showInDialog"> | ||
<ReuseTemplate /> | ||
</dialog> | ||
<div v-else> | ||
<ReuseTemplate /> | ||
</div> | ||
</template> | ||
``` | ||
|
||
- `<DefineTemplate>` will register the template and renders nothing. | ||
- `<ReuseTemplate>` will render the template provided by `<DefineTemplate>`. | ||
- `<DefineTemplate>` must be used before `<ReuseTemplate>`. | ||
|
||
> **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 `<DefineTemplate>` | ||
- Directly bind the data on `<ReuseTemplate>` to pass them to the template | ||
|
||
```html | ||
<script setup> | ||
import { createReusableTemplate} from '@vueuse/core' | ||
const [DefineTemplate, ReuseTemplate] = createReusableTemplate() | ||
</script> | ||
|
||
<template> | ||
<DefineTemplate v-slot="{ data, msg, anything }"> | ||
<div>{{ data }} passed from usage</div> | ||
</DefineTemplate> | ||
|
||
<ReuseTemplate :data="data" msg="The first usage" /> | ||
<ReuseTemplate :data="anotherData" msg="The second usage" /> | ||
<ReuseTemplate v-bind="{ data: something, msg: 'The third' }" /> | ||
</template> | ||
``` | ||
|
||
### TypeScript Support | ||
|
||
`createReusableTemplate` accepts a generic type to provide type support for the data passed to the template: | ||
|
||
```html | ||
<script setup lang="ts"> | ||
import { createReusableTemplate } from '@vueuse/core' | ||
// Comes with pair of `DefineTemplate` and `ReuseTemplate` | ||
const [DefineFoo, ReuseFoo] = createReusableTemplate<{ msg: string }>() | ||
// You can create multiple reusable templates | ||
const [DefineBar, ReuseBar] = createReusableTemplate<{ items: string[] }>() | ||
</script> | ||
|
||
<template> | ||
<DefineFoo v-slot="{ msg }"> | ||
<!-- `msg` is typed as `string` --> | ||
<div>Hello {{ msg.toUpperCase() }}</div> | ||
</DefineFoo> | ||
|
||
<ReuseFoo msg="World" /> | ||
|
||
<!-- @ts-expect-error Type Error! --> | ||
<ReuseFoo :msg="1" /> | ||
</template> | ||
``` | ||
|
||
Optionally, if you are not a fan of array destructuring, the following usages are also legal: | ||
|
||
```html | ||
<script setup lang="ts"> | ||
import { createReusableTemplate } from '@vueuse/core' | ||
const { | ||
define: DefineFoo, | ||
reuse: ReuseFoo, | ||
} = createReusableTemplate<{ msg: string }>() | ||
</script> | ||
|
||
<template> | ||
<DefineFoo v-slot="{ msg }"> | ||
<div>Hello {{ msg.toUpperCase() }}</div> | ||
</DefineFoo> | ||
|
||
<ReuseFoo msg="World" /> | ||
</template> | ||
``` | ||
|
||
```html | ||
<script setup lang="ts"> | ||
import { createReusableTemplate } from '@vueuse/core' | ||
const TemplateFoo = createReusableTemplate<{ msg: string }>() | ||
</script> | ||
|
||
<template> | ||
<TemplateFoo.define v-slot="{ msg }"> | ||
<div>Hello {{ msg.toUpperCase() }}</div> | ||
</TemplateFoo.define> | ||
|
||
<TemplateFoo.reuse msg="World" /> | ||
</template> | ||
``` | ||
|
||
::: info | ||
Dot notation is not supported in Vue 2. | ||
::: | ||
|
||
### Passing Slots | ||
|
||
It's also possible to pass slots back from `<ReuseTemplate>`. You can access the slots on `<DefineTemplate>` from `$slots`: | ||
|
||
```html | ||
<script setup> | ||
import { createReusableTemplate } from '@vueuse/core' | ||
const [DefineTemplate, ReuseTemplate] = createReusableTemplate() | ||
</script> | ||
|
||
<template> | ||
<DefineTemplate v-slot="{ $slots, otherProp }"> | ||
<div some-layout> | ||
<!-- To render the slot --> | ||
<component :is="$slots.default" /> | ||
</div> | ||
</DefineTemplate> | ||
|
||
<ReuseTemplate> | ||
<div>Some content</div> | ||
</ReuseTemplate> | ||
<ReuseTemplate> | ||
<div>Another content</div> | ||
</ReuseTemplate> | ||
</template> | ||
``` | ||
|
||
::: 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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, Slot | undefined>, | ||
Props = {}, | ||
> = DefineComponent<Props> & { | ||
new(): { $slots: { default(_: Bindings & { $slots: Slots }): any } } | ||
} | ||
|
||
export type ReuseTemplateComponent< | ||
Bindings extends object, | ||
Slots extends Record<string, Slot | undefined>, | ||
> = DefineComponent<Bindings> & { | ||
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<string, Slot | undefined> = Record<string, Slot | undefined>, | ||
>(name?: string) { | ||
__onlyVue27Plus() | ||
|
||
let render: Slot | undefined | ||
|
||
const define = defineComponent({ | ||
setup(_, { slots }) { | ||
return () => { | ||
render = slots.default | ||
} | ||
}, | ||
}) as DefineTemplateComponent<Bindings, Slots> | ||
|
||
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<Bindings, Slots> | ||
|
||
return makeDestructurable( | ||
{ define, reuse }, | ||
[define, reuse] as const, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters