生成第一个表单自定义工具扩展

表单自定义工具是SharePoint 框架组件,使你可以通过将组件关联到使用的内容类型来替代列表或库级别的表单体验。 表单定制器组件可以在 SharePoint Online 中使用,并且可以使用新式 JavaScript 工具和库生成它们。

重要

表单自定义工具作为 SharePoint 框架 1.15 的一部分发布,因此请确保在环境中使用正确的版本。 有关详细信息,请参阅 v1.15 发行说明

提示

可以从 GitHub 找到本教程的输出。

创建扩展项目

  1. 在最喜爱的位置创建新的项目目录。

    md form-customizer
    
  2. 转到项目目录。

    cd form-customizer
    
  3. 通过运行 Yeoman SharePoint 生成器创建新的 HelloWorld 扩展。

    yo @microsoft/sharepoint
    
  4. 出现提示时,请输入以下值(为下面省略的所有提示选择默认选项):

    • 解决方案名称是什么?:表单定制器
    • 要创建哪种类型的客户端组件?:扩展
    • 要创建哪种类型的客户端扩展? 表单自定义工具
    • 表单自定义工具的名称是什么? HelloWorld
    • 要使用哪个模板?:无 JavaScript 框架

    此时,Yeoman 安装必需的依赖项,并为解决方案文件和“HelloWorld”扩展搭建基架。 这可能需要几分钟的时间。

  5. 请将以下命令键入到控制台以启动 Visual Studio Code。

    code .
    

    注意

    由于 SharePoint 客户端解决方案基于 HTML/TypeScript,因此可使用任何支持客户端开发的代码编辑器来生成扩展。

  6. 打开 ./src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json 文件。

    此文件定义扩展类型和扩展的唯一标识符,该标识符 id 可用于设置为用于内容类型级别,以便使用此组件启用自定义呈现。

对表单自定义工具进行编码

打开 ./src/extensions/helloWorld/HelloWorldFormCustomizer.ts 文件。

请注意,表单自定义工具的基类是从 sp-listview-extensibility 包导入的,其中包含表单自定义工具所需的SharePoint 框架代码。

import { Log } from '@microsoft/sp-core-library';
import {
  BaseFormCustomizer
} from '@microsoft/sp-listview-extensibility';

表单自定义工具的逻辑包含在 、 render()onDispose() 方法中onInit()

  • onInit() 是执行扩展所需的设置的位置。 此事件在 this.contextthis.properties 分配后发生,但先于页面 DOM 到位。 与 Web 部件一样, onInit() 返回可用于执行异步操作的承诺; render() 在承诺解决之前不会调用。 如果不需要它,只返回 Promise.resolve<void>();
  • render() 呈现组件时发生。 它提供了 event.domElement 代码可以写入其内容的 HTML 元素。
  • onDispose() 在删除表单 host 元素之前发生。 它可用于释放在表单呈现期间分配的任何资源。 例如,如果 render() 装入一个 React 元素,则 onDispose() 必须用于释放它,否则就会出现资源泄露。

以下是 和 在默认解决方案中的内容render()onDispose()

  public render(): void {
    // Use this method to perform your custom rendering.
    this.domElement.innerHTML = `<div class="${ styles.helloWorld }"></div>`;
  }

  public onDispose(): void {
    // This method should be used to free any resources that were allocated during rendering.
    super.onDispose();
  }

默认情况下,表单 customzier 组件不呈现任何信息,让我们按如下所示更新呈现方法。

  public render(): void {
    // Use this method to perform your custom rendering.
    this.domElement.innerHTML = `<div class="${ styles.helloWorld }"><h1>Super cool custom form component</h1></div>`;
  }

调试表单自定义工具

