SPFx – Form Customizer

Introduction

Hi friends, today we are gonna see how we can customize the list forms (New, Edit and Display) using SPFx extensions. Recently there was an latest update released to the SharePoint Framework 1.15.0 which includes a new extension named Form Customizer, which helps to customize the default list forms. The version is still in beta, there might be slight change or no change when it is released as GA.

Video will be available soon…

Focus on the Code

I have created a list named News with a default Title field. Let us see how we can use the Form Customizer to render the different list forms.

Below are the tool chain that is used to create the extension explained below

  • SharePoint Framework v1.15.0-beta.6
  • Node v14.17.6
  • npm v6.14.15
  • gulp cli version 2.3.0
  • Yo version 4.3.0

Let us start by creating a new SPFx webpart project using SharePoint yeoman generator, before that create a folder where you would like to create the web part.

You can also follow the documentation from Microsoft to create the new extension project.

SPFx – How to remove the Modern page title in SharePoint Online using Command Extension | Knowledge Share (spknowledge.com)

Navigate to the folder and run the below command.

yo @microsoft/sharepoint

The generator will asks you couple of questions

  • What is your solution name?
  • Which type of client-side component to create? Choose Extension
  • Which type of client-side extension to create? Choose Form Customizer
  • What is your Form Customizer name?
  • Which template would you like to use? Choose React

Note: Once the extension is created, make sure to test the vanilla extension working without any issue. Always test the vanilla project to execute and test whether its working or not before implementing the business logics.

Following are the npm packages that are installed on top of the inbuilt packages added.

  • @pnp/queryable – v3.2.0
  • @pnp/sp – v3.2.0

Note: To use the above mentioned version, there are few modifications that has to be done on the project. Pleae follow the link Getting Started – PnP/PnPjs. If you are not comfortable with the version 3, you can always use version 2. If you are using version 2, you should install only @pnp/sp package of version 2.x.x.

I had created the extension named TestCustomizer.

Below is the TestCustomizerFormCustomizer.ts file with some modifications

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Log } from '@microsoft/sp-core-library';
import {
    BaseFormCustomizer
} from '@microsoft/sp-listview-extensibility';
import { SPFI, spfi, SPFx } from "@pnp/sp";
import * as strings from 'TestCustomizerFormCustomizerStrings';
import TestCustomizer, { ITestCustomizerProps } from './components/TestCustomizer';

/**
 * If your field customizer uses the ClientSideComponentProperties JSON input,
 * it will be deserialized into the BaseExtension.properties object.
 * You can define an interface to describe it.
 */
export interface ITestCustomizerFormCustomizerProperties {
    // This is an example; replace with your own property
    sampleText?: string;
}

const LOG_SOURCE: string = 'TestCustomizerFormCustomizer';

export default class TestCustomizerFormCustomizer
    extends BaseFormCustomizer<ITestCustomizerFormCustomizerProperties> {

    private sp: SPFI = undefined;

    public onInit(): Promise<void> {
        // Add your custom initialization to this method. The framework will wait
        // for the returned promise to resolve before rendering the form.
        Log.info(LOG_SOURCE, 'Activated TestCustomizerFormCustomizer with properties:');
        Log.info(LOG_SOURCE, JSON.stringify(this.properties, undefined, 2));
        this.sp = spfi().using(SPFx(this.context));
        return Promise.resolve();
    }

    public render(): void {
        // Use this method to perform your custom rendering.

        const testCustomizer: React.ReactElement<{}> =
            React.createElement(TestCustomizer, {
                sp: this.sp,
                //context: this.context,
                listGuid: this.context.list.guid,
                itemID: this.context.itemId,
                displayMode: this.displayMode,
                onSave: this._onSave,
                onClose: this._onClose
            } as ITestCustomizerProps);

        ReactDOM.render(testCustomizer, this.domElement);
    }

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

    private _onSave = (): void => {

        // You MUST call this.formSaved() after you save the form.
        this.formSaved();
    }

    private _onClose = (): void => {
        // You MUST call this.formClosed() after you close the form.
        this.formClosed();
    }
}
  • The default class extends BaseFormCustomizer which is the base class for customizing the list forms.
  • Following are the methods that are newly added for the Form Customizer extension
    • formSaved – This method is to inform the application that the form has been saved. As of now when we call this method it will redirect to the Home page, maybe in future it may redirect to the respective list or it may remain the same.
    • formClosed – This method is to inform the application that the form is closed/cancelled. As of now when we call this method it will redirect to the Home page, maybe in future it may redirect to the respective list or it may remain the same.
  • Added the imports for @pnp/sp
  • Created a private variable to store the SPFI.
  • Setup the spfx context in the onInit method.
  • Passed the below properties to the actual component file
    • sp – pnp connection
    • listGuid – GUID of the list
    • itemID – Item ID of a particular item
    • displayMode – Display mode whether it is New – 8, Edit – 6 or Display – 4.
    • onSave – formSaved method
    • onClose – formClosed method

Below is the TestCustomizer.tsx file which is the actual component that will be displayed.

