Skip to content

AbpRecordModal ​

AbpRecordModal is part of a reusable module's extension-based page. For a business page owned by one application, explicit columns, buttons and form state are usually sufficient.

Connect an editor to a dialog ​

ts
import { defineToken, inject, provideAppInitializer } from '@lsw-abpvue/core';
import {
  EntityAction,
  EntityProp,
  ExtensionsService,
  FormProp,
  PropType,
  ToolbarAction,
} from '@lsw-abpvue/components';
import { Validators } from '@lsw-abpvue/theme-shared';

export interface CatalogBook {
  id: string;
  name: string;
  category?: string | undefined;
  price?: number | undefined;
  concurrencyStamp?: string | undefined;
  extraProperties?: Record<string, unknown> | undefined;
}
export const catalogKey = 'Catalog.BooksComponent';
export const CATALOG_PAGE = defineToken<{
  add(): void;
  edit(record: CatalogBook): Promise<void>;
  remove(record: CatalogBook): Promise<void>;
}>('CATALOG_PAGE');

export const catalogExtensions = provideAppInitializer(() => {
  const extensions = inject(ExtensionsService);
  extensions.entityProps.get<CatalogBook>(catalogKey).addContributor(props =>
    props.addTail(
      EntityProp.create<CatalogBook>({
        name: 'name',
        type: PropType.String,
        displayName: 'Name',
        sortable: true,
      }),
    ),
  );
  for (const factory of [extensions.createFormProps, extensions.editFormProps]) {
    factory.get<CatalogBook>(catalogKey).addContributor(props =>
      props.addTail(
        FormProp.create<CatalogBook>({
          name: 'name',
          type: PropType.String,
          displayName: 'Name',
          validators: () => [Validators.required(), Validators.maxLength(128)],
        }),
      ),
    );
  }
  extensions.entityActions.get<CatalogBook>(catalogKey).addContributor(props =>
    props.addManyTail([
      EntityAction.create<CatalogBook>({
        text: 'Edit',
        action: data => data.getInjected(CATALOG_PAGE).edit(data.record),
      }),
      EntityAction.create<CatalogBook>({
        text: 'Delete',
        action: data => data.getInjected(CATALOG_PAGE).remove(data.record),
      }),
    ]),
  );
  extensions.toolbarActions.get<readonly CatalogBook[]>(catalogKey).addContributor(props =>
    props.addTail(
      ToolbarAction.create<readonly CatalogBook[]>({
        text: 'New book',
        icon: 'bi bi-plus',
        action: data => data.getInjected(CATALOG_PAGE).add(),
      }),
    ),
  );
});
vue
<template>
  <AbpPage title="Books">
    <template #toolbar><AbpPageToolbar :data="items" /></template>
    <AbpExtensibleTable :data="items" :list="list" record-key="id" searchable />
    <AbpRecordModal
      :editor="editor"
      :label="{ key: 'Catalog::Books', defaultValue: 'Books' }"
      :create-title="{ key: 'Catalog::NewBook', defaultValue: 'New book' }"
    />
  </AbpPage>
</template>

<script setup lang="ts">
import {
  AbpExtensibleTable,
  AbpPage,
  AbpPageToolbar,
  AbpRecordModal,
  useRecordEditor,
  type RecordEditor,
} from '@lsw-abpvue/components';
import { inject, RestService, useListService, type PagedResultDto } from '@lsw-abpvue/core';
import { onBeforeUnmount, ref } from 'vue';
import { CATALOG_PAGE, catalogKey, type CatalogBook as Book } from './catalog-extensions';

