The edit panel for SPFx WebParts provides a new and consistent way to interact with the users. And although Microsoft has provided plenty of field types out of the box we still need to build our own some times. But what if we want to do something crazy? Like having an array of objects in our settings?


The easy part

Let’s say we store a list of users for a WebPart that shows Contact Persons. The first thing we would do is create a TypeScript model and extend the .manifest.json file. Something like this:

\src\models\personProperties.ts

export class PersonProperties {
  public guid: string;
  public firstName: string;
  public lastName: string; 
  public emailAddress: string; 
} 

\src\webparts\coolEditFields\CoolEditFieldsWebPart.manifest.json

"preconfiguredEntries": [{
  "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
  "group": { "default": "Other" },
  "title": { "default": "Cool edit fields" },
  "description": { "default": "WebPart with cool edit fields" },
  "officeFabricIconFontName": "Page",
  "properties": {
    "contactPersons": [
      { "guid": "3646e597-a5b0-448c-8fe4-e1c0ec5199d6", "firstName": "Dan", "lastName": "Deaconu", "emailAddress": "dan.deaconu@email.com" },
      { "guid": "09f550b5-42ce-4517-bf9f-eadde70dad7c","firstName": "Willy", "lastName": "Billy", "emailAddress": "willy.billy@email.com" }
     ]
  }
}]
The second thing to fix up is the interface for the WebPart properties, the interface for the WebPart Field Properties and React.createElement function:
\src\webaparts\coolEditFields\components\ICoolEditFieldsProps.ts
export interface ICoolEditFieldsProps {
 contactPersons:PersonProperties[];
}
Don’t forget to import your model 👍
import { PersonProperties } from "../../../models/personProperties";
\src\webparts\coolEditFields\CoolEditFieldsWebPart.ts
export interface ICoolEditFieldsWebPartProps {
 contactPersons:PersonProperties[];
}
And the React.createElement function (in the same file):
React.createElement(CoolEditFields, {
  contactPersons:this.properties.contactPersons,
});

💪 TIP you can use the spread operator if the properties are the same:
React.createElement(CoolEditFields, { ...this.properties });
How can we see the results?
Let’s update the WebPart to display the properties:
\src\webparts\coolEditFields\components\CoolEditFields.tsx
export default class CoolEditFields extends React.Component<ICoolEditFieldsProps, {}> {
  public render(): React.ReactElement<ICoolEditFieldsProps> {
    return (
      <div className={styles.coolEditFields}>
        <div className={styles.container}>
          {this.props.contactPersons.map(person => (
            <div className={styles.row}>
              <div className={styles.column}>
                <p className={styles.title}>
                  {person.firstName} {person.lastName}
                </p>
                {person.emailAddress} <br />
                {person.guid}
                <hr />
              </div>
            </div>
          ))}
        </div>
      </div>
    );
  }
}
Run gulp serve and the WebPart should look something like this:

The fun part

Settings are there, but how do we edit them? Time to create some components ⚡

Go ahead and create a new folder somewhere in your project. After that you will need the following files:

  • personsField.tsx
  • iPersonsField.ts
  • personsField.module.scss
  • persosnsField.module.scss

These files are for our custom React Component

  • propertyPanePersonsField.tsx
  • iPropertyPanePersonsField.ts
  • iPropertyPanePersonsFieldProps.ts
  • propertyPanePersonsField.tsx

These files tell our React Component how to talk with SharePoint


Let’s start with the interface for our React Component. This component will receive from SharePoint the property itself (in our case the array of persons), a callback when something has changed (similar to pushing state up) and a stateKey (required by SPFx, will trigger rerendering)

import { PersonProperties } from "../../../models/personProperties";

export interface IPersonsField {
   persons: PersonProperties[];
   onChanged: (persons:PersonProperties[]) =>void;
   stateKey:string;
}

The React Component will render an add button, the persons names with a small edit button and an add/edit modal. I won’t go into the details about React development, but here is the code (you’re gonna have to figure out the CSS on your own 😉)

import * as React from "react";
import { ActionButton, IconButton } from "office-ui-fabric-react/lib/Button";
import * as strings from "CoolEditFieldsWebPartStrings";
import { Dialog, DialogType, DialogFooter } from "office-ui-fabric-react/lib/Dialog";
import { DefaultButton } from "office-ui-fabric-react/lib/Button";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import { getGUID } from "@pnp/common";
import { find } from "@microsoft/sp-lodash-subset";
import { IPersonsField } from "./iPersonsField";
import { PersonProperties } from "../../../models/personProperties";
import styles from "./personsField.module.scss";

export interface IPersonsFieldState {
   openedModalItemGuid?: string;
   persons:PersonProperties[];
}

export default class PersonsField extends React.Component<IPersonsField, IPersonsFieldState> {
   constructor(props:IPersonsField) {
      super(props);
      this.state= {
         persons:this.props.persons,
      };
   }

   private onPersonPropertiesChanged (personGuid: string, propertyName: string, value:any) {
      var persons=this.state.persons;
     
      for (var column of persons) {
        if (column.guid === personGuid) {
           column[propertyName] =value;
        }
      }
   
      this.setState({ persons });
      this.props.onChanged(persons);
   }

