Skip to content

Commit 131696d

Browse files
antfuinnocenzi
andauthored
feat(createTemplatePromise): new function (#2957)
Co-authored-by: Enzo Innocenzi <enzo@innocenzi.dev>
1 parent 2a912d8 commit 131696d

File tree

17 files changed

+455
-24
lines changed

17 files changed

+455
-24
lines changed

packages/.vitepress/theme/styles/demo.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
position: relative;
66
margin-bottom: 10px;
77
border-radius: 8px;
8+
z-index: 50;
89
transition: background-color 0.5s;
910
}
1011

@@ -182,6 +183,11 @@
182183
border-style: dashed;
183184
padding: 1rem;
184185
}
186+
187+
dialog {
188+
z-index: 1000000;
189+
background: var(--vp-c-bg);
190+
}
185191
}
186192

187193

packages/.vitepress/theme/styles/vars.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
--vp-c-disabled-bg: rgba(125,125,125,0.2);
1515
/* fix contrast on gray cards: used by --vp-c-text-2 */
16+
--vp-c-text-light-2: rgba(56 56 56 / 70%);
1617
--vp-c-text-dark-2: rgba(56 56 56 / 70%);
18+
19+
--vp-custom-block-tip-bg: transparent;
1720
}
1821

1922
.dark {

packages/core/createReusableTemplate/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ outline: deep
77

88
Define and reuse template inside the component scope.
99

10+
1011
## Motivation
1112

1213
It's common to have the need to reuse some part of the template. For example:
@@ -179,7 +180,7 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
179180
</template>
180181
```
181182

182-
::: info
183+
::: warning
183184
Passing slots does not work in Vue 2.
184185
:::
185186

@@ -195,4 +196,3 @@ Alternative Approaches:
195196

196197
- [Vue Macros - `namedTemplate`](https://vue-macros.sxzz.moe/features/named-template.html)
197198
- [`unplugin-@vueuse/core`](https://github.com/liulinboyi/unplugin-@vueuse/core)
198-

packages/core/createReusableTemplate/index.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { DefineComponent, Slot } from 'vue-demi'
2-
import { defineComponent } from 'vue-demi'
3-
import { __onlyVue27Plus, makeDestructurable } from '@vueuse/shared'
2+
import { defineComponent, isVue3, version } from 'vue-demi'
3+
import { makeDestructurable } from '@vueuse/shared'
44

55
export type DefineTemplateComponent<
66
Bindings extends object,
77
Slots extends Record<string, Slot | undefined>,
8-
Props = {},
9-
> = DefineComponent<Props> & {
8+
> = DefineComponent<{}> & {
109
new(): { $slots: { default(_: Bindings & { $slots: Slots }): any } }
1110
}
1211

@@ -17,6 +16,17 @@ export type ReuseTemplateComponent<
1716
new(): { $slots: Slots }
1817
}
1918

19+
export type ReusableTemplatePair<
20+
Bindings extends object,
21+
Slots extends Record<string, Slot | undefined>,
22+
> = [
23+
DefineTemplateComponent<Bindings, Slots>,
24+
ReuseTemplateComponent<Bindings, Slots>,
25+
] & {
26+
define: DefineTemplateComponent<Bindings, Slots>
27+
reuse: ReuseTemplateComponent<Bindings, Slots>
28+
}
29+
2030
/**
2131
* This function creates `define` and `reuse` components in pair,
2232
* It also allow to pass a generic to bind with type.
@@ -26,8 +36,14 @@ export type ReuseTemplateComponent<
2636
export function createReusableTemplate<
2737
Bindings extends object,
2838
Slots extends Record<string, Slot | undefined> = Record<string, Slot | undefined>,
29-
>(name?: string) {
30-
__onlyVue27Plus()
39+
>(): ReusableTemplatePair<Bindings, Slots> {
40+
// compatibility: Vue 2.7 or above
41+
if (!isVue3 && !version.startsWith('2.7.')) {
42+
if (process.env.NODE_ENV !== 'production')
43+
throw new Error('[VueUse] createReusableTemplate only works in Vue 2.7 or above.')
44+
// @ts-expect-error incompatible
45+
return
46+
}
3147

3248
let render: Slot | undefined
3349

@@ -44,14 +60,14 @@ export function createReusableTemplate<
4460
setup(_, { attrs, slots }) {
4561
return () => {
4662
if (!render && process.env.NODE_ENV !== 'production')
47-
throw new Error(`[VueUse] Failed to find the definition of template${name ? ` "${name}"` : ''}`)
63+
throw new Error('[VueUse] Failed to find the definition of reusable template')
4864
return render?.({ ...attrs, $slots: slots })
4965
}
5066
},
5167
}) as ReuseTemplateComponent<Bindings, Slots>
5268

5369
return makeDestructurable(
5470
{ define, reuse },
55-
[define, reuse] as const,
56-
)
71+
[define, reuse],
72+
) as any
5773
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<script setup lang="ts">
2+
import { createTemplatePromise } from '.'
3+
4+
type DialogResult = 'ok' | 'cancel'
5+
6+
const TemplatePromise = createTemplatePromise<DialogResult, [string]>({
7+
transition: {
8+
name: 'fade',
9+
appear: true,
10+
},
11+
})
12+
13+
async function open(idx: number) {
14+
console.log(idx, 'Before')
15+
const result = await TemplatePromise.start(`Hello ${idx}`)
16+
console.log(idx, 'After', result)
17+
}
18+
19+
function asyncFn() {
20+
return new Promise<DialogResult>((resolve) => {
21+
setTimeout(() => {
22+
resolve('ok')
23+
}, 1000)
24+
})
25+
}
26+
</script>
27+
28+
<template>
29+
<div class="flex gap-2">
30+
<button @click="open(1)">
31+
Open 1
32+
</button>
33+
<button @click="open(2)">
34+
Open 2
35+
</button>
36+
<button @click="open(1); open(2)">
37+
Open 1 & 2
38+
</button>
39+
</div>
40+
<TemplatePromise v-slot="{ resolve, args, isResolving }">
41+
<div class="fixed inset-0 bg-black/10 flex items-center">
42+
<dialog open class="border-gray/10 shadow rounded ma">
43+
<div>Dialog {{ args[0] }}</div>
44+
<p>Open console to see logs</p>
45+
<div class="flex gap-2 justify-end">
46+
<button class="w-35" @click="resolve('cancel')">
47+
Cancel
48+
</button>
49+
<button class="w-35" :disabled="isResolving" @click="resolve(asyncFn())">
50+
{{ isResolving ? 'Confirming...' : 'OK' }}
51+
</button>
52+
</div>
53+
</dialog>
54+
</div>
55+
</TemplatePromise>
56+
</template>
57+
58+
<style>
59+
.fade-enter-active,
60+
.fade-leave-active {
61+
transition: opacity 0.5s ease;
62+
}
63+
64+
.fade-enter-from,
65+
.fade-leave-to {
66+
opacity: 0;
67+
}
68+
</style>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
category: Component
3+
outline: deep
4+
---
5+
6+
# createTemplatePromise
7+
8+
Template as Promise. Useful for constructing custom Dialogs, Modals, Toasts, etc.
9+
10+
## Usage
11+
12+
```html
13+
<script setup lang="ts">
14+
import { createTemplatePromise } from '@vueuse/core'
15+
16+
const TemplatePromise = createTemplatePromise<ReturnType>()
17+
18+
async function open() {
19+
const result = await TemplatePromise.start()
20+
// button is clicked, result is 'ok'
21+
}
22+
</script>
23+
24+
<template>
25+
<TemplatePromise v-slot="{ promise, resolve, reject, args }">
26+
<!-- your UI -->
27+
<button @click="resolve('ok')">OK</button>
28+
</TemplatePromise>
29+
</template>
30+
```
31+
32+
::: tip
33+
This function is migrated from [vue-template-promise](https://github.com/antfu/vue-template-promise)
34+
:::
35+
36+
## Features
37+
38+
- **Programmatic** - call your UI as a promise
39+
- **Template** - use Vue template to render, not a new DSL
40+
- **TypeScript** - full type safety via generic type
41+
- **Renderless** - you take full control of the UI
42+
- **Transition** - use support Vue transition
43+
44+
## Usage
45+
46+
`createTemplatePromise` returns a **Vue Component** that you can directly use in your template with `<script setup>`
47+
48+
```ts
49+
import { createTemplatePromise } from '@vueuse/core'
50+
51+
const TemplatePromise = createTemplatePromise()
52+
const MyPromise = createTemplatePromise<boolean>() // with generic type
53+
```
54+
55+
In template, use `v-slot` to access the promise and resolve functions.
56+
57+
```html
58+
<template>
59+
<TemplatePromise v-slot="{ promise, resolve, reject, args }">
60+
<!-- you can have anything -->
61+
<button @click="resolve('ok')">OK</button>
62+
</TemplatePromise>
63+
<MyPromise v-slot="{ promise, resolve, reject, args }">
64+
<!-- another one -->
65+
</MyPromise>
66+
</template>
67+
```
68+
69+
The slot will not be rendered initially (similar to `v-if="false"`), until you call the `start` method from the component.
70+
71+
```ts
72+
const result = await TemplatePromise.start()
73+
```
74+
75+
Once `resolve` or `reject` is called in the template, the promise will be resolved or rejected, returning the value you passed in. Once resolved, the slot will be removed automatically.
76+
77+
### Passing Arguments
78+
79+
You can pass arguments to the `start` with arguments.
80+
81+
```ts
82+
import { createTemplatePromise } from '@vueuse/core'
83+
84+
const TemplatePromise = createTemplatePromise<boolean, [string, number]>()
85+
```
86+
87+
```ts
88+
const result = await TemplatePromise.start('hello', 123) // Pr
89+
```
90+
91+
And in the template slot, you can access the arguments via `args` property.
92+
93+
```html
94+
<template>
95+
<TemplatePromise v-slot="{ args, resolve }">
96+
<div>{{ args[0] }}</div> <!-- hello -->
97+
<div>{{ args[1] }}</div> <!-- 123 -->
98+
<button @click="resolve(true)">OK</button>
99+
</TemplatePromise>
100+
</template>
101+
```
102+
103+
### Transition
104+
105+
You can use transition to animate the slot.
106+
107+
```html
108+
<script setup lang="ts">
109+
const TemplatePromise = createTemplatePromise<ReturnType>({
110+
transition: {
111+
name: 'fade',
112+
appear: true,
113+
},
114+
})
115+
</script>
116+
117+
<template>
118+
<TemplatePromise v-slot="{ resolve }">
119+
<!-- your UI -->
120+
<button @click="resolve('ok')">OK</button>
121+
</TemplatePromise>
122+
</template>
123+
124+
<style scoped>
125+
.fade-enter-active, .fade-leave-active {
126+
transition: opacity .5s;
127+
}
128+
.fade-enter, .fade-leave-to {
129+
opacity: 0;
130+
}
131+
</style>
132+
```
133+
134+
Learn more about [Vue Transition](https://v3.vuejs.org/guide/transitions-overview.html).
135+
136+
## Motivation
137+
138+
The common approach to call a dialog or a model programmatically would be like this:
139+
140+
```ts
141+
const dialog = useDialog()
142+
const result = await dialog.open({
143+
title: 'Hello',
144+
content: 'World',
145+
})
146+
```
147+
148+
This would work by sending these information to the top-level component and let it render the dialog. However, it limits the flexibility you could express in the UI. For example, you could want the title to be red, or have extra buttons, etc. You would end up with a lot of options like:
149+
150+
```ts
151+
const result = await dialog.open({
152+
title: 'Hello',
153+
titleClass: 'text-red',
154+
content: 'World',
155+
contentClass: 'text-blue text-sm',
156+
buttons: [
157+
{ text: 'OK', class: 'bg-red', onClick: () => {} },
158+
{ text: 'Cancel', class: 'bg-blue', onClick: () => {} },
159+
],
160+
// ...
161+
})
162+
```
163+
164+
Even this is not flexible enough. If you want more, you might end up with manual render function.
165+
166+
```ts
167+
const result = await dialog.open({
168+
title: 'Hello',
169+
contentSlot: () => h(MyComponent, { content }),
170+
})
171+
```
172+
173+
This is like reinventing a new DSL in the script to express the UI template.
174+
175+
So this function allows **expressing the UI in templates instead of scripts**, where it is supposed to be, while still being able to be manipulated programmatically.

0 commit comments

Comments
 (0)