Skip to content

Modal forms and unsaved changes ​

Use a modal for a short focused form. The page owns visibility, form values and the save request; the modal protects user-initiated closing and manages focus.

Complete form example ​

The example assumes POST /api/app/product accepts { name: string }. Register the theme and its confirmation host through your normal application layout.

vue
<template>
  <AbpButton @click="open">{{ localization.t('AbpUi::New') }}</AbpButton>
  <AbpModal v-model:visible="visible" :busy="saving" :dirty="form.dirty" size="lg" centered>
    <template #header
      ><h2>{{ localization.t('BookStore::Product') }}</h2></template
    >
    <AbpFormField
      v-slot="{ id, describedBy, invalid }"
      :label="localization.t('AbpIdentity::DisplayName:Name')"
      :errors="messages"
      required
    >
      <AbpInput
        :id="id"
        v-model="form.controls.name.value"
        :aria-describedby="describedBy"
        :invalid="invalid"
        :disabled="saving"
      />
    </AbpFormField>
    <p v-for="message in form.unmatchedServerErrors" :key="message" role="alert">{{ message }}</p>
    <template #footer="{ close }">
      <AbpButton variant="secondary" :disabled="saving" @click="close">
        {{ localization.t('AbpUi::Cancel') }}
      </AbpButton>
      <AbpButton :loading="saving" @click="save">{{ localization.t('AbpUi::Save') }}</AbpButton>
    </template>
  </AbpModal>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { inject, RestService, useLocalization } from '@lsw-abpvue/core';
import {
  AbpButton,
  AbpFormField,
  AbpInput,
  AbpModal,
  useAbpForm,
  useServerValidation,
  useValidationMessages,
  Validators,
} from '@lsw-abpvue/theme-shared';

const rest = inject(RestService);
const localization = useLocalization();
const visible = ref(false);
const saving = ref(false);
const form = useAbpForm({ name: { value: '', validators: [Validators.required()] } });
useServerValidation(form);
const validationMessages = useValidationMessages();
const messages = computed(() =>
  form.controls.name.touched ? validationMessages(form.controls.name.errors) : [],
);

function open(): void {
  form.reset();
  visible.value = true;
}

async function save(): Promise<void> {
  if (saving.value || !form.validate()) return;
  saving.value = true;
  try {
    await rest.request<{ name: string }, unknown>({
      method: 'POST',
      url: '/api/app/product',
      body: form.value,
    });
    visible.value = false;
    form.reset();
  } catch {
    // Global handlers display the failure; the dialog retains the entered values.
  } finally {
    saving.value = false;
  }
}
</script>

Place it in src/components/ModalFormExample.vue and render it from an application page. Provide the BookStore resource keys used for the title.

Close paths ​

PathBehavior
Footer close(), header close, Escape, backdropRuns close protection
Unsaved input or dirty: trueAsks whether to discard
busy: trueBlocks user close requests
Save succeeds and page sets visible = falseCloses directly
Save failsPage keeps modal and entered values
Component unmountsCleans up its pending confirmation and listeners

Bind the footer cancel button to the scoped close function. Setting visible = false from Cancel bypasses the discard confirmation. Use direct assignment only for a completed operation or an intentional programmatic close.

Native input/change events inside the modal mark it dirty. dirty covers custom controls and programmatic edits that do not emit those events. It is combined with tracked input changes; setting dirty to false does not undo a native change in an already open modal.

busy protects closing; custom footer controls must also bind their disabled/loading state. It does not automatically disable arbitrary slot content.

Suppress confirmation for one modal ​

vue
<AbpModal
  v-model:visible="visible"
  :suppress-unsaved-changes-warning="true"
  aria-label="Preview"
>
  <p>{{ preview }}</p>
</AbpModal>

Here visible and preview are page-owned values. The flag suppresses unsaved-change warnings for this instance. It does not disable the busy close guard.

Size and lifecycle ​

Use size to choose sm, md, lg or xl; the default is md. centered enables vertical centering. Put the title in the header slot, content in the default slot and actions in the footer.

init fires once per opening before the dialog enters the document. appear and disappear report visibility changes, not animation completion. Wrap common props in an application component when you need a consistent size or closing policy across your app.

While an edited modal is open, browser page-unload protection can request confirmation. Closing protection is not a router-wide unsaved-form guard. A page that needs to prevent SPA navigation should add its own route-leave check.

The header names the dialog; when there is no header, supply ariaLabel. Keyboard focus returns to the trigger after closing. Test Escape, backdrop, cancel, a failed save and repeated open/close cycles.

Modal props and slots, forms and confirmation provide the contracts.

Unofficial community project. MIT licensed. Not affiliated with Volosoft.