Simple list operations in SPFx using PnPjs

In your SharePoint Framework solutions, you will likely want to interact with data stored in SharePoint. PnPjs offers a fluent api used to call the SharePoint rest services. This article outlines what options you have, how they work and what their advantages.

Create a new web part project

Open power shell and run following comment to create a new web part by running the Yeoman SharePoint Generator

yo @microsoft/sharepoint

When prompted:

Enter the webpart name as your solution name, and then select Enter.
Select Create a subfolder with solution name for where to place the files.
Select Y to allow the solution to be deployed to all sites immediately.
Select N on the question if solution contains unique permissions.
Select WebPart as the client-side component type to be created.

The next set of prompts ask for specific information about your web part:

Enter your web part name, and then select Enter.
Enter your web part description, and then select Enter.
Select React framework as the framework you would like to use, and then select Enter.

Start Visual Studio Code (or your favorite code editor) within the context of the newly created project folder.

cd .\Simple_List_Operations\
code .

Install the library and required dependencies

npm install @pnp/sp --save

Import the library into your application, update constructor, and access the root sp object in render for PnPjs libraries

    sp.setup({
      spfxContext: this.props.spcontect
    });

Configure the custom properties

Create a new source code file under the src\webparts\simpleListOperations\components\ folder of the solution. Call the new file ISimpleListOperationsState.ts and use it to create a TypeScript Interface

export interface ISimpleListOperationsState {
  addText: string;
  updateText:IListItem[];
}

and one more in the same file

export interface IListItem {
  id: number;
  title: string;
}

In addition, you need to update the render method of the client-side web part to create a properly configured instance of the React component for rendering. The following code shows the updated method definition.

public render(): void {
    const element: React.ReactElement<ISimpleListOperationsProps> = React.createElement(
      SimpleListOperations,
      {
        description: this.properties.description,
        spcontext: this.context
      }
    );
    ReactDom.render(element, this.domElement);
  }

Update the SimpleListOperations.tsx file. First, add some import statements to import the types you defined earlier. Notice the import for ISimpleListOperationsPropsISimpleListOperationsState and IListItem. There are also some imports for the Office UI Fabric components used to render the UI of the React component and pnp sp imports.

import * as React from 'react';
import styles from './SimpleListOperations.module.scss';
import { ISimpleListOperationsProps } from './ISimpleListOperationsProps';
import { ISimpleListOperationsState, IListItem } from './ISimpleListOperationsState';
import { TextField, DefaultButton, PrimaryButton, Stack, IStackTokens, IIconProps } from 'office-ui-fabric-react/lib/';
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { autobind } from 'office-ui-fabric-react/lib/Utilities';

import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { IItemAddResult } from "@pnp/sp/items";

After the imports, define the icon for button component of Office UI Fabric

const stackTokens: IStackTokens = { childrenGap: 40 };
const DelIcon: IIconProps = { iconName: 'Delete' };
const ClearIcon: IIconProps = { iconName: 'Clear' };
const AddIcon: IIconProps = { iconName: 'Add' };

Replace this component with the following code.

 public render(): React.ReactElement<ISimpleListOperationsProps> {
    return (
      <div className={styles.simpleListOperations}>
        <div className={styles.container}>
          <div className={styles.row}>
            <div className={styles.column}>
              {this.state.updateText.map((row, index) => (
                <Stack horizontal tokens={stackTokens}>
                  <TextField label="Title" underlined value={row.title} onChanged={(textval) => { row.title = textval }} ></TextField>
                  <PrimaryButton text="Update" onClick={() => this._updateClicked(row)} />
                  <DefaultButton text="Delete" onClick={() => this._deleteClicked(row)} iconProps={DelIcon} />
                </Stack>
              ))}

              <br></br>
              <hr></hr>
              <label>Create new item</label>
              <Stack horizontal tokens={stackTokens}>
                <TextField label="Title" underlined value={this.state.addText} onChanged={(textval) => this.setState({ addText: textval })} ></TextField>
                <PrimaryButton text="Save" onClick={this._addClicked} iconProps={AddIcon} />
                <DefaultButton text="Clear" onClick={this._clearClicked} iconProps={ClearIcon} />
              </Stack>
            </div>
          </div>
        </div>
      </div>
    );
  }

