Skip to content

AbpExtensibleTable ​

AbpExtensibleTable 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.

Render module columns and row actions ​

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 ​

The enclosing editor provides EXTENSIONS_IDENTIFIER; registered columns and actions are assembled before the table mounts. The table binds paging, sorting, range summary and optional search to its list. The backend must support the filter and sort fields. Use stable recordKey, a unique preference key, and named cell-{name} slots for one-page rendering overrides. columnVisible hides the whole column; visible can hide one row's value. hiddenColumns stores names, not translated headers. Permission checks affect rendering, but the backend still authorizes the query.

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
datareadonly R[]Yes—
listListServiceYes—
recordKeystring | undefinedNoundefined
actionsTextstring | undefinedNoundefined
actionsColumnWidthnumber | undefinedNoundefined
captionstring | undefinedNoundefined
selectableboolean | undefinedNofalse
expandableboolean | undefinedNofalse
searchableboolean | undefinedNofalse
persistKeystring | undefinedNoundefined
selectedstring[]No() => []
expandedstring[]No() => []
hiddenColumnsstring[]No() => []

Events ​

NamePayload
update:selected[value: string[]]
update:expanded[value: string[]]
update:hiddenColumns[value: string[]]

Slots ​

NameContext
toolbar() => unknown
expanded-row(props: { row: R; index: number }) => unknown
empty() => unknown
cell-${string}(props: { row: R; value: PropValue; index: number }) => unknown

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