SharePoint React Components

This blog post will give an overview of an extension to the gd-sprest library for creating list item forms in SharePoint. The source code for this project can be found in github.

Project Overview

From all of the posts I’ve written so far, it just made sense to create an extension to the gd-sprest library for creating list item forms in SharePoint. The project uses the Office Fabric UI React framework to render the field components. This post will go over the test project, located in the github project. The test project is a simple dashboard to display the list items, with a simple menu for creating and viewing items using a panel.

Files

The test folder has the following files:
* cfg.ts – The configuration file to create the test list and custom fields.
* dashboard.tsx – The dashboard containing the list view and list item form panel.
* data.ts – The data source class.
* index.tsx – The main entry point of the project.
* itemForm.tsx – The list item form.
* list.tsx – The list view.

Configuration

The configuration file uses the automation feature of the gd-sprest library. This configuration file defines the test list with the custom field types the gd-sprest-react library currently supports.

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

/**
 * Test Configuration
 */
export const Configuration = new Helper.SPConfig({
    ListCfg: [
        {
            CustomFields: [
                {
                    Name: "TestBoolean",
                    SchemaXml: '<Field ID="{E6C387B9-AA16-4115-B57F-601720F9D85B}" Name="TestBoolean" StaticName="TestBoolean" DisplayName="Boolean" Type="Boolean">' +
                        '<Default>0</Default>' +
                        '</Field>'
                },
                {
                    Name: "TestChoice",
                    SchemaXml: '<Field ID="{8B6EB335-3D5C-42B5-A2DB-601720E8A0BC}" Name="TestChoice" StaticName="TestChoice" DisplayName="Choice" Type="Choice">' +
                        '<Default>Choice 3</Default>' +
                        '<CHOICES>' +
                        '<CHOICE>Choice 1</CHOICE>' +
                        '<CHOICE>Choice 2</CHOICE>' +
                        '<CHOICE>Choice 3</CHOICE>' +
                        '<CHOICE>Choice 4</CHOICE>' +
                        '<CHOICE>Choice 5</CHOICE>' +
                        '</CHOICES>' +
                        '</Field>'
                },
                {
                    Name: "TestComments",
                    SchemaXml: '<Field ID="{0E11F904-4DA2-48E1-B45B-601720923498}" Name="TestComments" StaticName="TestComments" DisplayName="Comments" Type="Note" AppendOnly="TRUE" />'
                },
                {
                    Name: "TestDate",
                    SchemaXml: '<Field ID="{5BF47BE2-2697-47C1-B6FE-6017207B221A}" Name="TestDate" StaticName="TestDate" DisplayName="Date Only" Type="DateTime" Format="DateOnly" />'
                },
                {
                    Name: "TestDateTime",
                    SchemaXml: '<Field ID="{0F804508-A8F4-4DE6-9319-601720CE5294}" Name="TestDateTime" StaticName="TestDateTime" DisplayName="Date/Time" Type="DateTime" />'
                },
                {
                    Name: "TestLookup",
                    SchemaXml: '<Field ID="{ACF5F7EE-629A-452B-8381-60172088E176}" Name="TestLookup" StaticName="TestLookup" DisplayName="Lookup" Type="Lookup" List="SPReact" ShowField="Title" />'
                },
                {
                    Name: "TestMultiChoice",
                    SchemaXml: '<Field ID="{22AFA098-4B62-4236-8C01-6017208DAB49}" Name="TestMultiChoice" StaticName="TestMultiChoice" DisplayName="Multi-Choice" Type="MultiChoice">' +
                        '<Default>Choice 3</Default>' +
                        '<CHOICES>' +
                        '<CHOICE>Choice 1</CHOICE>' +
                        '<CHOICE>Choice 2</CHOICE>' +
                        '<CHOICE>Choice 3</CHOICE>' +
                        '<CHOICE>Choice 4</CHOICE>' +
                        '<CHOICE>Choice 5</CHOICE>' +
                        '</CHOICES>' +
                        '</Field>'
                },
                {
                    Name: "TestMultiLookup",
                    SchemaXml: '<Field ID="{68465DA3-34DD-4FEA-BE7A-60172019C4FA}" Name="TestMultiLookup" StaticName="TestMultiLookup" DisplayName="Multi-Lookup" Type="LookupMulti" List="SPReact" Mult="TRUE" ShowField="Title" />'
                },
                {
                    Name: "TestMultiUser",
                    SchemaXml: '<Field ID="{35C91E16-6C53-4202-B4AA-60172082983A}" Name="TestMultiUser" StaticName="TestMultiUser" DisplayName="Multi-User" Type="User" Mult="TRUE" UserSelectionMode="0" UserSelectionScope="0" />'
                },
                {
                    Name: "TestNote",
                    SchemaXml: '<Field ID="{0E11F904-4DA2-48E1-B45B-601720977191}" Name="TestNote" StaticName="TestNote" DisplayName="Note" Type="Note" />'
                },
                {
                    Name: "TestNumberDecimal",
                    SchemaXml: '<Field ID="{8EABA3DF-D439-4C78-B6E9-601720F7C222}" Name="TestNumberDecimal" StaticName="TestNumberDecimal" DisplayName="Decimal" Type="Number" />'
                },
                {
                    Name: "TestNumberInteger",
                    SchemaXml: '<Field ID="{02CD9CA9-2E41-42B1-B487-6017208731FD}" Name="TestNumberInteger" StaticName="TestNumberInteger" DisplayName="Integer" Type="Number" />'
                },
                {
                    Name: "TestUrl",
                    SchemaXml: '<Field ID="{9983709F-C54C-4816-AC2C-601720A0553B}" Name="TestUrl" StaticName="TestUrl" DisplayName="Url" Type="URL" />'
                },
                {
                    Name: "TestUser",
                    SchemaXml: '<Field ID="{041F5349-6D87-4DF8-8A7A-6017206F6F44}" Name="TestUser" StaticName="TestUser" DisplayName="User" Type="User" UserSelectionMode="0" UserSelectionScope="0" />'
                },
            ],
            ListInformation: {
                BaseTemplate: SPTypes.ListTemplateType.GenericList,
                Title: "SPReact"
            },
            ViewInformation: [
                {
                    ViewFields: [
                        "LinkTitle", "TestBoolean", "TestChoice", "TestDate", "TestDateTime",
                        "TestLookup", "TestMultiChoice", "TestMultiLookup", "TestMultiUser",
                        "TestNote", "TestNumberDecimal", "TestNumberInteger", "TestUrl", "TestUser"
                    ],
                    ViewName: "All Items"
                }
            ]
        }
    ]
});

