Skip to content

Commit

Permalink
feat(NcDialogButton): Allow to return false from callback to keep d…
Browse files Browse the repository at this point in the history
…ialog open

If the callback returns `false` then the click event will not be forwarded.
This could be usful if the click triggers a validation that fails and the user
should be able to fix the issue.

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Aug 29, 2024
1 parent 47e15cd commit 3fbf2d8
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 9 deletions.
4 changes: 4 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ msgstr ""
msgid "Load more \"{options}\""
msgstr ""

#. TRANSLATORS: The button is in a loading state
msgid "Loading …"
msgstr ""

#. TRANSLATORS: A color name for RGB(45, 115, 190)
msgid "Mariner"
msgstr ""
Expand Down
63 changes: 62 additions & 1 deletion src/components/NcDialog/NcDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ Note that this is not possible if the dialog contains a navigation!
</div>
</template>
<script>
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
import IconCheck from '@mdi/svg/svg/check.svg?raw'
export default {
Expand All @@ -128,6 +127,68 @@ export default {
}
</script>
```

### Loading buttons
Sometimes a dialog ends with a request and this request might fail due to server-side-validation.
In this case it is often desired to keep the dialog open, this can be done by returning `false` from the button callback,
to not block this callback should return a `Promise<false>`.

While the promise is awaited the button will have a loading state,
this means, as long as no custom `icon`-slot is used, a loading icon will be shown.
Please note that the **button will not be disabled or accessibility reasons**,
because disabled elements cannot be focused and so the loading state could not be communicated e.g. via screen readers.

```vue
<template>
<div>
<NcButton @click="showDialog = true">Show dialog</NcButton>
<NcDialog :buttons="buttons"
name="Create user"
:message="message"
:open.sync="showDialog"
@update:open="clickClosesDialog = false" />
</div>
</template>
<script>
export default {
data() {
return {
showDialog: false,
clickClosesDialog: false,
}
},
computed: {
buttons() {
return [
{
label: 'Create user',
type: 'primary',
callback: async () => {
// wait 3 seconds
await new Promise((resolve) => window.setTimeout(resolve, 3000))
this.clickClosesDialog = !this.clickClosesDialog
console.warn(this.clickClosesDialog)
// Do not close the dialog on first and then every second button click
if (this.clickClosesDialog) {
// return false means the dialog stays open
return false
}
},
}
]
},
message() {
if (this.clickClosesDialog) {
return 'Next button click will work and close the dialog.'
} else {
return 'Clicking the button will load but not close the dialog.'
}
},
},
}
</script>
```
</docs>

<template>
Expand Down
36 changes: 28 additions & 8 deletions src/components/NcDialogButton/NcDialogButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,35 @@ Dialog button component used by NcDialog in the actions slot to display the butt
<template #icon>
<!-- @slot Allow to set a custom icon for the button -->
<slot name="icon">
<NcIconSvgWrapper v-if="icon !== undefined" :svg="icon" />
<!-- The loading state is an information that must be accessible -->
<NcLoadingIcon v-if="isLoading" :name="t('Loading …') /* TRANSLATORS: The button is in a loading state*/" />
<NcIconSvgWrapper v-else-if="icon !== undefined" :svg="icon" />
</slot>
</template>
</NcButton>
</template>

<script>
import { defineComponent } from 'vue'
import { defineComponent, ref } from 'vue'
import NcButton from '../NcButton/index.js'
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
import NcLoadingIcon from '../NcLoadingIcon/index.js'
import { t } from '../../l10n.js'
export default defineComponent({
name: 'NcDialogButton',
components: {
NcButton,
NcIconSvgWrapper,
NcLoadingIcon,
},
props: {
/**
* The function that will be called when the button is pressed
* @type {() => void}
* The function that will be called when the button is pressed.
* If the function returns `false` the click is ignored and the dialog will not be closed.
* @type {() => void|false|Promise<void|false>}
*/
callback: {
type: Function,
Expand Down Expand Up @@ -99,16 +105,30 @@ export default defineComponent({
emits: ['click'],
setup(props, { emit }) {
const isLoading = ref(false)
/**
* Handle clicking the button
* @param {MouseEvent} e The click event
*/
const handleClick = (e) => {
props.callback?.()
emit('click', e)
const handleClick = async (e) => {
// Do not re-emit while loading
if (isLoading.value) {
return
}
isLoading.value = true
try {
const response = await props.callback?.()
if (response !== false) {
emit('click', e)
}
} finally {
isLoading.value = false
}
}
return { handleClick }
return { handleClick, isLoading, t }
},
})
</script>

0 comments on commit 3fbf2d8

Please sign in to comment.