Update the React component type declaration and add a constructor, as shown in the following example.

  constructor(prop: ISimpleListOperationsProps, state: ISimpleListOperationsState) {
    super(prop);
    this.state = {
      addText: '',
      updateText: [],
    };
    sp.setup({
      spfxContext: this.props.spcontext
    });
    if (Environment.type === EnvironmentType.SharePoint) {
      this._getListItems();
    }
    else if (Environment.type === EnvironmentType.Local) {
      // return (<div>Whoops! you are using local host...</div>);
    }
  }

place the below code after the react component code, these functions using PnPjs to get the data, create list items, update list item and delete the list item.

async _getListItems() {
    const allItems: any[] = await sp.web.lists.getByTitle("Colors").items.getAll();
    console.log(allItems);
    let items: IListItem[] = [];
    allItems.forEach(element => {
      items.push({ id: element.Id, title: element.Title });
    });
    this.setState({ updateText: items });
  }

  @autobind
  async _updateClicked(row: IListItem) {
    const updatedItem = await sp.web.lists.getByTitle("Colors").items.getById(row.id).update({
      Title: row.title,
    });

  }

  @autobind
  async _deleteClicked(row: IListItem) {
    const deletedItem = await sp.web.lists.getByTitle("Colors").items.getById(row.id).recycle();
    this._getListItems();
  }

  @autobind
  async _addClicked() {
    const iar: IItemAddResult = await sp.web.lists.getByTitle("Colors").items.add({
      Title: this.state.addText
    });
    this.setState({ addText: '' });
    this._getListItems();
  }

  @autobind
  private _clearClicked(): void {
    this.setState({ addText: '' })
  }

Deploy the solution

You’re now ready to build, bundle, package, and deploy the solution.

Run the gulp commands to verify that the solution builds correctly.

gulp build

Use the following command to bundle and package the solution.

gulp bundle --ship
gulp package-solution --ship

Browse to the app catalog of your target tenant and upload the solution package. You can find the solution package under the sharepoint/solution folder of your solution. It is the .sppkg file. After you upload the solution package in the app catalog. you can find and the web part anywhere across the tenant.

Sharing is caring!

If you have any questions, feel free to let me know in the comments section.
Happy coding!!!

Office UI fabric Callout in SharePoint Framework (SPFx)

As we know Office UI Fabric which official front-end framework is for building a user interface that fits seamlessly into SharePoint modern experience. In this article, we going to see more detail about the Callout component. The Callout is a powerful way to simplify a user interface. Also, we going to see how to update the title for a selected list item in the SharePoint. At the end of the article, you can find the downlink to download this whole project.

Creating a new SharePoint Framework extension project

The first step we have to create a new SharePoint framework extension project, I have used SharePoint Framework version 1.8.2, if you have any question regarding set-up new SharePoint framework development environment then you can refer my one of the previous article where I explained simple steps to set up a new development environment and create the new project. please refer below image for select input while creating project.

Creating Office UI Fabric Callout component

In order to create new react component we have to create a new folder in the name of components under src\extensions, in that folder we have to create three files,

  1. src\extensions\components\Callout.module.scss
  2. src\extensions\components\Callout.tsx
  3. src\extensions\components\ICalloutProps.ts

In the SCSS file we just used for styling purpose, Its almost same like CSS but using scss we can access CSS classes and functions, tsx is the react component file, here we managing to create control and the events.

Here we creating a component and pushing that using SharePoint base dialog. We also updating list item’s title, for that we passing this.context from the List view commend set file in the react component file, if you have any question about the code logic flow, please post into the comments section below.

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { ICalloutProps, ICalloutState } from './ICalloutProps';
import { Callout } from 'office-ui-fabric-react/lib/Callout';
import styles01 from './Callout.module.scss';
import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'


export default class CalloutComponent extends BaseDialog {
  public itemTitle: string;
  public itemID: number;
  public spcontext?: any | null;

  public render(): void {
    ReactDOM.render(<Cillout itemID={this.itemID} spcontext={this.spcontext} Title={this.itemTitle} domElement={document.activeElement.parentElement} onDismiss={this.onDismiss.bind(this)} />,
      this.domElement);
  }

  public getConfig(): IDialogConfiguration {
    return {
      isBlocking: false
    };
  }

  private onDismiss() {
    ReactDOM.unmountComponentAtNode(this.domElement);
  }
}

class Cillout extends React.Component<ICalloutProps, ICalloutState> {

  constructor(props: ICalloutProps) {
    super(props);
    this.state = {
      Title: this.props.Title
    };

    this.setState({ Title: this.props.Title });
    this._saveClicked = this._saveClicked.bind(this);
    this._onChangedTitle = this._onChangedTitle.bind(this);
  }