import * as React from 'react';
import { useEffect } from 'react';
import { Log, FormDisplayMode, Guid } from '@microsoft/sp-core-library';
import styles from './TestCustomizer.module.scss';
import { SPFI } from '@pnp/sp';
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import NewForm from './NewForm';
import EditForm from './EditForm';
import DisplayForm from './DisplayForm';

export interface ITestCustomizerProps {
    sp: SPFI;
    //context: FormCustomizerContext;
    listGuid: Guid;
    itemID: number;
    displayMode: FormDisplayMode;
    onSave: () => void;
    onClose: () => void;
}

const LOG_SOURCE: string = 'TestCustomizer';

const TestCustomizer: React.FC<ITestCustomizerProps> = (props) => {
    useEffect(() => {
        Log.info(LOG_SOURCE, 'React Element: TestCustomizer mounted');
        return () => {
            Log.info(LOG_SOURCE, 'React Element: TestCustomizer unmounted');
        }
    }, []);

    return (<div className={styles.testCustomizer}>
        {props.displayMode === FormDisplayMode.New &&
            <NewForm sp={props.sp} listGuid={props.listGuid} onSave={props.onSave}
                onClose={props.onClose} />
        }
        {props.displayMode === FormDisplayMode.Edit &&
            <EditForm sp={props.sp} listGuid={props.listGuid} itemId={props.itemID}
                onSave={props.onSave} onClose={props.onClose} />
        }
        {props.displayMode === FormDisplayMode.Display &&
            <DisplayForm sp={props.sp} listGuid={props.listGuid} itemId={props.itemID}
                onClose={props.onClose} />
        }
    </div>);
};

export default TestCustomizer;

The above code does nothing but will check the display mode from the props variables sent. Based on the display mode, the respective form is loaded. I have created 3 components NewForm, EditForm & DisplayForm. Let us see the form code below

NewForm.tsx

import * as React from 'react';
import { useState, FC } from 'react';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { SPFI } from '@pnp/sp';
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { Guid } from '@microsoft/sp-core-library';

export interface INewFormProps {
    sp: SPFI;
    listGuid: Guid;
    onSave: () => void;
    onClose: () => void;
}

const NewForm: FC<INewFormProps> = (props) => {
    const [title, setTitle] = useState<string>('');
    const [msg, setMsg] = useState<any>(undefined);

    const clearControls = () => {
        setTitle('');
    };

    const saveListItem = async () => {
        setMsg(undefined);
        await props.sp.web.lists.getById(props.listGuid.toString()).items.add({
            Title: title
        });
        setMsg({ scope: MessageBarType.success, Message: 'New item created successfully!' });
        clearControls();
    };

    return (
        <React.Fragment>
            <div>New Form</div>
            <div style={{ margin: '10px' }}>
                <TextField label="Enter Title:" value={title} onChange={(e, v) => setTitle(v)} />
                <PrimaryButton text="Save" onClick={saveListItem} />
            </div>
            {msg && msg.Message &&
                <MessageBar messageBarType={msg.scope ? msg.scope : MessageBarType.info}>{msg.Message}</MessageBar>
            }
        </React.Fragment>
    );
};

export default NewForm;

The above form will be loaded when the page type is configured as New Form. The form is very simple and it has one text box to capture the Title, a button to Save the value to a new item in the list and a MessageBar to display the success message. The List Guid is passed as a properties from the main component.

EditForm.tsx

import * as React from 'react';
import { useEffect, useState, FC } from 'react';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { SPFI } from '@pnp/sp';
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { Guid } from '@microsoft/sp-core-library';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';

export interface IEditFormProps {
    sp: SPFI;
    listGuid: Guid;
    itemId: number;
    onSave: () => void;
    onClose: () => void;
}

const EditForm: FC<IEditFormProps> = (props) => {
    const [title, setTitle] = useState<string>('');
    const [msg, setMsg] = useState<any>(undefined);

    const saveListItem = async () => {
        await props.sp.web.lists.getById(props.listGuid.toString()).items.getById(props.itemId).update({
            Title: title
        });
        setMsg({ scope: MessageBarType.success, Message: 'Save successfull!' });
    };

    const populateItemForEdit = async () => {
        if (props.itemId) {
            let itemToUpdate: any = await props.sp.web.lists.getById(props.listGuid.toString()).items
                .select('ID', 'Title')
                .getById(props.itemId)();
            if (itemToUpdate) {
                setTitle(itemToUpdate?.Title);
            }
        } else {
            setMsg({ scope: MessageBarType.error, Message: 'Sorry, item not found!' });
        }
    };

    useEffect(() => {
        populateItemForEdit();
    }, []);

    return (
        <React.Fragment>
            <div>Edit Form</div>
            <div style={{ margin: '10px' }}>
                <TextField label="Enter Title:" value={title} onChange={(e, v) => setTitle(v)} />
                <PrimaryButton text="Save" onClick={saveListItem} />
            </div>
            {msg && msg.Message &&
                <MessageBar messageBarType={msg.scope ? msg.scope : MessageBarType.info}>{msg.Message}</MessageBar>
            }
        </React.Fragment>
    );
};

export default EditForm;