   public render():JSX.Element {
     var selectedPerson = find(this.state.persons, (person: PersonProperties) => {
        return person.guid == this.state.openedModalItemGuid;
      });
   
   return (
    <div>
      <ActionButton
          iconProps={{ iconName:"DoubleColumn" }}
          onClick={() => {
            var newPersons = this.state.persons;
            newPersons.push({
               guid:getGUID(),
               firstName:"John",
               lastName:"Doe",
               emailAddress:"",
            });
            this.setState({
              persons:newPersons,
            });
            this.props.onChanged(newPersons);
        }}>{strings.create}</ActionButton>

   {this.state.persons.map(personProperties=> (
      <div className={styles.hvColumnSetting} key={personProperties.guid}>
         <div className={styles.hvColumnTitle}>
            {personProperties.firstName} {personProperties.lastName}
         </div>
         <IconButtoniconProps={{ iconName:"Edit" }} onClick={() => this.setState({ openedModalItemGuid:personProperties.guid })}/>
      </div>
   ))}
   {this.state.openedModalItemGuid ? (
      <Dialog hidden={false} onDismiss={() =>this.setState({ openedModalItemGuid:undefined })} minWidth={630} title={strings.modalHeader} type={DialogType.normal}>
         <TextField
            label={strings.firstName}
            value={selectedPerson.firstName}
            onChange={(_event, value) =>this.onPersonPropertiesChanged(selectedPerson.guid, "firstName", value)}
        />
        <TextField
           label={strings.lastName}
           value={selectedPerson.lastName}
           onChange={(_event, value) =>this.onPersonPropertiesChanged(selectedPerson.guid, "lastName", value)}
       />
       <TextField
           label={strings.emailAddress}
           value={selectedPerson.emailAddress}
           onChange={(_event, value) =>this.onPersonPropertiesChanged(selectedPerson.guid, "emailAddress", value)}
       />
      <DialogFooter>
         <DefaultButtononClick={() =>this.setState({ openedModalItemGuid:undefined })}text={strings.close}/>
     </DialogFooter>
    </Dialog>
   ) : null}
  </div>
  );
 }
}

So little code for so much logic? The magic of modern programming 🧙‍♂️

Let’s tell SharePoint how to talk with our component. Firstly we need the interface for our custom PropertyPane. This includes our data (in this case an array of PersonProperties) and a callback when the data has changed.

\src\webparts\coolEditFields\personsField\iPropertyPanePersonsField.ts

import { PersonProperties } from "../../../models/personProperties";

export interface IPropertyPanePersonsFieldProps {
  persons: PersonProperties[];
  onPropertyChange: (propertyPath: string, newValue: any) => void;
}
Secondly we need to merge our custom PropertyPane interface with the one from SharePoint

\src\webparts\coolEditFields\personsField\iPropertyPanePersonsFieldInternalProps.ts

import { IPropertyPanePersonsFieldProps } from "./iPropertyPanePersonsFieldProps";
import { IPropertyPaneCustomFieldProps } from "@microsoft/sp-webpart-base";

export interface IPropertyPanePersonsFieldInternalProps extends IPropertyPanePersonsFieldProps, IPropertyPaneCustomFieldProps {}
Lastly we create a new PropertyPane Component. This will merge our component with the properties from SharePoint, take care of rendering and callbacks.

\src\webparts\coolEditFields\personsField\propertyPanePersonsField.ts

import * as React from "react";
import * as ReactDom from "react-dom";
import { IPropertyPaneField, PropertyPaneFieldType } from "@microsoft/sp-webpart-base";
import { IPropertyPanePersonsFieldProps } from "./iPropertyPanePersonsFieldProps";
import { IPropertyPanePersonsFieldInternalProps } from "./iPropertyPanePersonsFieldInternalProps";
import { IPersonsField } from "./iPersonsField";
import { PersonProperties } from "../../../models/personProperties";
import PersonsField from "./personsField";

export class PropertyPanePersonsField implements IPropertyPaneField {
  public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
  public targetProperty: string;
  public properties: IPropertyPanePersonsFieldInternalProps;
  private elem: HTMLElement;

  constructor(targetProperty: string, properties: IPropertyPanePersonsFieldProps) {
    this.targetProperty = targetProperty;
    this.properties = {
      key: "personsPropertersSettingsField",
      onRender: this.onRender.bind(this),
      persons: properties.persons,
      onPropertyChange: properties.onPropertyChange,
    };
  }

  public render(): void {
    if (!this.elem) {
      return;
    }

    this.onRender(this.elem);
  }

  private onRender(elem: HTMLElement): void {
    if (!this.elem) {
      this.elem = elem;
    }

    const element: React.ReactElement = React.createElement(PersonsField, {
      persons: this.properties.persons,
      onChanged: this.onChanged.bind(this),
      // required to allow the component to be re-rendered by calling this.render() externally
      stateKey: new Date().toString(),
    });
    ReactDom.render(element, elem);
  }

  private onChanged(value: PersonProperties[]): void {
    this.properties.onPropertyChange(this.targetProperty, value);
  }
}

🎉 Now we can use it  🎉

Head over to the WebPart Component and add the custom component to getPropertyPaneConfiguration()

 groups: [
            { groupName: "Contact Persons",
               groupFields: [
                new PropertyPanePersonsField("columns", {
                  persons: this.properties.contactPersons,
                  onPropertyChange: this.updatePersonsSettings.bind(this),
                })] }
          ],

You’re gonna need the update function too 😉

private updatePersonsSettings(propertyPath: string, newValue: PersonProperties[]) {
    update(this.properties, propertyPath, () => {
      return newValue;
    });
    this.render();
  }

🚀 gulp serve and check it out 🚀

avatar

Full Stack Developer

Leave a comment

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Time limit is exhausted. Please reload the CAPTCHA.