SharePoint 2013 Modern WebPart

This post will give an overview of creating SharePoint webparts with a custom configuration using client-side code. This solution is designed for SharePoint 2013+ environments. The code for this project can be found in github.

Overview

I have an article going over the high-level details of this approach. I will walk you through the details of creating a SharePoint webpart with custom properties at the code level in this blog post. The focus of this solution is to demonstrate how to create a webpart with a custom configuration. We will be using the gd-sprest SharePoint Automation feature to create a list and webpart, and the gd-sprest-react library for the WebPart component.

1. Create the Project

I will be using the sp-scripts project as a starting point. Please refer to this blog post for an overview of this starter project. After cloning the project, clear the src folder.

2. Project Configuration (src/cfg.ts)

First we will define the project assets. In our case, we will create a contacts list and a webpart to display it in. For detailed information about the automation feature in the gd-sprest, refer to this post.

import { Helper, List, SPTypes } from "gd-sprest";

/**
 * Configuration
 */
export const Configuration = new Helper.SPConfig({
    // List Configuration
    ListCfg: [
        {
            // Custom fields for this list
            CustomFields: [
                {
                    Name: "MCCategory",
                    SchemaXml: '<Field ID="{3356AABA-7570-45C8-A200-601720F9E2C9}" Name="MCCategory" StaticName="MCCategory" DisplayName="Category" Type="Choice"><CHOICES><CHOICE>Business</CHOICE><CHOICE>Family</CHOICE><CHOICE>Personal</CHOICE></CHOICES></Field>'
                },
                {
                    Name: "MCPhoneNumber",
                    SchemaXml: '<Field ID="{DA322FB9-DD35-4DAC-8524-6017209BB414}" Name="MCPhoneNumber" StaticName="MCPhoneNumber" DisplayName="Phone Number" Type="Text" />'
                }
            ],

            // The list creation information
            ListInformation: {
                BaseTemplate: SPTypes.ListTemplateType.GenericList,
                Title: "My Contacts"
            },

            // Update the 'Title' field's display name
            TitleFieldDisplayName: "Full Name",

            // Update the default 'All Items' view
            ViewInformation: [
                {
                    ViewFields: ["MCCategory", "LinkTitle", "MCPhoneNumber"],
                    ViewName: "All Items",
                    ViewQuery: "<OrderBy><FieldRef Name='MCCategory' /><FieldRef Name='Title' /></OrderBy>"
                }
            ]
        }
    ],

    // WebPart Configuration
    WebPartCfg: [
        {
            FileName: "dev_wpDemo.webpart",
            Group: "Demo",
            XML: `<?xml version="1.0" encoding="utf-8"?>
<webParts>
    <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
        <metaData>
            <type name="Microsoft.SharePoint.WebPartPages.ScriptEditorWebPart, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
            <importErrorMessage>$Resources:core,ImportantErrorMessage;</importErrorMessage>
        </metaData>
        <data>
            <properties>
                <property name="Title" type="string">My Contacts</property>
                <property name="Description" type="string">Demo displaying my contacts.</property>
                <property name="ChromeType" type="chrometype">None</property>
                <property name="Content" type="string">
                    &lt;script type="text/javascript" src="/sites/dev/siteassets/webpartdemo/wpDemo.js"&gt;&lt;/script&gt;
                    &lt;div id="wp-contacts"&gt;&lt;/div&gt;
                    &lt;div id="wp-contactsCfg" style="display: none;"&gt;&lt;/div&gt;
                    &lt;script type="text/javascript"&gt;SP.SOD.executeOrDelayUntilScriptLoaded(function() { new WebPartDemo(); }, 'wpDemo.js');&lt;/script&gt;
                </property>
            </properties>
        </data>
    </webPart>
</webParts>`
        }
    ]
});

3. Create Test Data (src/scripts/cfg/ts)

This part isn’t needed, but will make demoing a lot easier. This code will generate sample data for the list.