Data Source

The data source class contains the methods to load the list items and for saving/updating items. The list entity type name MUST be set for creating the list item when complex field types are used. To ensure intellisense is available, the test item interface will inherit from the IListItemResult interface. The load method gives an example of how to expand complex field types to ensure the data exists.

import { Promise } from "es6-promise";
import { List, Types } from "gd-sprest";

/**
 * Test Item Information
 */
export interface ITestItem extends Types.IListItemResult {
    Attachments?: boolean;
    TestBoolean?: boolean;
    TestChoice?: string;
    TestDate?: string;
    TestDateTime?: string;
    TestLookup?: Types.ComplexTypes.FieldLookupValue;
    TestLookupId?: string | number;
    TestMultiChoice?: string;
    TestMultiLookup?: string;
    TestMultiLookupId?: string;
    TestMultiUser?: { results: Array<number> };
    TestMultiUserId?: Array<number>;
    TestNote?: string;
    TestNumberDecimal?: number;
    TestNumberInteger?: number;
    TestUrl?: string;
    TestUser?: Types.ComplexTypes.FieldUserValue;
    TestUserId?: string | number;
    Title?: string;
}

/**
 * Data source for the test project
 */
export class DataSource {
    /**
     * Properties
     */

    // List Name
    static ListName = "SPReact";

    // List Item Entity Type Name (Required for complex field item add operation)
    static ListItemEntityTypeFullName = "SP.Data.SPReactListItem";

