Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions docs/spfx/extensions/get-started/building-form-customizer.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Build your first Form customizer extension
description: Form customizers are SharePoint Framework components giving you an option to override the form experience at a list or library level by associating the component with the used content type.
ms.date: 11/14/2025
ms.date: 01/06/2025
ms.custom: scenarios:getting-started
---

Expand All @@ -15,8 +15,6 @@ Form customizers are SharePoint Framework components that give you an option to
> [!TIP]
> You can find the output from this tutorial from [GitHub](https://github.com/pnp/spfx-reference-scenarios/tree/main/samples/spfx-formcustomizer-basics).

[!INCLUDE [spfx-gulp-heft-migration-wip](../../../../includes/snippets/spfx-gulp-heft-migration-wip.md)]

## Create an extension project

1. Create a new project directory in your favorite location.
Expand Down Expand Up @@ -144,7 +142,7 @@ You can test and debug your Form Customizer within a live SharePoint Online site

Let's call out a few specific topics from the **serve.json** file

- You can see multiple different configurations that can be used to debug new, edit, and view forms with specific query parameter differences. You can define the used configuration in your gulp serve command, for example, as `gulp serve --config=helloWorld_EditForm`
- You can see multiple different configurations that can be used to debug new, edit, and view forms with specific query parameter differences. You can define the used configuration in your **heft start** command, for example, as `heft start --serve-config helloWorld_EditForm`
- componentId is automatically associated to be the first list formatting component in your solution (if you have multiple components)
- To simplify the debugging, you do not need to define the target content type `id` to which the component is associated, but in the runtime, the association is performed in the content type level by updating at least one of the following properties in the content type:
- ContentType.**NewFormClientSideComponentId** - component id for new form
Expand All @@ -157,7 +155,7 @@ You can test and debug your Form Customizer within a live SharePoint Online site
1. Compile your code and host the compiled files from the local machine by running this command:

```console
gulp serve
heft start
```

When the code compiles without errors, it serves the resulting manifest from **https://localhost:4321**.
Expand Down Expand Up @@ -187,6 +185,11 @@ Now that we have created the baseline component and tested that it works properl
Close: string;
Title: string;
}