// Method to add test data
Configuration.addTestData = () => {
    // Get the list
    let list = new List("My Contacts");

    // Define the list of names
    let names = [
        "John A. Doe",
        "Jane B. Doe",
        "John C. Doe",
        "Jane D. Doe",
        "John E. Doe",
        "Jane F. Doe",
        "John G. Doe",
        "Jane H. Doe",
        "John I. Doe",
        "Jane J. Doe"
    ];

    // Loop 10 item
    for (let i = 0; i < 10; i++) {
        // Set the category
        let category = "";
        switch (i % 3) {
            case 0:
                category = "Business";
                break;
            case 1:
                category = "Family";
                break;
            case 2:
                category = "Personal";
                break;
        }


        // Add the item
        list.Items().add({
            MCCategory: category,
            MCPhoneNumber: "nnn-nnn-nnnn".replace(/n/g, i.toString()),
            Title: names[i]
        })
            // Execute the request, but wait for the previous request to complete
            .execute((item) => {
                // Log
                console.log("[WP Demo] Test item '" + item["Title"] + "' was created successfully.");
            }, true);
    }

    // Wait for the requests to complete
    list.done(() => {
        // Log
        console.log("[WP Demo] The test data has been added.");
    });
};

4. Create the WebPart Components

The common component of the gd-sprest-react library has a webpart component which works in webpart, wiki and publishing pages. This section will go over how to implement it.

4.a WebPart Configuration (src/wpCfg.tsx)

The webpart configuration component will be displayed when the page is being edited. To make life easier, we will inherit the webpart configuration panel component from the gd-sprest-react library. I’ll break the file down into pieces.

Imported Components
* react – The react library
* gd-srest – Used to query the current web for the lists.
* gd-sprest-react – Used to inherit the webpart configuration panel.
* office-ui-fabric-react – Components used to create the configuration panel.

import * as React from "react";
import { Web } from "gd-sprest";
import { WebPartConfigurationPanel, IWebPartCfg, IWebPartConfigurationProps, IWebPartConfigurationState } from "gd-sprest-react";
import { Dropdown, IDropdownOption, PrimaryButton, Spinner } from "office-ui-fabric-react";

Interfaces
* IDemoWebPartCfg – Inherits the webpart configuration. This is where you define the custom configuration for you webpart.
* Properties – Inherits the webpart properties. Override the configuration property with the custom webpart configuration interface.
* State – Inherits the webpart state. Override the configuration property with the custom webpart configuration interface. This is where you define the state variables for your components.

/**
 * Demo WebPart Configuration
 */
export interface IDemoWebPartCfg extends IWebPartCfg {
    ListName: string;
}

/**
 * Properties
 */
interface Props extends IWebPartConfigurationProps {
    cfg: IDemoWebPartCfg;
}

/**
 * State
 */
interface State extends IWebPartConfigurationState {
    cfg: IDemoWebPartCfg;
    lists: Array<IDropdownOption>;
}

WebPart Configuration Class
Inherits the webpart configuration panel. The base constructor will already set the configuration in the state to an empty object if it doesn’t exist. We will set the default values to our state variables, and load the lists from the current web.

/**
 * WebPart Configuration
 */
export class WebPartCfg extends WebPartConfigurationPanel<Props, State> {
    /**
     * Constructor
     */
    constructor(props: Props) {
        super(props);

        // Set the state
        this.state = {
            cfg: this.state.cfg,
            lists: null
        };

        // Load the lists
        this.load();
    }

Panel Contents
The class will require you to define this method. If the lists haven’t been loaded, we will display a spinner; otherwise we will display a dropdown list for the user to select a list from.

    // Method to render the component
    onRenderContents = (cfg: IDemoWebPartCfg) => {
        // See if the lists have been loaded
        if (this.state.lists) {
            return (
                <div>
                    <Dropdown
                        label="List:"
                        onChanged={this.updateListName}
                        options={this.state.lists}
                        selectedKey={cfg ? cfg.ListName : ""}
                    />
                    <PrimaryButton text="Save" onClick={this.save} />
                </div>
            );
        }

        // Return a loading image
        return (
            <Spinner label="Loading the lists..." />
        );
    }

Methods
* load – Loads the lists from the current web and updates the state variable.
* save – Saves the webpart configuration.
* updateListName – Updates the list name in the state’s configuration variable.

    /**
     * Methods
     */