    /**
     * Methods
     */

    // Method to load the test data
    static load = (itemId?:number): PromiseLike<ITestItem | Array<ITestItem>> => {
        // Return a promise
        return new Promise((resolve, reject) => {
            // Get the list
            (new List(DataSource.ListName))
                // Get the items
                .Items()
                // Set the query
                .query({
                    Filter: itemId > 0 ? "ID eq " + itemId : "",
                    Expand: ["AttachmentFiles", "TestLookup", "TestMultiLookup", "TestMultiUser", "TestUser"],
                    OrderBy: ["Title"],
                    Select: ["*", "Attachments", "AttachmentFiles", "TestLookup/ID", "TestLookup/Title", "TestMultiLookup/ID", "TestMultiLookup/Title", "TestMultiUser/ID", "TestMultiUser/Title", "TestUser/ID", "TestUser/Title"],
                    Top: 50
                })
                // Execute the request
                .execute((items) => {
                    // Ensure the items exist
                    if (items.results) {
                        // Resolve the request
                        resolve(itemId > 0 ? items.results[0] : items.results);
                    } else {
                        // Reject the request
                        reject();
                    }
                });
        });
    }

    // Method to save a test item
    static save = (item: ITestItem): PromiseLike<ITestItem> => {
        // Return a promise
        return new Promise((resolve, reject) => {
            // See if this is an existing item
            if (item.update) {
                // Update the item
                item.update({
                    TestBoolean: item.TestBoolean,
                    TestChoice: item.TestChoice,
                    TestDate: item.TestDate,
                    TestDateTime: item.TestDateTime,
                    TestLookupId: item.TestLookupId,
                    TestMultiChoice: item.TestMultiChoice,
                    TestMultiLookupId: item.TestMultiLookupId,
                    TestMultiUserId: item.TestMultiUserId,
                    TestNote: item.TestNote,
                    TestNumberDecimal: item.TestNumberDecimal,
                    TestNumberInteger: item.TestNumberInteger,
                    TestUrl: item.TestUrl,
                    TestUserId: item.TestUserId
                } as ITestItem)
                    // Execute the request
                    .execute((request) => {
                        // Ensure the update was successful
                        if(request.response == "") {
                            // Resolve the request
                            resolve(item);
                        } else {
                            // Reject the request
                            reject(request.response);
                        }
                    });
            } else {
                // Set the item metadata - This is required for complex field updates
                item["__metadata"] = { type: DataSource.ListItemEntityTypeFullName };

                // Get the list
                (new List(DataSource.ListName))
                    // Get the items
                    .Items()
                    // Add the item
                    .add(item)
                    // Execute the request
                    .execute((item: ITestItem) => {
                        // Load the item again to get the expanded field values
                        DataSource.load(item.Id).then((item: ITestItem) => {
                            // Resolve the request
                            resolve(item);
                        })
                    });
            }
        });
    }
}

Dashboard

The dashboard is referenced by the entry point of the project, and contains all of the components for this project.

import * as React from "react";
import { PrimaryButton } from "office-ui-fabric-react";
import { Panel } from "../build";
import { DataSource, ITestItem } from "./data";
import { ItemForm } from "./itemForm";
import { TestList } from "./list";

/**
 * State
 */
interface State {
    item:ITestItem;
}

/**
 * Dashboard
 */
export class Dashboard extends React.Component<null, State> {
    /**
     * Constructor
     */
    constructor() {
        super();

        // Set the state
        this.state = {
            item: {} as ITestItem
        };
    }

    /**
     * Public Interface
     */

    // Render the component
    render() {
        return (
            <div>
                <PrimaryButton onClick={this.onClick} text="New Item" />
                <TestList viewItem={this.viewItem} ref="list" />
                <Panel
                    isLightDismiss={true}
                    headerText="Test Item Form"
                    onRenderFooterContent={this.renderFooter}
                    ref="panel">
                    <div className="ms-Grid">
                        <div className="ms-Grid-row">
                            <div className="ms-Grid-col ms-u-md12">
                                <span className="ms-fontSize-l" ref="message"></span>
                            </div>
                        </div>
                    </div>
                    <ItemForm item={this.state.item} ref="item" />
                </Panel>
            </div>
        );
    }

