Skip to content

AbpExtensibleForm ​

AbpExtensibleForm 用于可复用模块基于扩展的页面。单个应用自己拥有的业务页面,通常直接定义列、按钮和表单状态即可。

渲染注册的创建或编辑字段 ​

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>

完整集成假定 /api/app/documentation-catalog 支持 GET 列表、POST 创建、PUT 更新和 DELETE,记录包含 id/name,可选 concurrencyStamp/extraProperties。复制两个文件,把 catalogExtensions 加入已有启动 providers,并注册 CatalogModulePage 路由。默认项在页面挂载前注册,宿主贡献器在默认项之后注册。页面提供 CATALOG_PAGE 命令,不清空宿主贡献。包模块可按模块教程从路由解析阶段暴露贡献器选项。应用布局提供通知与确认宿主。

数据与生命周期 ​

没有记录时 editor 构建创建字段,已有记录时构建编辑字段。表单持有响应式控制,渲染器负责分组与显示。field-{name} 插槽提供 prop 和 control,覆盖显示时绑定 control.value。自定义控件需遵守 disabled/readonly 并发出更新。隐藏字段仍可能有验证器,可见性不自动移除控制或验证。额外属性通过 toRequestBody() 保存,保留原有隐藏值。后端验证仍是最终依据。

扩展模块 ​

使用稳定的公开组件标识,例如 Catalog.BooksComponent,消费者通过同一标识注册贡献器。修改 key 会改变扩展容器和偏好归属。贡献器按注册顺序执行,再次添加同名项不会自动替换。

可复用模块应从公开入口暴露默认值与贡献器选项。启动和路由注册参见模块教程,默认值、顺序和排查参见扩展行为。

检查完整流程 ​

打开列表、创建有效记录、获取详情后编辑、先取消一次删除再确认一次。检查失败请求保留值,额外属性编辑后仍存在,刷新按预期保留或重置页码。最后增加一个宿主贡献器,确认默认项仍然只有一份。

配套端点由后端示例提供。复制目录服务,使用拥有 AbpIdentity.Users 权限的用户登录。它是宿主重启会清空的教学存储,不是框架内置业务 API。

属性、事件与插槽 ​

源码

类型来自公开契约,默认表达式来自当前实现。短横线表示没有显式默认值;省略的可选布尔属性通常为 false。

Props ​

名称类型必填默认值
formExtensibleForm<R>是—
recordR | undefined否—

Events ​

没有声明组件专有事件。

Slots ​

名称上下文
field-${string}(props: { prop: FormProp<R>; control: AbpFormControl }) => unknown

非官方社区项目,采用 MIT 许可证,与 Volosoft 无关。