Skip to content

Commit

Permalink
feat(createReusableTemplate): new function (#2961)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Apr 13, 2023
1 parent 976d835 commit bd53cc3
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 0 deletions.
198 changes: 198 additions & 0 deletions 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
<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)

83 changes: 83 additions & 0 deletions 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')
})
})
57 changes: 57 additions & 0 deletions 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<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,
)
}
1 change: 1 addition & 0 deletions 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'
Expand Down

0 comments on commit bd53cc3

Please sign in to comment.