    /**
     * Events
     */

    // The click event for the button
    private onClick = (ev: React.MouseEvent<HTMLButtonElement>) => {
        // Prevent postback
        ev.preventDefault();

        // Update the state
        this.setState({ item: {} as ITestItem }, () => {
            // Show the item form
            (this.refs["panel"] as Panel).show();
        });
    }

    /**
     * Methods
     */

    // Method to render the footer
    private renderFooter = () => {
        return (
            <div className="ms-Grid">
                <div className="ms-Grid-row">
                    <div className="ms-Grid-col ms-u-md2 ms-u-mdPush9">
                        <PrimaryButton
                            onClick={this.save}
                            text="Save"
                        />
                    </div>
                </div>
            </div>
        );
    }

    // Method to save the item
    private save = () => {
        let itemForm:ItemForm = this.refs["item"] as ItemForm;

        // Save the item
        DataSource.save(itemForm.getValues()).then((item: ITestItem) => {
            // Ensure the item exists
            if(item.existsFl) {
                // Save the attachments
                itemForm.saveAttachments(item.Id).then(() => {
                    // Update the message
                    (this.refs["message"] as HTMLSpanElement).innerHTML = "The item was saved successfully.";
                });
            } else {
                // Update the message
                (this.refs["message"] as HTMLSpanElement).innerHTML = "Error: " + item.response;
            }
        });
    }

    // Method to view an item
    private viewItem = (item:ITestItem) => {
        // Update the state
        this.setState({ item }, () => {
            // Show the item form
            (this.refs["panel"] as Panel).show();
        });
    }
}

List View

The list view class is a simple example of using the “List” component of the Office Fabric UI React framework.

import * as React from "react";
import {Types} from "gd-sprest";
import {
    DetailsList, IColumn,
    PrimaryButton
} from "office-ui-fabric-react";
import { DataSource, ITestItem } from "./data";

/**
 * Properties
 */
interface Props {
    viewItem?: (item: ITestItem) => void;
}

/**
 * State
 */
interface State {
    items: Array<ITestItem>;
}

/**
 * Test List
 */
export class TestList extends React.Component<Props, State> {
    /**
     * Constructor
     */
    constructor(props: Props) {
        super(props);

        // Set the state
        this.state = {
            items: []
        };

        // Load the items
        DataSource.load().then((items: Array<ITestItem>) => {
            // Update the state
            this.setState({ items });
        });
    }

    /**
     * Global Variables
     */

    // List Columns
    private _columns: Array<IColumn> = [
        { key: "Action", fieldName: "Id", name: "Action", minWidth: 100, maxWidth: 200 },
        { key: "Title", fieldName: "Title", name: "Title", minWidth: 100, maxWidth: 200 },
        { key: "TestBoolean", fieldName: "TestBoolean", name: "Boolean", minWidth: 100, maxWidth: 200 },
        { key: "TestChoice", fieldName: "TestChoice", name: "Choice", minWidth: 100, maxWidth: 200 },
        { key: "TestDate", fieldName: "TestDate", name: "Date", minWidth: 100, maxWidth: 200 },
        { key: "TestLookup", fieldName: "TestLookup", name: "Lookup", minWidth: 100, maxWidth: 200 },
        { key: "TestUrl", fieldName: "TestUrl", name: "URL", minWidth: 100, maxWidth: 200 }
    ];

    /**
     * Public Interface
     */

    // Render the component
    render() {
        return (
            <div className="ms-Grid">
                <div className="ms-Grid-row">
                    <div className="ms-Grid-col ms-u-md12">
                        <DetailsList
                            columns={this._columns}
                            items={this.state.items}
                            onRenderItemColumn={this.renderColumn}
                        />
                    </div>
                </div>
            </div>
        );
    }

    /**
     * Methods
     */