    // Method to load the lists from the current web
    private load = () => {
        // Get the current web
        (new Web())
            // Get the lists
            .Lists()
            // Execute the request
            .execute((lists) => {
                let options: Array<IDropdownOption> = [];

                // Parse the lists
                for (let i = 0; i < lists.results.length; i++) {
                    let list = lists.results[i];

                    // Add the option
                    options.push({
                        key: list.Title,
                        text: list.Title
                    });
                }

                // Update the state
                this.setState({
                    lists: options
                });
            });
    }

// Method to save the configuration
    private save = (ev: React.MouseEvent<HTMLButtonElement>) => {
        // Prevent postback
        ev.preventDefault();

        // Save the webpart configuration
        this.saveConfiguration(this.state.cfg);
    };

// Method to update the list name
    private updateListName = (option?: IDropdownOption) => {
        // Update the configuration
        let cfg = this.state.cfg;
        cfg.ListName = option.text;

        // Update the state
        this.setState({ cfg });
    }
}
4.b WebPart (src/wp.tsx)

The webpart component will be displayed by default. I’ll break the file down into pieces.

Imported Components
* react – The react library.
* gd-srest – Used to query the current list from the configuration.
* office-ui-fabric-react – Components used to create the webpart.
* wpCfg – Used to import the webpart configuration. This will be used for intellisense.
* wpDemo.scss – The custom styling for this component.

import * as React from "react";
import { List } from "gd-sprest";
import { DetailsList, Pivot, PivotItem } from "office-ui-fabric-react";
import { IDemoWebPartCfg } from "./wpCfg";
import "./wpDemo.scss";

Interfaces
* IContact – The intellisense for the list item.
* Props – The webpart properties.
* State – The webpart state variables.

/**
 * Contact
 */
interface IContact {
    MCCategory: string;
    MCPhoneNumber: string;
    ID: number;
    Title: string;
}

/**
 * Properties
 */
interface Props {
    cfg: IDemoWebPartCfg;
}

/**
 * State
 */
interface State {
    contacts: Array<IContact>;
    selectedTab: string;
}

WebPart
This webpart constructor will set the state variable default values, and load the list data. The render method will return the list data using tabs.

/**
 * Contacts WebPart
 */
export class ContactsWebPart extends React.Component<Props, State> {
    // Constructor
    constructor(props: Props) {
        super(props);

        // Set the state
        this.state = {
            contacts: [],
            selectedTab: "Business"
        };

        // Load the list data
        this.load();
    }

    // Render the component
    render() {
        return (
            <Pivot onLinkClick={this.updateContacts}>
                <PivotItem linkText="Business">
                    {this.renderContacts()}
                </PivotItem>
                <PivotItem linkText="Family">
                    {this.renderContacts()}
                </PivotItem>
                <PivotItem linkText="Personal">
                    {this.renderContacts()}
                </PivotItem>
            </Pivot>
        );
    }

Methods
* load – Loads the list data.
* renderContacts – The render method for each item in the list view.
* updateContacts – Updates the contacts based on the selected tab.

    /**
     * Methods
     */

    // Method to load the list data
    private load = () => {
        // Get the list
        (new List(this.props.cfg.ListName))
            // Get the items
            .Items()
            // Set the query
            .query({
                OrderBy: ["MCCategory", "Title"],
                Select: ["MCCategory", "MCPhoneNumber", "Title"],
                Top: 500
            })
            // Execute the request
            .execute((items) => {
                // Update the state
                this.setState({
                    contacts: items.results || [] as any
                });
            });
    }

    // Method to render the contacts
    private renderContacts = () => {
        let contacts = [];

        // Parse the contacts
        for (let i = 0; i < this.state.contacts.length; i++) {
            let contact = this.state.contacts[i];

            // See if this is a contact we are rendering
            if (contact.MCCategory == this.state.selectedTab) {
                // Add the contact
                contacts.push({
                    "Full Name": contact.Title,
                    "Phone Number": contact.MCPhoneNumber
                });
            }
        }

        // Return the contacts
        return (
            contacts.length == 0 ?
                <h3>No '{this.state.selectedTab}' contacts exist</h3>
                :
                <DetailsList className="contacts-list" items={contacts} />
        );
    }

