Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-crabs-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-app/vue-components": patch
---

Moves modal state to Form component
13 changes: 11 additions & 2 deletions packages/vue-components/src/components/Dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<!-- pass down slots -->
<!-- @vue-skip -->
<template
v-for="(_, name) in $slots"
v-for="(_, name) in otherSlots"
#[name]="slotData"
>
<slot
Expand All @@ -26,6 +26,7 @@
</template>

<script setup lang="ts">
import { computed, useSlots } from "vue"
import { useOnClose } from "./OmegaForm/blockDialog"
import { onMountedWithCleanup } from "./OmegaForm/onMountedWithCleanup"

Expand All @@ -35,7 +36,15 @@ const props = defineProps<{

const open = defineModel<boolean>({ default: false })

const onCancel = useOnClose(() => open.value = false)
const slots = useSlots()
const otherSlots = computed(() => {
const { default: _, ...rest } = slots
return rest
})

const onCancel = useOnClose(() => {
return open.value = false
})

onMountedWithCleanup(() => {
const onEscape = (e: KeyboardEvent) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useStore } from "@tanstack/vue-form"
import { defineSlots } from "vue"
import { usePreventClose } from "./blockDialog"
import { getOmegaStore } from "./getOmegaStore"
import { type DefaultTypeProps, type OmegaFormApi, type OmegaFormState } from "./OmegaFormStuff"
import { type OmegaFormReturn } from "./useOmegaForm"
Expand All @@ -52,6 +54,13 @@ const subscribedValues = getOmegaStore(
props.subscribe
)

if (!props.form.ignorePreventCloseEvents) {
console.log(props.form.ignorePreventCloseEvents)
usePreventClose(() => {
return props.form.useStore((state) => state.isDirty)
})
}

defineSlots<{
default(props: { subscribedValues: typeof subscribedValues.value }): void
}>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export const usePreventClose = (mkIsDirty: () => Ref<boolean>) => {
}

export const useOnClose = (close: () => void) => {
const bus = provideBus()
// Use existing bus if available, otherwise provide a new one
let bus = injectBus()
if (!bus) {
bus = provideBus()
}

const onClose = () => {
const evt: DialogClosing = {}
bus.emit("dialog-closing", evt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Array, Data, Effect, Fiber, Option, Order, S } from "effect-app"
import { runtimeFiberAsPromise } from "effect-app/utils"
import { isObject } from "effect/Predicate"
import { Component, computed, ComputedRef, ConcreteComponent, h, type InjectionKey, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from "vue"
import { usePreventClose } from "./blockDialog"
import { MergedInputProps } from "./InputProps"
import OmegaArray from "./OmegaArray.vue"
import OmegaAutoGen from "./OmegaAutoGen.vue"
Expand Down Expand Up @@ -129,6 +128,7 @@ export interface OF<From, To> extends OmegaFormApi<From, To> {
meta: MetaRecord<From>
clear: () => void
i18nNamespace?: string
ignorePreventCloseEvents?: boolean
registerField: (
field: ComputedRef<{
name: string
Expand Down Expand Up @@ -870,6 +870,7 @@ export const useOmegaForm = <

const formWithExtras: OF<From, To> = Object.assign(form, {
i18nNamespace: omegaConfig?.i18nNamespace,
ignorePreventCloseEvents: omegaConfig?.ignorePreventCloseEvents,
meta,
clear,
handleSubmit: (meta?: Record<string, any>) => {
Expand All @@ -886,10 +887,6 @@ export const useOmegaForm = <

const errorContext = { form: formWithExtras, fieldMap }

if (!omegaConfig?.ignorePreventCloseEvents) {
usePreventClose(() => formWithExtras.useStore((state) => state.isDirty))
}

return Object.assign(formWithExtras, {
errorContext,
Form: fHoc(formWithExtras)(OmegaForm as any) as any,
Expand Down
34 changes: 34 additions & 0 deletions packages/vue-components/stories/Dialog.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Meta as StoryMeta, StoryObj } from "@storybook/vue3"
import Dialog from "../src/components/Dialog.vue"
import AsyncBlockingComponent from "./Dialog/AsyncBlocking.vue"
import SimpleBlockingComponent from "./Dialog/SimpleBlocking.vue"
import UploadBlockingComponent from "./Dialog/UploadBlocking.vue"

const meta: StoryMeta<typeof Dialog> = {
title: "Components/Dialog",
component: Dialog
}

export default meta
type Story = StoryObj<typeof Dialog>

export const SimpleBlocking: Story = {
render: () => ({
components: { SimpleBlockingComponent },
template: "<SimpleBlockingComponent />"
})
}

export const AsyncBlocking: Story = {
render: () => ({
components: { AsyncBlockingComponent },
template: "<AsyncBlockingComponent />"
})
}

export const UploadBlocking: Story = {
render: () => ({
components: { UploadBlockingComponent },
template: "<UploadBlockingComponent />"
})
}
46 changes: 46 additions & 0 deletions packages/vue-components/stories/Dialog/AsyncBlocking.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<template>
<div class="pa-4">
<h2 class="text-h5 mb-4">
Async Blocking with Custom Modal
</h2>
<p class="mb-4">
Demonstrates <code>injectBus()</code> and <code>evt.prevent = Promise&lt;boolean&gt;</code> to use a custom modal instead of native confirm.
</p>

<v-btn @click="open = true">
Open Dialog
</v-btn>

<Dialog v-model="open">
<template #default="{ cancel }">
<v-card
min-width="400"
max-width="600"
>
<v-card-title>
Create Project
</v-card-title>
<v-card-text>
<AsyncBlockingContent />
</v-card-text>
<v-card-actions>
<v-btn
variant="text"
@click="cancel"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</template>
</Dialog>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue"
import Dialog from "../../src/components/Dialog.vue"
import AsyncBlockingContent from "./AsyncBlockingContent.vue"

const open = ref(false)
</script>
91 changes: 91 additions & 0 deletions packages/vue-components/stories/Dialog/AsyncBlockingContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<div>
<p class="mb-4 text-caption">
Blocking enabled: <strong>{{ shouldBlock }}</strong>
</p>
<p class="mb-4">
Toggle the switch and try to close. You'll see a custom modal with Allow/Prevent options.
</p>

<v-switch
v-model="shouldBlock"
label="Enable blocking"
color="primary"
/>

<p class="mt-4 text-body-2">
This uses <code>evt.prevent = Promise&lt;boolean&gt;</code> for async blocking with a custom modal.
</p>

<!-- Custom Confirmation Modal -->
<v-dialog
v-model="showConfirm"
max-width="400"
persistent
>
<v-card>
<v-card-title class="bg-warning">
Confirm Close
</v-card-title>
<v-card-text class="pt-4">
The blocking is enabled. What would you like to do?
</v-card-text>
<v-card-actions>
<v-btn
variant="text"
@click="handlePrevent"
>
Prevent (Stay)
</v-btn>
<v-spacer />
<v-btn
color="primary"
@click="handleAllow"
>
Allow (Close)
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue"
import { type DialogClosing, injectBus } from "../../src/components/OmegaForm/blockDialog"
import { onMountedWithCleanup } from "../../src/components/OmegaForm/onMountedWithCleanup"

const shouldBlock = ref(false)
const showConfirm = ref(false)
let confirmResolver: ((allow: boolean) => void) | null = null

const bus = injectBus()

if (bus) {
onMountedWithCleanup(() => {
const handler = (evt: DialogClosing) => {
if (shouldBlock.value) {
evt.prevent = new Promise((resolve) => {
confirmResolver = resolve
showConfirm.value = true
})
}
}

bus.on("dialog-closing", handler)
return () => bus.off("dialog-closing", handler)
})
}

const handlePrevent = () => {
showConfirm.value = false
confirmResolver?.(false) // Prevent close
confirmResolver = null
}

const handleAllow = () => {
showConfirm.value = false
confirmResolver?.(true) // Allow close
confirmResolver = null
}
</script>
49 changes: 49 additions & 0 deletions packages/vue-components/stories/Dialog/SimpleBlocking.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<div class="pa-4">
<h2 class="text-h5 mb-4">
Simple Sync Blocking
</h2>
<p class="mb-4">
Demonstrates <code>usePreventClose()</code> - the simplest way to block dialog closing with a native confirm.
</p>

<v-btn @click="open = true">
Open Dialog
</v-btn>

<Dialog v-model="open">
<template #default="{ cancel }">
<v-card
min-width="400"
max-width="600"
>
<v-card-title>
Edit User
</v-card-title>
<v-card-text>
<SimpleBlockingContent />
</v-card-text>
<v-card-actions>
<v-btn
variant="text"
@click="() => {
console.log('cancel')
cancel()
}"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</template>
</Dialog>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue"
import Dialog from "../../src/components/Dialog.vue"
import SimpleBlockingContent from "./SimpleBlockingContent.vue"

const open = ref(false)
</script>
30 changes: 30 additions & 0 deletions packages/vue-components/stories/Dialog/SimpleBlockingContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div>
<p class="mb-4 text-caption">
Blocking enabled: <strong>{{ shouldBlock }}</strong>
</p>
<p class="mb-4">
Toggle the switch and try to close. When enabled, you'll see: "Es sind ungespeicherte Änderungen vorhanden.
Wirklich schließen?"
</p>

<v-switch
v-model="shouldBlock"
label="Enable blocking"
color="primary"
/>

<p class="mt-4 text-body-2">
This uses <code>usePreventClose(() => shouldBlock)</code> to sync block the dialog.
</p>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue"
import { usePreventClose } from "../../src/components/OmegaForm/blockDialog"

const shouldBlock = ref(false)

usePreventClose(() => shouldBlock)
</script>
Loading
Loading