    // Method to render the column
    private renderColumn = (item?: ITestItem, index?: number, column?: IColumn) => {
        let value = item[column.fieldName];

        // Render the value, based on the key
        switch (column.key) {
            // ID Field
            case "Action":
                // Render a button
                return (
                    <PrimaryButton onClick={ev => this.viewItem(ev, item)} text="View" />
                );

            // Boolean Field
            case "TestBoolean":
                return (
                    <span>{value ? "Yes" : "No"}</span>
                );

            // Lookup Field
            case "TestLookup":
                return (
                    <span>{value ? value.Title : ""}</span>
                );

            // URL Field
            case "TestUrl":
                let urlValue:Types.ComplexTypes.FieldUrlValue = value;
                return (
                    <a href={urlValue.Url}>{urlValue.Description || urlValue.Url}</a>
                );

            // Default
            default:
                // Render the value
                return (
                    <span>{typeof(value) === "string" ? value : ""}</span>
                );
        }
    }

    // Method to view an item
    private viewItem = (ev: React.MouseEvent<any>, item?: ITestItem) => {
        // Prevent postback
        ev.preventDefault();

        // View the item
        this.props.viewItem ? this.props.viewItem(item) : null;
    }
}

Item Form

The list item form displays the supported field types of this library. Since this is a test project in the gd-sprest-react library the reference to import the components is set to “../build”. Your projects will use “gd-sprest-react” to import the components. The components require the list name and name (internal field name). With this basic information, the code will query the field and figure out how to render the component. This will ensure the configuration is controlled by SharePoint, and not the code. The defaultValue is used for edit forms, which is set the field value of the selected item. The “ref” property is used for getting the form values of the form. I’ve set it to the “InternalFieldName” used for updating/creating list items using the REST api. This value will be different for lookup and user fields, where “Id” is appended to it. The getValues method shows how to get the selected form value using a simple loop.

import * as React from "react";
import { Label, PrimaryButton } from "office-ui-fabric-react";
import { DataSource, ITestItem } from "./data";
import {
    Field, FieldAttachments, FieldBoolean, FieldChoice, FieldDateTime, FieldLookup,
    FieldNumber, FieldNumberTypes, FieldText, FieldUrl, FieldUser
} from "../build";

/**
 * Properties
 */
interface Props {
    item?: ITestItem
}

/**
 * Item Form
 */
export class ItemForm extends React.Component<Props, null> {
    /**
     * Public Interface
     */

    // Method to get the form values
    getValues = ():ITestItem => {
        let item: ITestItem = this.props.item || {} as ITestItem;

        // Parse the references
        for (let fieldName in this.refs) {
            let ref = this.refs[fieldName];

            // See if this is a field
            if (ref instanceof Field) {
                // Update the item value
                item[fieldName] = (ref as Field<any, any>).state.value;
            }
        }

        // Return the item
        return item;
    }

    // Render the component
    render() {
        return (
            <div className="ms-Grid">
                <div className="ms-Grid-row">
                    {this.renderForm()}
                </div>
            </div>
        );
    }

    // Method to save the item attachments
    saveAttachments = (itemId:number) => {
        // Save the attachments
        return (this.refs["attachments"] as FieldAttachments).save(itemId);
    }

    /**
     * Methods
     */