可以在实时 SharePoint Online 网站中测试和调试表单自定义工具。 无需将自定义项部署到租户应用目录即可执行此操作,这使得调试体验简单高效。

  1. 若要测试扩展,需要首先创建列表来测试自定义工具。 因此,请移动到 SharePoint Online 租户中要测试表单定制器的网站。

  2. 在工具栏上,选择“新建”,然后选择“列表”

    创建新列表

  3. 从新列表创建体验中选择 “空白 列表”

    选择空白列表

  4. 创建名为 Business 的新列表,然后选择“ 创建”。

    创建名为“订单”的新列表

  5. 在 Visual Studio Code 中,打开 ./config/serve.json 文件。

    pageUrl更新属性以匹配我们在预览步骤中创建的列表的 URL。 更改后, serve.json 应如以下代码所示:

    {
      "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
      "port": 4321,
      "https": true,
      "serveConfigurations": {
        "default": {
          "pageUrl": "https://yourtenant.sharepoint.com/sites/demo/_layouts/15/SPListForm.aspx",
          "formCustomizer": {
            "componentId": "fb3e7dee-a6fa-4e80-add1-e06081167bb5",
            "PageType": 8,
            "RootFolder": "/sites/demo/Lists/business",
            "properties": {
              "sampleText": "Value"
            }
          }
    

    让我们从 serve.json 文件中调用几个特定主题

    • 可以看到多个不同的配置,这些配置可用于调试具有特定查询参数差异的新表单、编辑窗体和查看表单。 可以在 gulp serve 命令中定义使用的配置,例如 gulp serve --config=helloWorld_EditForm
    • 如果有多个组件) ,componentId 将自动关联为解决方案中的第一个列表格式设置组件 (
    • 若要简化调试,无需定义组件关联的目标内容类型 ID,但在运行时中,通过更新内容类型中的至少一个属性,在内容类型级别执行关联:
      • ContentType。NewFormClientSideComponentId - 新窗体的组件 ID
      • ContentType。NewFormClientSideComponentProperties - 可选配置详细信息
      • ContentType。DispFormClientSideComponentId - 编辑表单的组件 ID
      • ContentType。DispFormClientSideComponentProperties - 可选配置详细信息
      • ContentType。EditFormClientSideComponentId - 组件 ID 显示窗体
      • ContentType。EditFormClientSideComponentProperties - 可选配置详细信息
  6. 通过运行以下命令来编译代码,并从本地计算机托管编译的文件:

    gulp serve
    

    如果在完成代码编译后没有出现任何错误,它将从 https://localhost:4321 提供生成的清单。

    gulp 服务

    这将启动默认浏览器并加载 在 serve.json 文件中定义的页面。

  7. 出现提示时,通过选择“加载调试脚本”接受加载调试清单。

    接受加载调试脚本

    请注意自定义组件如何根据我们更新为 render 方法的自定义内容在页面中呈现。

    使用默认 outpu 呈现的自定义工具的列表视图

向示例添加表单项编辑功能

现在,我们已创建基线组件并测试它是否正常工作。 我们将为显示、编辑和新窗体创建单独的呈现逻辑,并支持将新项保存到列表中。

  1. 打开 ./src/extensions/helloWorld/loc/myStrings.d.ts 文件,并将新的 Title 添加到 IHelloWorldFormCustomizerStrings 接口 。 编辑后,接口应如下所示。

    declare interface IHelloWorldFormCustomizerStrings {
      Save: string;
      Cancel: string;
      Close: string;
      Title: string;
    }
    
  2. 打开 ./src/extensions/helloWorld/loc/en-us.js 文件,并将新的 Title 字符串添加到该文件。 编辑后,文件内容应如下所示。

    define([], function() {
      return {
        "Save": "Save",
        "Cancel": "Cancel",
        "Close": "Close",
        "Title": "Title"
      }
    });
    
  3. 打开 ./src/extensions/helloWorld/HelloWorldFormCustomizer.module.scss 文件,并按如下所示更新样式定义。 我们将为 组件添加错误样式。

    .helloWorld {
      background-color: "[theme:white, default:#ffffff]";
      color: "[theme:themePrimary, default:#0078d4]";
      padding: 0.5rem;
    
      .error {
        color: red;
      }
    }
    
  4. 移动到 HelloWorldFormCustomizer.ts 文件的顶部。

  5. 找到该行 import styles from './HelloWorldFormCustomizer.module.scss'; 并紧跟在它后面添加以下行:

    import { FormDisplayMode } from '@microsoft/sp-core-library';
    import {
      SPHttpClient,
      SPHttpClientResponse
    } from '@microsoft/sp-http';
    
  6. HelloWorldFormCustomizer 类中包括_item_etag专用类型,如此代码片段所示。 请注意,类定义已存在于代码中。

    // This already exists in YOUR code
    export default class HelloWorldFormCustomizer
      extends BaseFormCustomizer<IHelloWorldFormCustomizerProperties> {
    
    // Added for the item to show in the form; use with edit and view form
    private _item: {
      Title?: string;
    };
    // Added for item's etag to ensure integrity of the update; used with edit form
    private _etag?: string;
    
  7. 按如下所示更新 onInit () 方法。 此代码使用 this.displayMode 来确定呈现状态,然后根据需要提取所选列表项。

    public onInit(): Promise<void> {
      if (this.displayMode === FormDisplayMode.New) {
        // we're creating a new item so nothing to load
        return Promise.resolve();
      }
    
      // load item to display on the form
      return this.context.spHttpClient
        .get(this.context.pageContext.web.absoluteUrl + `/_api/web/lists/getbytitle('${this.context.list.title}')/items(${this.context.itemId})`, SPHttpClient.configurations.v1, {
          headers: {
            accept: 'application/json;odata.metadata=none'
          }
        })
        .then(res => {
          if (res.ok) {
            // store etag in case we'll need to update the item
            this._etag = res.headers.get('ETag');
            return res.json();
          }
          else {
            return Promise.reject(res.statusText);
          }
        })
        .then(item => {
          this._item = item;
          return Promise.resolve();
        });
    }
    
  8. 更新 render () 方法,如下所示。 仅以显示模式或编辑模式呈现窗体,具体取决于窗体的显示模式。 在这种情况下,我们使用相同的呈现效果来获得新的和编辑体验,但如果需要,你可以轻松获得专用选项。

      public render(): void {
        // render view form
        if (this.displayMode === FormDisplayMode.Display) {
    
          this.domElement.innerHTML =
                        `<div class="${styles.basics}">
                          <label for="title">${strings.Title}</label>
                          <br />
                            ${this._item?.Title}
                          <br />
                          <br />
                          <input type="button" id="cancel" value="${strings.Close}" />
                        </div>`;
    
          document.getElementById('cancel').addEventListener('click', this._onClose.bind(this));
        }
        // render new/edit form
        else {
          this.domElement.innerHTML =
                      `<div class="${styles.basics}">
                        <label for="title">${strings.Title}</label><br />
                        <input type="text" id="title" value="${this._item?.Title || ''}"/>
                        <br />
                        <br />
                        <input type="button" id="save" value="${strings.Save}" />
                        <input type="button" id="cancel" value="${strings.Cancel}" />
                        <br />
                        <br />
                        <div class="${styles.error}"></div>
                      </div>`;
    
          document.getElementById('save').addEventListener('click', this._onSave.bind(this));
          document.getElementById('cancel').addEventListener('click', this._onClose.bind(this));
        }
      }
    
  9. 按如下所示更新 HelloWorldFormCustomizer 类中的_onSave方法。

    private _onSave = async (): Promise<void> => {
      // disable all input elements while we're saving the item
      this.domElement.querySelectorAll('input').forEach(el => el.setAttribute('disabled', 'disabled'));
      // reset previous error message if any
      this.domElement.querySelector(`.${styles.error}`).innerHTML = '';
    
      let request: Promise<SPHttpClientResponse>;
      const title: string = (document.getElementById('title') as HTMLInputElement).value;
    
      switch (this.displayMode) {
        case FormDisplayMode.New:
          request = this._createItem(title);
          break;
        case FormDisplayMode.Edit:
          request = this._updateItem(title);
      }
    
      const res: SPHttpClientResponse = await request;
    
      if (res.ok) {
        // You MUST call this.formSaved() after you save the form.
        this.formSaved();
      }
      else {
        const error: { error: { message: string } } = await res.json();
    
        this.domElement.querySelector(`.${styles.error}`).innerHTML = `An error has occurred while saving the item. Please try again. Error: ${error.error.message}`;
        this.domElement.querySelectorAll('input').forEach(el => el.removeAttribute('disabled'));
      }
    }
    
  10. 将新方法 _createItem 添加到 HelloWorldFormCustomizer 类。

    private _createItem(title: string): Promise<SPHttpClientResponse> {
      return this.context.spHttpClient
        .post(this.context.pageContext.web.absoluteUrl + `/_api/web/lists/getByTitle('${this.context.list.title}')/items`, SPHttpClient.configurations.v1, {
          headers: {
            'content-type': 'application/json;odata.metadata=none'
          },
          body: JSON.stringify({
            Title: title
          })
        });
    }
    
  11. 将新方法 _updateItem 添加到 HelloWorldFormCustomizer 类。

    private _updateItem(title: string): Promise<SPHttpClientResponse> {
      return this.context.spHttpClient
        .post(this.context.pageContext.web.absoluteUrl + `/_api/web/lists/getByTitle('${this.context.list.title}')/items(${this.context.itemId})`, SPHttpClient.configurations.v1, {
          headers: {
            'content-type': 'application/json;odata.metadata=none',
            'if-match': this._etag,
            'x-http-method': 'MERGE'
          },
          body: JSON.stringify({
            Title: title
          })
        });
    }
    

现在,代码已完成,支持最少的“新建”、“编辑”和“显示”体验,并且你可以使用不同的配置来测试不同的体验进行调试。

SharePoint 上下文中的自定义窗体

扩展的部署

只要准备好开始使用组件,就需要考虑几个与内容类型的组件关联相关的步骤。 部署步骤如下:

  1. 将解决方案部署到 SharePoint 应用程序目录
  2. 如果未使用租户范围的部署,请将解决方案安装到要使用扩展的网站集
  3. 使用 ContentType 对象中的特定属性将自定义组件关联到内容类型。 有几个选项可以执行此操作:
    1. 如果使用站点范围的部署选项,则可以从解决方案中预配使用的列表和内容类型
    2. 可以使用 REST 或 CSOM API 将组件关联到内容类型。 请注意,如果在网站集或内容类型中心级别关联组件,则会自动将其继承到所有新的内容类型实例