  public render(): JSX.Element {
    return (
      <div>
        <Callout
          className={styles01["ms-CalloutExample-callout"]}
          role="alertdialog"
          gapSpace={0}
          target={this.props.domElement}
          onDismiss={this.onDismiss.bind(this)}
          setInitialFocus={true}
          hidden={false}
        >
          <div className={styles01["ms-CalloutExample-header"]}>
            <p className={styles01["ms-CalloutExample-title"]}>
              Property panel
            </p>
          </div>
          <div className={styles01["ms-CalloutExample-inner"]}>
            <div className={styles01["ms-CalloutExample-content"]}>
              <p className={styles01["ms-CalloutExample-subText"]}>
                <TextField label="Title" value={this.state.Title} underlined onChanged={this._onChangedTitle} />
              </p>
            </div>
            <div className={styles01["ms-CalloutExample-actions"]}>
              <PrimaryButton text="Save" onClick={this._saveClicked} />
            </div>
          </div>
        </Callout>
      </div>
    );
  }
  private onDismiss(ev: any) {
    this.props.onDismiss();
  }

  private _onChangedTitle(newValue: string): void {
    this.setState({ Title: newValue });
  }

  private _saveClicked() {
    const body: string = JSON.stringify({
      '__metadata': {
        'type': 'SP.Data.' + this.props.spcontext.pageContext.list.title + 'ListItem'
      },
      'Title': this.state.Title
    });
    this.props.spcontext.spHttpClient.get(this.props.spcontext.pageContext.web.absoluteUrl + `/_api/web/lists/getbytitle('${this.props.spcontext.pageContext.list.title}')/items(` + this.props.itemID + ')', SPHttpClient.configurations.v1).then
      ((Response: SPHttpClientResponse) => {
        this.props.spcontext.spHttpClient.post(this.props.spcontext.pageContext.web.absoluteUrl + `/_api/web/lists/getbytitle('${this.props.spcontext.pageContext.list.title}')/items(` + this.props.itemID + ')', SPHttpClient.configurations.v1,
          {
            headers: {
              'Accept': 'application/json;odata=nometadata',
              'Content-type': 'application/json;odata=verbose',
              'odata-version': '',
              'IF-MATCH': Response.headers.get('ETag'),
              'X-HTTP-Method': 'MERGE'
            },
            body: body
          }).then((response: SPHttpClientResponse) => {
            console.log(`Status code: ${response.status}`);
            console.log(`Status text: ${response.statusText}`);
            this.props.onDismiss();
          });
      });
  }
}

And then we have typescript file for property interface, in the file, we managing all property interfaces which is required for our extension and the react components.

export interface ICalloutProps {
  isCalloutVisible?: boolean;
  onDismiss: () => void;
  domElement: any;
  Title:string;
  spcontext?:any|null;
  itemID:number;
}

export interface ICalloutState {
  Title:string;
}

Mapping react component into list view command set

List view commend set files are created by the yeoman generator while creating the project, we just wanted to map ours react component into the this.

  1. \src\extensions\fabricCallout\FabricCalloutCommandSet.ts
  2. \src\extensions\fabricCallout\FabricCalloutCommandSet.manifest.json
import { override } from '@microsoft/decorators';
import {
  BaseListViewCommandSet,
  Command,
  IListViewCommandSetListViewUpdatedParameters,
  IListViewCommandSetExecuteEventParameters
} from '@microsoft/sp-listview-extensibility';
import Callout from '../components/Callout'; 

export default class FabricCalloutCommandSet extends BaseListViewCommandSet<{}> {
  @override
  public onInit(): Promise<void> {
    return Promise.resolve();
  }

  @override
  public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
    const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');
    if (compareOneCommand) {
      // This command should be hidden unless exactly one row is selected.
      compareOneCommand.visible = event.selectedRows.length === 1;
    }
  }

  @override
  public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
    switch (event.itemId) {
      case 'COMMAND_1':
        const callout: Callout = new Callout();
        callout.itemTitle=event.selectedRows[0].getValueByName('Title');
        callout.itemID=event.selectedRows[0].getValueByName('ID');
        callout.spcontext= this.context;
        callout.show();
        break;
      default:
        throw new Error('Unknown command');
    }
  }
}

We can change the commend set title and the icon the commend set manifest file,

Also, we can manage where we want to show out custom commend set, for that we have to edit the elements.xml file under \sharepoint\assets\ for more detail about this file please refer the Microsoft documentation

If you have any questions, feel free to let me know in the comments section. Good Luck!!!