Skip to content

AbpPageToolbar ​

AbpPageToolbar 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 contributed page commands ​

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 ​

Put this component in the page toolbar slot. data contains the currently loaded page, not every record matching the filter and not the selected ids. Its contributed callbacks receive PropData<readonly R[]> with getInjected for page services. Permission and visibility predicates decide which commands are rendered. A custom asynchronous command owns its repeated-request guard and refresh. For a bulk export across all pages, call a backend export endpoint with the filter instead of treating current data as the whole query result.

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[] | undefinedNo() => []

Events ​

No component-specific events are declared.

Slots ​

No named slots.

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