declare module 'HelloWorldFormCustomizerStrings' {
const strings: IHelloWorldFormCustomizerStrings;
export = strings;
}
```

1. Open the **./src/extensions/helloWorld/loc/en-us.js** file, and add new **Title** string to the file. File content should be as follows after your edits.
Expand All @@ -205,7 +208,7 @@ Now that we have created the baseline component and tested that it works properl
1. Open the **./src/extensions/helloWorld/HelloWorldFormCustomizer.module.scss** file, and update the styling definition as follows. We are adding error styling for the component.

```scss
.helloWorld {
.basics {
background-color: "[theme:white, default:#ffffff]";
color: "[theme:themePrimary, default:#0078d4]";
padding: 0.5rem;
Expand All @@ -225,6 +228,7 @@ Now that we have created the baseline component and tested that it works properl
SPHttpClient,
SPHttpClientResponse
} from '@microsoft/sp-http';
import * as strings from 'HelloWorldFormCustomizerStrings';
```

1. Include **_item** and **_etag** private types inside of the **HelloWorldFormCustomizer** class as shown in this code snippet. Notice that the class definition already exists in your code.
Expand All @@ -237,7 +241,7 @@ Now that we have created the baseline component and tested that it works properl
// 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;
```
Expand All @@ -261,7 +265,7 @@ Now that we have created the baseline component and tested that it works properl
.then(res => {
if (res.ok) {
// store etag in case we'll need to update the item
this._etag = res.headers.get('ETag');
this._etag = res.headers.get('ETag') || undefined;
return res.json();
}
else {
Expand Down Expand Up @@ -292,7 +296,7 @@ Now that we have created the baseline component and tested that it works properl
<input type="button" id="cancel" value="${strings.Close}" />
</div>`;

document.getElementById('cancel').addEventListener('click', this._onClose.bind(this));
document.getElementById('cancel')?.addEventListener('click', this._onClose.bind(this));
}
// render new/edit form
else {
Expand All @@ -309,8 +313,8 @@ Now that we have created the baseline component and tested that it works properl
<div class="${styles.error}"></div>
</div>`;

document.getElementById('save').addEventListener('click', this._onSave.bind(this));
document.getElementById('cancel').addEventListener('click', this._onClose.bind(this));
document.getElementById('save')?.addEventListener('click', this._onSave.bind(this));
document.getElementById('cancel')?.addEventListener('click', this._onClose.bind(this));
}
}
```
Expand All @@ -322,17 +326,23 @@ Now that we have created the baseline component and tested that it works properl
// 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 = '';
const errorElement = this.domElement.querySelector(`.${styles.error}`);
if (errorElement) {
errorElement.innerHTML = '';
}

let request: Promise<SPHttpClientResponse>;
const title: string = (document.getElementById('title') as HTMLInputElement).value;


let request: Promise<SPHttpClientResponse>;
switch (this.displayMode) {
case FormDisplayMode.New:
request = this._createItem(title);
break;
case FormDisplayMode.Edit:
request = this._updateItem(title);
break;
default:
return;
}

const res: SPHttpClientResponse = await request;
Expand All @@ -344,7 +354,10 @@ Now that we have created the baseline component and tested that it works properl
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}`;
const errorElement = this.domElement.querySelector(`.${styles.error}`);
if (errorElement) {
errorElement.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'));
}
}
Expand Down Expand Up @@ -374,7 +387,7 @@ Now that we have created the baseline component and tested that it works properl
.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,
'if-match': this._etag || '*',
'x-http-method': 'MERGE'
},
body: JSON.stringify({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Build your first ListView Command Set extension
description: Create an extension project, and then code and debug your extension by using SharePoint Framework (SPFx) Extensions.
ms.date: 12/14/2023
ms.date: 01/06/2025
ms.custom: scenarios:getting-started
---
# Build your first ListView Command Set extension
Expand All @@ -12,8 +12,6 @@ You can follow these steps by watching the video on the Microsoft 365 Platform C

> [!Video https://www.youtube.com/embed/uaUGtLrNbRA]

[!INCLUDE [spfx-gulp-heft-migration-wip](../../../../includes/snippets/spfx-gulp-heft-migration-wip.md)]

## Create an extension project

1. Create a new project directory in your favorite location.
Expand Down Expand Up @@ -186,7 +184,7 @@ You cannot currently use the local Workbench to test SharePoint Framework Extens
1. Compile your code and host the compiled files from the local machine by running this command:

```console
gulp serve
heft start
```

When the code compiles without errors, it serves the resulting manifest from **https://localhost:4321**.
Expand Down Expand Up @@ -248,7 +246,7 @@ The default solution takes advantage of a new Dialog API, which can be used to s
1. In your console window, ensure that you do not have any exceptions. If you do not already have the solution running in localhost, execute the following command:

```console
gulp serve
heft start
```

1. Accept the loading of debug manifests by selecting **Load debug scripts** when prompted.
Expand Down Expand Up @@ -363,13 +361,13 @@ Since solutions will by default use the **asset packaging** capability, your Jav
1. In the console window, enter the following command to package your client-side solution that contains the extension so that we get the basic structure ready for packaging:

```console
gulp bundle --ship
heft build --production
```

1. Execute the following command so that the solution package is created:

```console
gulp package-solution --ship
heft package-solution --production
```

The command creates the following package: **./sharepoint/solution/command-extension.sppkg** folder:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Build your first Field Customizer extension
description: Extensions are client-side components that run inside the context of a SharePoint page. Extensions can be deployed to SharePoint Online, and you can use modern JavaScript tools and libraries to build them.
ms.date: 09/18/2025
ms.date: 01/09/2026
ms.custom: scenarios:getting-started
---
# Build your first Field Customizer extension
Expand All @@ -12,8 +12,6 @@ You can follow these steps by watching the video on the Microsoft 365 Platform C

> [!Video https://www.youtube.com/embed/mBZ7Sq_KfDA]

[!INCLUDE [spfx-gulp-heft-migration-wip](../../../../includes/snippets/spfx-gulp-heft-migration-wip.md)]

## Create an extension project

1. Create a new project directory in your favorite location.
Expand Down Expand Up @@ -165,7 +163,7 @@ You can't use the local Workbench to test SharePoint Framework Extensions. You n
1. Compile your code and host the compiled files from the local machine by running this command:

```console
gulp serve
heft start
```

When the code compiles without errors, it serves the resulting manifest from **https://localhost:4321**.
Expand Down Expand Up @@ -220,10 +218,10 @@ Now that we've successfully tested the out-of-the-box starting point of the Fiel
1. In the console window, ensure that you don't have any errors. If the solution isn't running, execute the following task:

```console
gulp serve
heft start
```

1. In your previously created list, refresh the browser window with the debugging query parameters or restart the browser with **gulp serve**.
1. In your previously created list, refresh the browser window with the debugging query parameters or restart the browser with **heft start**.
1. Accept the loading of debug manifests by selecting **Load debug scripts** when prompted.

![Accept loading debug scripts](../../../images/ext-field-accept-debug-scripts.png)
Expand Down Expand Up @@ -320,13 +318,13 @@ Now you're ready to deploy the solution to a SharePoint site and get the field a
1. In the console window, enter the following command to package your client-side solution that contains the extension so that we get the basic structure ready for packaging:

```console
gulp bundle --ship
heft build --production
```

1. Execute the following command so that the solution package is created:

```console
gulp package-solution --ship
heft package-solution --production
```

The command creates the package: **./sharepoint/solution/field-extension.sppkg**:
Expand Down Expand Up @@ -374,3 +372,4 @@ The process for publishing your app is identical among the different extension t

- [Build your first ListView Command Set extension](building-simple-cmdset-with-dialog-api.md)
- [Overview of SharePoint Framework Extensions](../overview-extensions.md)