Skip to content

Commit

Permalink
feat: Add spawnDialog function
Browse files Browse the repository at this point in the history
This allows to spawn a Vue dialog without the need of mounting the
component.
So it is possible to spawn a dialog directly from any JS code outside of
components (e.g. callback functions).

This method was originally taken from `@nextcloud/dialogs`,
but I think it should belong to this Vue components library.

Co-authored-by: Ferdinand Thiessen <[email protected]>
Co-authored-by: Grigorii K. Shartsev <[email protected]>
Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux and ShGKme committed Nov 6, 2024
1 parent b7eeee8 commit 9f1edb6
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 1 deletion.
109 changes: 109 additions & 0 deletions docs/functions/dialog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
```ts static
import {
spawnDialog,
} from '@nextcloud/vue/dist/Functions/dialog.js'
```

## Definitions

```ts static
/**
* Helper to spawn a Vue dialog without having to mount it from a component
*
* @param dialog The dialog component to spawn - the component must emit the 'close' event whenever it is closed
* @param props Properties to pass to the dialog
* @param props.container Optionally pass a query selector for the dialog container element
* @param onClose Callback when the dialog is closed (parameters of the 'close' event of the dialog)
*/
function spawnDialog(
dialog: Component | AsyncComponent,
props: Record<string, unknown>,
onClose: (...rest: unknown[]) => void = () => {},
): Vue;
```

## Usage

The main use case is to be able to spawn a dialog from code, without the need of including the dialog component in the template.
So a Vue dialog can be spawned in any context and not just from Vue components but also from callback functions or other API.

```vue
<template>
<div>
<NcButton @click="onSpawnDialog">🖱 click to spawn a dialog</NcButton>
</div>
</template>
<script>
import { spawnDialog } from '@nextcloud/vue/dist/Functions/dialog.js'
// Example dialog component that would be normally imported
// Important: The dialog must emit a 'close' event whenever it is closed
const ExampleDialog = {
template: `
<NcDialog :buttons="buttons"
name="Spawned dialog"
message="This dialog was spawned using the 'spawnDialog' function."
@closing="saveResult"
@update:open="onClose" />`,
props: {
timesClicked: {
type: Number,
required: true,
},
},
data() {
return {
result: null,
}
},
computed: {
buttons() {
return [
{
label: 'Cancel',
callback: () => 'cancelled',
},
{
label: 'Accept',
type: 'primary',
callback: () => 'accepted',
},
]
}
},
methods: {
saveResult(result) {
this.result = result
},
onClose() {
this.$emit('close', this.result)
},
}
}
export default {
data() {
return {
timesClicked: 0,
}
},
methods: {
onSpawnDialog() {
this.timesClicked += 1
spawnDialog(
ExampleDialog,
{
timesClicked: this.timesClicked,
},
(result) => window.alert(`Dialog was ${result}`)
)
},
},
}
</script>
```
50 changes: 50 additions & 0 deletions src/functions/dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { AsyncComponent, Component } from 'vue'

import Vue, { toRaw } from 'vue'

interface DialogProps {
[index: string]: unknown
container?: string
}

/**
* Helper to spawn a Vue dialog without having to mount it from a component
*
* @param dialog The dialog component to spawn
* @param props Properties to pass to the dialog
* @param props.container Optionally pass a query selector for the dialog container element
* @param onClose Callback when the dialog is closed
*/
export function spawnDialog(
dialog: Component | AsyncComponent,
props?: DialogProps,
onClose: (...rest: unknown[]) => void = () => {},
): Vue {
const el = document.createElement('div')

const container: HTMLElement = typeof props?.container === 'string'
? (document.querySelector(props.container) || document.body)
: document.body
container.appendChild(el)

const vm = new Vue({
el,
name: 'VueDialogHelper',
render: (h) =>
h(dialog, {
props,
on: {
close: (...rest: unknown[]) => {
onClose(...rest.map(v => toRaw(v)))
vm.$destroy()
el.remove()
},
},
}),
})
return vm
}
1 change: 1 addition & 0 deletions src/functions/index.js → src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

export * from './a11y/index.ts'
export * from './dialog/index.ts'
export * from './emoji/index.ts'
export * from './reference/index.js'
export * from './isDarkTheme/index.ts'
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

export * from './components/index.js'
export * from './composables/index.js'
export * from './functions/index.js'
export * from './functions/index.ts'
export * from './directives/index.js'
export * from './mixins/index.js'

Expand Down
4 changes: 4 additions & 0 deletions styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ module.exports = async () => {
name: 'a11y',
content: 'docs/functions/a11y.md',
},
{
name: 'dialog',
content: 'docs/functions/dialog.md',
},
{
name: 'emoji',
content: 'docs/functions/emoji.md',
Expand Down

0 comments on commit 9f1edb6

Please sign in to comment.