Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(createTemplatePromise): new function (#2957)
Co-authored-by: Enzo Innocenzi <enzo@innocenzi.dev>
- Loading branch information
Showing
17 changed files
with
455 additions
and
24 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
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
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
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
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,68 @@ | ||
<script setup lang="ts"> | ||
import { createTemplatePromise } from '.' | ||
type DialogResult = 'ok' | 'cancel' | ||
const TemplatePromise = createTemplatePromise<DialogResult, [string]>({ | ||
transition: { | ||
name: 'fade', | ||
appear: true, | ||
}, | ||
}) | ||
async function open(idx: number) { | ||
console.log(idx, 'Before') | ||
const result = await TemplatePromise.start(`Hello ${idx}`) | ||
console.log(idx, 'After', result) | ||
} | ||
function asyncFn() { | ||
return new Promise<DialogResult>((resolve) => { | ||
setTimeout(() => { | ||
resolve('ok') | ||
}, 1000) | ||
}) | ||
} | ||
</script> | ||
|
||
<template> | ||
<div class="flex gap-2"> | ||
<button @click="open(1)"> | ||
Open 1 | ||
</button> | ||
<button @click="open(2)"> | ||
Open 2 | ||
</button> | ||
<button @click="open(1); open(2)"> | ||
Open 1 & 2 | ||
</button> | ||
</div> | ||
<TemplatePromise v-slot="{ resolve, args, isResolving }"> | ||
<div class="fixed inset-0 bg-black/10 flex items-center"> | ||
<dialog open class="border-gray/10 shadow rounded ma"> | ||
<div>Dialog {{ args[0] }}</div> | ||
<p>Open console to see logs</p> | ||
<div class="flex gap-2 justify-end"> | ||
<button class="w-35" @click="resolve('cancel')"> | ||
Cancel | ||
</button> | ||
<button class="w-35" :disabled="isResolving" @click="resolve(asyncFn())"> | ||
{{ isResolving ? 'Confirming...' : 'OK' }} | ||
</button> | ||
</div> | ||
</dialog> | ||
</div> | ||
</TemplatePromise> | ||
</template> | ||
|
||
<style> | ||
.fade-enter-active, | ||
.fade-leave-active { | ||
transition: opacity 0.5s ease; | ||
} | ||
.fade-enter-from, | ||
.fade-leave-to { | ||
opacity: 0; | ||
} | ||
</style> |
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,175 @@ | ||
--- | ||
category: Component | ||
outline: deep | ||
--- | ||
|
||
# createTemplatePromise | ||
|
||
Template as Promise. Useful for constructing custom Dialogs, Modals, Toasts, etc. | ||
|
||
## Usage | ||
|
||
```html | ||
<script setup lang="ts"> | ||
import { createTemplatePromise } from '@vueuse/core' | ||
const TemplatePromise = createTemplatePromise<ReturnType>() | ||
async function open() { | ||
const result = await TemplatePromise.start() | ||
// button is clicked, result is 'ok' | ||
} | ||
</script> | ||
|
||
<template> | ||
<TemplatePromise v-slot="{ promise, resolve, reject, args }"> | ||
<!-- your UI --> | ||
<button @click="resolve('ok')">OK</button> | ||
</TemplatePromise> | ||
</template> | ||
``` | ||
|
||
::: tip | ||
This function is migrated from [vue-template-promise](https://github.com/antfu/vue-template-promise) | ||
::: | ||
|
||
## Features | ||
|
||
- **Programmatic** - call your UI as a promise | ||
- **Template** - use Vue template to render, not a new DSL | ||
- **TypeScript** - full type safety via generic type | ||
- **Renderless** - you take full control of the UI | ||
- **Transition** - use support Vue transition | ||
|
||
## Usage | ||
|
||
`createTemplatePromise` returns a **Vue Component** that you can directly use in your template with `<script setup>` | ||
|
||
```ts | ||
import { createTemplatePromise } from '@vueuse/core' | ||
|
||
const TemplatePromise = createTemplatePromise() | ||
const MyPromise = createTemplatePromise<boolean>() // with generic type | ||
``` | ||
|
||
In template, use `v-slot` to access the promise and resolve functions. | ||
|
||
```html | ||
<template> | ||
<TemplatePromise v-slot="{ promise, resolve, reject, args }"> | ||
<!-- you can have anything --> | ||
<button @click="resolve('ok')">OK</button> | ||
</TemplatePromise> | ||
<MyPromise v-slot="{ promise, resolve, reject, args }"> | ||
<!-- another one --> | ||
</MyPromise> | ||
</template> | ||
``` | ||
|
||
The slot will not be rendered initially (similar to `v-if="false"`), until you call the `start` method from the component. | ||
|
||
```ts | ||
const result = await TemplatePromise.start() | ||
``` | ||
|
||
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. | ||
|
||
### Passing Arguments | ||
|
||
You can pass arguments to the `start` with arguments. | ||
|
||
```ts | ||
import { createTemplatePromise } from '@vueuse/core' | ||
|
||
const TemplatePromise = createTemplatePromise<boolean, [string, number]>() | ||
``` | ||
|
||
```ts | ||
const result = await TemplatePromise.start('hello', 123) // Pr | ||
``` | ||
|
||
And in the template slot, you can access the arguments via `args` property. | ||
|
||
```html | ||
<template> | ||
<TemplatePromise v-slot="{ args, resolve }"> | ||
<div>{{ args[0] }}</div> <!-- hello --> | ||
<div>{{ args[1] }}</div> <!-- 123 --> | ||
<button @click="resolve(true)">OK</button> | ||
</TemplatePromise> | ||
</template> | ||
``` | ||
|
||
### Transition | ||
|
||
You can use transition to animate the slot. | ||
|
||
```html | ||
<script setup lang="ts"> | ||
const TemplatePromise = createTemplatePromise<ReturnType>({ | ||
transition: { | ||
name: 'fade', | ||
appear: true, | ||
}, | ||
}) | ||
</script> | ||
|
||
<template> | ||
<TemplatePromise v-slot="{ resolve }"> | ||
<!-- your UI --> | ||
<button @click="resolve('ok')">OK</button> | ||
</TemplatePromise> | ||
</template> | ||
|
||
<style scoped> | ||
.fade-enter-active, .fade-leave-active { | ||
transition: opacity .5s; | ||
} | ||
.fade-enter, .fade-leave-to { | ||
opacity: 0; | ||
} | ||
</style> | ||
``` | ||
|
||
Learn more about [Vue Transition](https://v3.vuejs.org/guide/transitions-overview.html). | ||
|
||
## Motivation | ||
|
||
The common approach to call a dialog or a model programmatically would be like this: | ||
|
||
```ts | ||
const dialog = useDialog() | ||
const result = await dialog.open({ | ||
title: 'Hello', | ||
content: 'World', | ||
}) | ||
``` | ||
|
||
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: | ||
|
||
```ts | ||
const result = await dialog.open({ | ||
title: 'Hello', | ||
titleClass: 'text-red', | ||
content: 'World', | ||
contentClass: 'text-blue text-sm', | ||
buttons: [ | ||
{ text: 'OK', class: 'bg-red', onClick: () => {} }, | ||
{ text: 'Cancel', class: 'bg-blue', onClick: () => {} }, | ||
], | ||
// ... | ||
}) | ||
``` | ||
|
||
Even this is not flexible enough. If you want more, you might end up with manual render function. | ||
|
||
```ts | ||
const result = await dialog.open({ | ||
title: 'Hello', | ||
contentSlot: () => h(MyComponent, { content }), | ||
}) | ||
``` | ||
|
||
This is like reinventing a new DSL in the script to express the UI template. | ||
|
||
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. |
Oops, something went wrong.