const rest = inject(RestService);
const request = new AbortController();
const loadingDetail = ref(false);
onBeforeUnmount(() => request.abort());
const list = useListService({ persistKey: catalogKey });
const { items } = list.hookToQuery((query, signal) =>
  rest.request<never, PagedResultDto<Book>>(
    { method: 'GET', url: '/api/app/documentation-catalog', params: { ...query } },
    { signal },
  ),
);
const editor: RecordEditor<Book> = useRecordEditor<Book>({
  identifier: catalogKey,
  providers: [
    {
      provide: CATALOG_PAGE,
      useValue: {
        add: () => editor.show(),
        edit: editBook,
        remove: (record: Book) => editor.remove(record),
      },
    },
  ],
  reload: () => list.getWithoutPageReset(),
  create: body =>
    rest.request(
      { method: 'POST', url: '/api/app/documentation-catalog', body },
      { signal: request.signal },
    ),
  update: (id, body) =>
    rest.request(
      {
        method: 'PUT',
        url: `/api/app/documentation-catalog/${id}`,
        body: { ...editor.editing.value, ...body },
      },
      { signal: request.signal },
    ),
  delete: id =>
    rest.request(
      { method: 'DELETE', url: `/api/app/documentation-catalog/${id}` },
      { signal: request.signal },
    ),
  idOf: record => record.id,
  stampOf: record => record.concurrencyStamp,
  nameOf: record => record.name,
  deletionMessage: { key: 'Catalog::DeleteBook', defaultValue: 'Delete {0}?' },
});

async function editBook(record: Book): Promise<void> {
  if (loadingDetail.value || editor.busy.value) return;
  loadingDetail.value = true;
  try {
    const detail = await rest.request<never, Book>(
      { method: 'GET', url: `/api/app/documentation-catalog/${record.id}` },
      { signal: request.signal },
    );
    if (!request.signal.aborted) editor.show(detail);
  } catch {
    /* Request handlers report failure; keep the incomplete editor closed. */
  } finally {
    loadingDetail.value = false;
  }
}
</script>

This complete integration assumes /api/app/documentation-catalog supports GET list, POST create, PUT update and DELETE, and returns records with id/name and optional concurrencyStamp/extraProperties. Copy both files, add catalogExtensions to the existing startup providers, and register a route for CatalogModulePage. Defaults are registered before the page mounts; register host contributors after these defaults. The page supplies CATALOG_PAGE commands and does not clear host contributions. A packaged module can expose contributor options through its route resolver as described in the module tutorial. The application layout supplies notification and confirmation hosts.

Data and lifecycle ​

Create one useRecordEditor in setup. show() opens creation; show(record) opens editing. If a list DTO omits editable fields or the concurrency stamp, fetch the detail DTO before showing it. The editor validates and builds the body, passes the concurrency stamp on update, keeps the dialog on failure and reloads on success. Delete asks confirmation first. Header/body/footer slots replace their respective default rendering; bind busy states on custom controls. The optional save prop replaces submission, so the page must preserve validation, error handling and successful close behavior itself.

Extend the module ​

Use a stable public component identifier such as Catalog.BooksComponent. Consumers register contributors under that identifier. A changed key starts a different extension bucket and preference identity. Contributors execute in registration order; adding the same name again does not automatically replace it.

A reusable module should expose defaults and contributor options through its public entry. Follow the reusable module tutorial for package startup and route registration. Extension behavior explains defaults, ordering and diagnostics.

Check the completed workflow ​

Open the list, create a valid record, edit a fetched record, reject deletion once and confirm it once. Check a failed request retains values, extra properties survive an edit, and list refresh preserves or resets the page as intended. Then add one host contributor and verify the default still exists exactly once.

Install the supplied catalogue backend before running this page. It uses an in-memory teaching store and the existing AbpIdentity.Users policy; the endpoint is not part of every ABP application.

Props, events and slots ​

Source

Types come from the public contract and defaults from the current implementation. A dash means no explicit default; optional boolean props are normally false when omitted.

Props ​

NameTypeRequiredDefault
editorRecordEditor<R>Yes—
labelLocalizationParamYes—
createTitleLocalizationParamYes—
editTitleLocalizationParam | undefinedNo—
size'sm' | 'md' | 'lg' | 'xl' | undefinedNo—
save(() => void) | undefinedNo—

Events ​

No component-specific events are declared.

Slots ​

NameContext
default() => unknown
header() => unknown
footer(context: { close: () => Promise<void> }) => unknown

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