Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(createReusableTemplate): new function #2961

Merged
merged 3 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
198 changes: 198 additions & 0 deletions packages/core/createReusableTemplate/index.md
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)

83 changes: 83 additions & 0 deletions packages/core/createReusableTemplate/index.test.ts
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')
})
})
57 changes: 57 additions & 0 deletions packages/core/createReusableTemplate/index.ts
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,
)
}
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './computedAsync'
export * from './computedInject'
export * from './createReusableTemplate'
export * from './createUnrefFn'
export * from './onClickOutside'
export * from './onKeyStroke'
Expand Down