The above form will be loaded when the page type is configured as Edit Form. This form is pretty much same as the New Form except instead of creating an item, it will load the item info based on the Item ID passed and it will update the item back in the list. The List Guid and Item ID is passed as a properties from the main component.

DisplayForm.tsx

import * as React from 'react';
import { useEffect, useState, FC } from 'react';
import { SPFI } from '@pnp/sp';
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { Guid } from '@microsoft/sp-core-library';
import { Label } from 'office-ui-fabric-react/lib/Label';

export interface IDisplayFormProps {
    sp: SPFI;
    listGuid: Guid;
    itemId: number;
    onClose: () => void;
}

const DisplayForm: FC<IDisplayFormProps> = (props) => {
    const [title, setTitle] = useState<string>('');

    const populateItemForDisplay = async () => {
        let itemToUpdate: any = await props.sp.web.lists.getById(props.listGuid.toString()).items
            .select('ID', 'Title')
            .getById(props.itemId)();
        if (itemToUpdate) {
            setTitle(itemToUpdate?.Title);
        }
    };

    useEffect(() => {
        populateItemForDisplay();
    }, []);

    return (
        <React.Fragment>
            <div>Display Form</div>
            <div style={{ margin: '10px' }}>
                <b>Title: </b>&nbsp;<Label>{title}</Label>
            </div>
        </React.Fragment>
    );
};

export default DisplayForm;

The above form will be loaded when the page type is configured as Display Form. This form just has a label to display the selected item Title. The List Guid and Item ID is passed as a properties from the main component.

Serve.json

This is the file where we need to modify some of the properties to display various forms. For the Edit and Display form it is mandatory to pass the ID property and based on that we can get the particular item from the list. Since it is a beta version, we can test the different forms only by this way and there is still no information on the deployment process and how the forms will be mapped to the list and the content types. Following are the main properties that are required

  • pageUrl – URL must be updated to the actual tenant url, please do not update the entire URL since we need to load the SPListForm.aspx like how we load the Workbench.aspx
  • PageType – This is required to decide which form to be loaded. New – 8, Edit – 6 or Display – 4
  • RootFolder – Server relative URL to the list
  • ContentTypeID – Since the list can have multiple content types, need to specify the content type to which the forms must be associated.
  • ID – Item ID which is required for the Edit and Display form.
{
  "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
  "port": 4321,
  "https": true,
  "serveConfigurations": {
    "default": {
      "pageUrl": "https://contoso.sharepoint.com/sites/Demo/_layouts/15/SPListForm.aspx",
      "formCustomizer": {
        "componentId": "6276b62f-f87c-496a-b70c-76f3d917eaa0",
        "PageType": 8,
        "RootFolder": "/sites/Demo/Lists/News",
        "ContentTypeID": "0x0100B15FCCBACA110C499D2F2F4EF9884D100018EEF4E0088F4145BBE952DE94DC46B0",
        "ID": 1,
        "properties": {
          "sampleText": "Value"
        }
      }
    },
    "testCustomizer_NewForm": {
      "pageUrl": "https://contoso.sharepoint.com/sites/Demo/_layouts/15/SPListForm.aspx",
      "formCustomizer": {
        "componentId": "6276b62f-f87c-496a-b70c-76f3d917eaa0",
        "PageType": 8,
        "RootFolder": "/sites/Demo/Lists/News",
        "ContentTypeID": "0x0100B15FCCBACA110C499D2F2F4EF9884D100018EEF4E0088F4145BBE952DE94DC46B0",
        "properties": {
          "sampleText": "Value"
        }
      }
    },
    "testCustomizer_EditForm": {
      "pageUrl": "https://contoso.sharepoint.com/sites/mySite/_layouts/15/SPListForm.aspx",
      "formCustomizer": {
        "componentId": "6276b62f-f87c-496a-b70c-76f3d917eaa0",
        "PageType": 6,
        "RootFolder": "/sites/Demo/Lists/News",
        "ContentTypeID": "0x0100B15FCCBACA110C499D2F2F4EF9884D100018EEF4E0088F4145BBE952DE94DC46B0",
        "ID": 1,
        "properties": {
          "sampleText": "Value"
        }
      }
    },
    "testCustomizer_ViewForm": {
      "pageUrl": "https://contoso.sharepoint.com/sites/mySite/_layouts/15/SPListForm.aspx",
      "formCustomizer": {
        "componentId": "6276b62f-f87c-496a-b70c-76f3d917eaa0",
        "PageType": 4,
        "RootFolder": "/sites/Demo/Lists/News",
        "ContentTypeID": "0x0100B15FCCBACA110C499D2F2F4EF9884D100018EEF4E0088F4145BBE952DE94DC46B0",
        "ID": 1,
        "properties": {
          "sampleText": "Value"
        }
      }
    }
  }
}

Conclussion

I hope you had learned something new on the latest SharePoint Framework release on how to use Form Customizer to customize the list forms. As of now, the features are very limited and we all hope there will be more enhancement to the Form Customizer when it is released in GA or maybe in the later versions.

Please don’t forget to leave your comments and also plesae watch my technical videos on the below URL

You can refer to the source code here react-form-customizer

Knowledge Share

Happy Coding…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s