    // Method to update the contacts
    private updateContacts = (link: PivotItem, ev: React.MouseEvent<HTMLElement>) => {
        // Prevent postback
        ev.preventDefault();

        // Update the state
        this.setState({
            selectedTab: link.props.linkText
        });
    }
}

5. Global Variable (src/index.tsx)

This is where we pull everything together. We will use the WebPart component from the gd-sprest-react library, and reference the webpart components we created earlier. Let’s break this file down.

Constructor

The constructor will create an instance of the webpart, where we define the following:
* Configuration – The static reference to the configuration element. This will be used to install/uninstall the demo list and webpart.
* cfgElementId – The element id of the webpart configuration element.
* displayElement – The component to render by default.
* editElement – The component to render when the page is being edited.
* targetElementId – The target element to render the webpart to.

import * as React from "react";
import { WebPart } from "gd-sprest-react";
import { Configuration } from "./cfg";
import { ContactsWebPart } from "./wp";
import { WebPartCfg } from "./wpCfg";

/**
 * WebPart Demo
 */
class WebPartDemo {
    // Configuration
    static Configuration = Configuration;

    /**
     * Constructor
     */
    constructor() {
        // Create an instance of the webpart
        new WebPart({
            cfgElementId: "wp-contactsCfg",
            displayElement: ContactsWebPart,
            editElement: WebPartCfg,
            targetElementId: "wp-contacts"
        });
    }
}
Script Reference

Next we will create a global variable, so we can reference this script from the webpart. We will use the SharePoint Script-On-Demand (SP SOD) library to notify any scripts waiting on this file to execute. In our case, this will be the webpart.

// Add the global variable
window["WebPartDemo"] = WebPartDemo;

// Let SharePoint know the script has been loaded
window["SP"].SOD.notifyScriptLoadedAndExecuteWaitingJobs("wpDemo.js");
WebPart Contents

The webpart in the configuration file is a Script Editor WebPart, where we define the contents. Referencing the contents of the webpart below, this is where the cfgElementId and targetElementId values come from. I’ve added a script reference for this demo. The last script element uses the SharePoint Script-On-Demand (SP SOD) library to wait for the main script to be loaded before executing the initialization method of this webpart.

<script type="text/javascript" src="/sites/dev/siteassets/webpartdemo/wpDemo.js"></script>
<div id="wp-contacts"></div>
<div id="wp-contactsCfg" style="display: none;"></div>
<script type="text/javascript">SP.SOD.executeOrDelayUntilScriptLoaded(function() { new WebPartDemo(); }, 'wpDemo.js')</script>

Demo

1. Deployment

  • Run ‘npm run build’ to compile the project.
  • Copy the output file (dist/wpDemo.js) to a SharePoint library.
    Note – This demo will upload to the ‘Site Assets’ library under the ‘WebPartDemo’ folder.

2. Create the List and WebPart

  • Press F-12 to open the developer browser tools
  • Click on the “Console” tab
  • Manually load the script file
var s = document.createElement("script");
s.src = "/sites/dev/siteassets/webpartdemo/wpDemo.js";
document.head.appendChild(s);

Note – This demo uses the web with a relative url of ‘/sites/dev’

* Run the ‘Install’ method

WebPartDemo.Configuration.install();

3. Test the WebPart

  • Access a webpart or wiki page
  • Edit the page
  • Add a WebPart to the page
  • Select the “My Contacts” WebPart from the “Demo” group
  • Edit the WebPart Configuration
  • Type in “My Contacts” as the target list
  • Click “Save” to update the webpart configuration. After saving the webpart the page will automatically refresh.
    Note – The page must refresh, since we are updating the webpart on the backend.
  • After saving the page you should now see tabs, but a message stating there are no contacts.

4. Add Test Data

  • Press F-12 to open the developer browser tools
  • Since the webpart is on the page, we have access to the WPDemo global variable. Run the method to add the test data.
WebPartDemo.Configuration.addTestData();


* You should now see contacts populated under each tab

* Validate the data against the list data

Leave a Reply

Your email address will not be published. Required fields are marked *