    // Method to render the item form
    private renderForm = () => {
        let item: ITestItem = this.props.item || {} as ITestItem;
        return (
            <div className="ms-Grid-col ms-u-md12">
                <FieldAttachments
                    files={item.AttachmentFiles}
                    listName={DataSource.ListName}
                    ref="attachments"
                />
                <FieldText
                    defaultValue={item.Title}
                    listName={DataSource.ListName}
                    name="Title"
                    ref="Title"
                />
                <FieldBoolean
                    defaultValue={item.TestBoolean}
                    listName={DataSource.ListName}
                    name="TestBoolean"
                    ref="TestBoolean"
                />
                <FieldChoice
                    defaultValue={item.TestChoice}
                    listName={DataSource.ListName}
                    name="TestChoice"
                    ref="TestChoice"
                />
                <FieldDateTime
                    defaultValue={item.TestDate}
                    listName={DataSource.ListName}
                    name="TestDate"
                    ref="TestDate"
                />
                <FieldDateTime
                    defaultValue={item.TestDateTime}
                    listName={DataSource.ListName}
                    name="TestDateTime"
                    ref="TestDateTime"
                />
                <FieldLookup
                    defaultValue={item.TestLookup}
                    listName={DataSource.ListName}
                    name="TestLookup"
                    ref="TestLookupId"
                />
                <FieldChoice
                    defaultValue={item.TestMultiChoice}
                    listName={DataSource.ListName}
                    name="TestMultiChoice"
                    ref="TestMultiChoice"
                />
                <FieldLookup
                    defaultValue={item.TestMultiLookup}
                    listName={DataSource.ListName}
                    name="TestMultiLookup"
                    ref="TestMultiLookupId"
                />
                <FieldUser
                    defaultValue={item.TestMultiUser}
                    listName={DataSource.ListName}
                    name="TestMultiUser"
                    ref="TestMultiUserId"
                />
                <FieldText
                    defaultValue={item.TestNote}
                    listName={DataSource.ListName}
                    name="TestNote"
                    ref="TestNote"
                />
                <FieldNumber
                    defaultValue={item.TestNumberDecimal}
                    listName={DataSource.ListName}
                    name="TestNumberDecimal"
                    ref="TestNumberDecimal"
                    type={FieldNumberTypes.Decimal}
                />
                <FieldNumber
                    defaultValue={item.TestNumberInteger}
                    listName={DataSource.ListName}
                    name="TestNumberInteger"
                    ref="TestNumberInteger"
                />
                <FieldUrl
                    defaultValue={item.TestUrl}
                    listName={DataSource.ListName}
                    name="TestUrl"
                    ref="TestUrl"
                />
                <FieldUser
                    defaultValue={item.TestUser}
                    listName={DataSource.ListName}
                    name="TestUser"
                    ref="TestUserId"
                />
            </div>
        );
    }
}

Main

The main entry point of the project will create a global variable. We will add the configuration so we can install/uninstall it from the site. I’ve created an initialize method which I will render the project to. The last line of the code will notify the SharePoint Script-On-Demand (SP SOD) library that the “test.js” script has been loaded.

import * as React from "react";
import {render} from "react-dom";
import {Configuration} from "./cfg";
import {Dashboard} from "./dashboard";
declare var SP;

// Create the global variable
window["gdSPRestReact"] = {
    // The test configuration
    Configuration,

    // The initialization method
    init: () => {
        // Get the target element
        let el = document.querySelector("#target");
        if(el) {
            // Render the dashboard to the target element
            render(<Dashboard />, el);
        }
    }
};

// Let SharePoint know the script has been loaded
SP.SOD.notifyScriptLoadedAndExecuteWaitingJobs("test.js");

Demo

Build and Deploy

After building the test project and uploading the file to SharePoint, you can edit any test page and add a ScriptEditor WebPart to it setting the contents to the code listed below. The test script uses the SharePoint Script-On-Demand (SP SOD) to execute the initialization method after the “test.js” script is loaded.

    <div id="target"></div>
      <script type="text/javascript" src="[url to the test.js file]"></script>
    <script type="text/javascript">
        SP.SOD.executeOrDelayUntilScriptLoaded(function() { new gdSPRestReact.init(); }, "test.js");
    </script>

Install the List


Now that the script file is referenced on the page, you’ll see an empty list. The next step is to open the browser console window (F-12), and typing the following command to install the test project. After the test project installs, refresh the page.

gdSPRestReact.Configuration.install();

Create a New Item

Refresh the page, and click on the “New Item” button to display the new item form.

List View

After saving an item, refresh the page and you will see it in the list view.

Uninstall the List

Just wanted to demo how to clean-up after yourself. Similar to the install, there is an uninstall method to remove the configuration items.

gdSPRestReact.Configuration.uninstall();

Leave a Reply

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