In the article Develop First Client Side Web Part, we developed the basic SharePoint client web part which can run independently without any interaction with SharePoint.
In this article, we will explore how to interact with the SharePoint list for CRUD (Create, Read, Update, and Delete) operations using Knockout JS. Knockout JS is not natively supported by SharePoint Framework.
Brief info about Knockout JS
KnockoutJS was developed and maintained as an open source project by Steve Sanderson, a Microsoft employee. Knockout JS follows JavaScript implementation of the Model-View-ViewModel pattern with templates. Read more about KnockoutJS here.
Create SPFx Solution
Open the command prompt. Create a directory for SPFx solution.
Configure Property for List Name
SPFx solutions by default have the description property created. Let us change the property to list name. We will use this property to configure the list name on which the CRUD operation is to perform.
KnockoutJS was developed and maintained as an open source project by Steve Sanderson, a Microsoft employee. Knockout JS follows JavaScript implementation of the Model-View-ViewModel pattern with templates. Read more about KnockoutJS here.
Create SPFx Solution
Open the command prompt. Create a directory for SPFx solution.
- md spfx-crud-knockoutjs
Navigate to the above-created directory.
- cd spfx-crud-knockout js
Run Yeoman SharePoint Generator to create the solution.
- yo @microsoft/sharepoint
Yeoman generator will present you with the wizard by asking questions about the solution to be created.
Solution Name
Hit Enter to have a default name (spfx-crud-knockoutjs in this case) or type in any other name for your solution.
Selected choice - Hit Enter.
Hit Enter to have a default name (spfx-crud-knockoutjs in this case) or type in any other name for your solution.
Selected choice - Hit Enter.
Target for component
Here, we can select the target environment where we are planning to deploy the client webpart; i.e., SharePoint Online or SharePoint OnPremise (SharePoint 2016 onwards).
Here, we can select the target environment where we are planning to deploy the client webpart; i.e., SharePoint Online or SharePoint OnPremise (SharePoint 2016 onwards).
Selected choice - SharePoint Online only (latest)
Location of files
We may choose to use the same folder or create a subfolder for our solution.
We may choose to use the same folder or create a subfolder for our solution.
Selected choice - Same folder
Deployment option
Selecting Y will allow the app to deployed instantly to all sites and will be accessible everywhere.
Selecting Y will allow the app to deployed instantly to all sites and will be accessible everywhere.
Selected choice - N (install on each site explicitly)
Type of client-side component to create
We can choose to create client side webpart or an extension. Choose webpart option.
We can choose to create client side webpart or an extension. Choose webpart option.
Selected choice - WebPart
Web part name
Hit enter to select the default name or type in any other name.
Hit enter to select the default name or type in any other name.
Selected choice - KnockoutCRUD
Web part description
Hit enter to select the default description or type in any other value.
Hit enter to select the default description or type in any other value.
Selected choice - CRUD operations with Knockout JS
Framework to use
Select any JavaScript framework to develop the component. Available choices are (No JavaScript Framework, React, and Knockout)
Select any JavaScript framework to develop the component. Available choices are (No JavaScript Framework, React, and Knockout)
Selected choice - Knockout
Yeoman generator will perform a scaffolding process to generate the solution. The scaffolding process will take a significant amount of time. Once the scaffolding process is completed, lock down the version of project dependencies by running the below command
Yeoman generator will perform a scaffolding process to generate the solution. The scaffolding process will take a significant amount of time. Once the scaffolding process is completed, lock down the version of project dependencies by running the below command
- npm shrinkwrap
In the command prompt type the below command to open the solution in the code editor of your choice.
- code .
SPFx solutions by default have the description property created. Let us change the property to list name. We will use this property to configure the list name on which the CRUD operation is to perform.
Step 1
Open mystrings.d.ts under \src\webparts\knockoutCrud\loc\ folder
Step 2
Rename DescriptionFieldLabel to ListNameFieldLabel
Open mystrings.d.ts under \src\webparts\knockoutCrud\loc\ folder
Step 2
Rename DescriptionFieldLabel to ListNameFieldLabel
- declare interface IKnockoutCrudWebPartStrings {
- PropertyPaneDescription: string;
- BasicGroupName: string;
- ListNameFieldLabel: string;
- }
- declare module 'KnockoutCrudWebPartStrings' {
- const strings: IKnockoutCrudWebPartStrings;
- export = strings;
- }
Step 3
In en-us.js file under \src\webparts\knockoutCrud\loc\ folder set the display name for listName property
In en-us.js file under \src\webparts\knockoutCrud\loc\ folder set the display name for listName property
- define([], function() {
- return {
- "PropertyPaneDescription": "Description",
- "BasicGroupName": "Group Name",
- "ListNameFieldLabel": "List Name"
- }
- });
Step 4
Open main webpart file (KnockoutCrudWebPart.ts) under \src\webparts\knockoutCrud folder.
Step 5
Rename description property pane field to listName
Open main webpart file (KnockoutCrudWebPart.ts) under \src\webparts\knockoutCrud folder.
Step 5
Rename description property pane field to listName
- import * as ko from 'knockout';
- import { Version } from '@microsoft/sp-core-library';
- import {
- BaseClientSideWebPart,
- IPropertyPaneConfiguration,
- PropertyPaneTextField
- } from '@microsoft/sp-webpart-base';
- import * as strings from 'KnockoutCrudWebPartStrings';
- import KnockoutCrudViewModel, { IKnockoutCrudBindingContext } from './KnockoutCrudViewModel';
- let _instance: number = 0;
- export interface IKnockoutCrudWebPartProps {
- listName: string;
- }
- export default class KnockoutCrudWebPart extends BaseClientSideWebPart<IKnockoutCrudWebPartProps> {
- private _id: number;
- private _componentElement: HTMLElement;
- private _koDescription: KnockoutObservable<string> = ko.observable('');
- /**
- * Shouter is used to communicate between web part and view model.
- */
- private _shouter: KnockoutSubscribable<{}> = new ko.subscribable();
- /**
- * Initialize the web part.
- */
- protected onInit(): Promise<void> {
- this._id = _instance++;
- const tagName: string = `ComponentElement-${this._id}`;
- this._componentElement = this._createComponentElement(tagName);
- this._registerComponent(tagName);
- // When web part description is changed, notify view model to update.
- this._koDescription.subscribe((newValue: string) => {
- this._shouter.notifySubscribers(newValue, 'description');
- });
- const bindings: IKnockoutCrudBindingContext = {
- listName: this.properties.listName,
- shouter: this._shouter
- };
- ko.applyBindings(bindings, this._componentElement);
- return super.onInit();
- }
- public render(): void {
- if (!this.renderedOnce) {
- this.domElement.appendChild(this._componentElement);
- }
- this._koDescription(this.properties.listName);
- }
- private _createComponentElement(tagName: string): HTMLElement {
- const componentElement: HTMLElement = document.createElement('div');
- componentElement.setAttribute('data-bind', `component: { name: "${tagName}", params: $data }`);
- return componentElement;
- }
- private _registerComponent(tagName: string): void {
- ko.components.register(
- tagName,
- {
- viewModel: KnockoutCrudViewModel,
- template: require('./KnockoutCrud.template.html'),
- synchronous: false
- }
- );
- }
- protected get dataVersion(): Version {
- return Version.parse('1.0');
- }
- protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
- return {
- pages: [
- {
- header: {
- description: strings.PropertyPaneDescription
- },
- groups: [
- {
- groupName: strings.BasicGroupName,
- groupFields: [
- PropertyPaneTextField('listName', {
- label: strings.ListNameFieldLabel
- })
- ]
- }
- ]
- }
- ]
- };
- }
- }
Step 6
Update the ViewModel inside KnockoutCrudViewModel.ts to reflect listName property
Update the ViewModel inside KnockoutCrudViewModel.ts to reflect listName property
- import * as ko from 'knockout';
- import styles from './KnockoutCrud.module.scss';
- import { IKnockoutCrudWebPartProps } from './KnockoutCrudWebPart';
- export interface IKnockoutCrudBindingContext extends IKnockoutCrudWebPartProps {
- shouter: KnockoutSubscribable<{}>;
- }
- export default class KnockoutCrudViewModel {
- public listName: KnockoutObservable<string> = ko.observable('');
- public knockoutCrudClass: string = styles.knockoutCrud;
- public containerClass: string = styles.container;
- public rowClass: string = styles.row;
- public columnClass: string = styles.column;
- public titleClass: string = styles.title;
- public subTitleClass: string = styles.subTitle;
- public descriptionClass: string = styles.description;
- public buttonClass: string = styles.button;
- public labelClass: string = styles.label;
- constructor(bindings: IKnockoutCrudBindingContext) {
- this.listName(bindings.listName);
- // When web part description is updated, change this view model's description.
- bindings.shouter.subscribe((value: string) => {
- this.listName(value);
- }, this, 'listName');
- }
- }
In the template (KnockoutCrud.template.html) reflects the listName property
- <div data-bind="attr: { class:knockoutCrudClass }">
- <div data-bind="attr: { class:containerClass }">
- <div data-bind="attr: { class:rowClass }">
- <div data-bind="attr: { class:columnClass }">
- <span data-bind="attr: { class:titleClass }">Welcome to SharePoint!</span>
- <p data-bind="attr: { class:subTitleClass }">Customize SharePoint experiences using Web Parts.</p>
- <p data-bind="attr: { class:descriptionClass }, text:listName"></p>
- <a href="https://aka.ms/spfx" data-bind="attr: { class:buttonClass }">
- <span data-bind="attr: { class:labelClass }">Learn more</span>
- </a>
- </div>
- </div>
- </div>
- </div>
Step 8
In the command prompt, type “gulp serve”
Step 9
In the SharePoint local workbench page, add the web part.
Step 10
Edit the web part to ensure the listName property pane field is getting reflected.
In the command prompt, type “gulp serve”
Step 9
In the SharePoint local workbench page, add the web part.
Step 10
Edit the web part to ensure the listName property pane field is getting reflected.
Configure ViewModel
Step 1
Open KnockoutCrudViewModel.ts, and add the below import statements
- import { IWebPartContext } from '@microsoft/sp-webpart-base';
- import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
Step 2
Add context to interface IKnockoutCrudBindingContext
Add context to interface IKnockoutCrudBindingContext
- export interface IKnockoutCrudBindingContext extends IKnockoutCrudWebPartProps {
- shouter: KnockoutSubscribable<{}>;
- context: IWebPartContext;
- }
Add an interface for representing SharePoint list item
- export interface IListItem {
- Id: number;
- Title: string;
- }
Step 4
Add the below property to bind to UI
Add the below property to bind to UI
- public message: KnockoutObservable<string> = ko.observable('');
Implement generic method to get latest item id
- private getLatestItemId(): Promise<number> {
- return this._context.spHttpClient.get(this._context.pageContext["web"]["absoluteUrl"]
- + `/_api/web/lists/GetByTitle('${this._listName}')/items?$orderby=Id desc&$top=1&$select=id`, SPHttpClient.configurations.v1)
- .then((response: SPHttpClientResponse): Promise<any> => {
- return response.json();
- })
- .then((data: any): number => {
- this.message("Load succeeded");
- return data.value[0].ID;
- },
- (error: any) => {
- this.message("Load failed");
- }) as Promise<number>;
- }
Implement Create Operation
We will use the REST API to add the item to list.
We will use the REST API to add the item to list.
- private createItem(): void {
- const body: string = JSON.stringify({
- 'Title': `Item ${new Date()}`
- });
- this._context.spHttpClient.post(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items`,
- SPHttpClient.configurations.v1,
- {
- headers: {
- 'Accept': 'application/json;odata=nometadata',
- 'Content-type': 'application/json;odata=nometadata',
- 'odata-version': ''
- },
- body: body
- })
- .then((response: SPHttpClientResponse): Promise<IListItem> => {
- return response.json();
- })
- .then((item: IListItem): void => {
- this.message(`Item '${item.Title}' (ID: ${item.Id}) successfully created`);
- }, (error: any): void => {
- this.message('Error while creating the item: ' + error);
- });
- }
Implement Read Operation
We will use REST API to read the latest item.
We will use REST API to read the latest item.
- private readItem(): void {
- this.getLatestItemId()
- .then((itemId: number): Promise<SPHttpClientResponse> => {
- if (itemId === -1) {
- throw new Error('No items found in the list');
- }
- this.message(`Loading information about item ID: ${itemId}...`);
- return this._context.spHttpClient.get(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${itemId})?$select=Title,Id`,
- SPHttpClient.configurations.v1,
- {
- headers: {
- 'Accept': 'application/json;odata=nometadata',
- 'odata-version': ''
- }
- });
- })
- .then((response: SPHttpClientResponse): Promise<IListItem> => {
- return response.json();
- })
- .then((item: IListItem): void => {
- this.message(`Item ID: ${item.Id}, Title: ${item.Title}`);
- }, (error: any): void => {
- this.message('Loading latest item failed with error: ' + error);
- });
- }
Implement Update Operation
We will use REST API to update the latest item.
We will use REST API to update the latest item.
- private updateItem(): void {
- let latestItemId: number = undefined;
- this.message('Loading latest item...');
- this.getLatestItemId()
- .then((itemId: number): Promise<SPHttpClientResponse> => {
- if (itemId === -1) {
- throw new Error('No items found in the list');
- }
- latestItemId = itemId;
- this.message(`Loading information about item ID: ${itemId}...`);
- return this._context.spHttpClient.get(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${latestItemId})?$select=Title,Id`,
- SPHttpClient.configurations.v1,
- {
- headers: {
- 'Accept': 'application/json;odata=nometadata',
- 'odata-version': ''
- }
- });
- })
- .then((response: SPHttpClientResponse): Promise<IListItem> => {
- return response.json();
- })
- .then((item: IListItem): void => {
- this.message(`Item ID1: ${item.Id}, Title: ${item.Title}`);
- const body: string = JSON.stringify({
- 'Title': `Updated Item ${new Date()}`
- });
- this._context.spHttpClient.post(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${item.Id})`,
- SPHttpClient.configurations.v1,
- {
- headers: {
- 'Accept': 'application/json;odata=nometadata',
- 'Content-type': 'application/json;odata=nometadata',
- 'odata-version': '',
- 'IF-MATCH': '*',
- 'X-HTTP-Method': 'MERGE'
- },
- body: body
- })
- .then((response: SPHttpClientResponse): void => {
- this.message(`Item with ID: ${latestItemId} successfully updated`);
- }, (error: any): void => {
- this.message(`Error updating item: ${error}`);
- });
- });
- }
Implement Delete Operation
We will use REST API to delete the latest item.
We will use REST API to delete the latest item.
- private deleteItem(): void {
- if (!window.confirm('Are you sure you want to delete the latest item?')) {
- return;
- }
- this.message('Loading latest items...');
- let latestItemId: number = undefined;
- let etag: string = undefined;
- this.getLatestItemId()
- .then((itemId: number): Promise<SPHttpClientResponse> => {
- if (itemId === -1) {
- throw new Error('No items found in the list');
- }
- latestItemId = itemId;
- this.message(`Loading information about item ID: ${latestItemId}...`);
- return this._context.spHttpClient.get(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${latestItemId})?$select=Id`,
- SPHttpClient.configurations.v1,
- {
- headers: {
- 'Accept': 'application/json;odata=nometadata',
- 'odata-version': ''
- }
- });
- })
- .then((response: SPHttpClientResponse): Promise<IListItem> => {
- etag = response.headers.get('ETag');
- return response.json();
- })
- .then((item: IListItem): Promise<SPHttpClientResponse> => {
- this.message(`Deleting item with ID: ${latestItemId}...`);
- return this._context.spHttpClient.post(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${item.Id})`,
- SPHttpClient.configurations.v1,
- {
- headers: {
- 'Accept': 'application/json;odata=nometadata',
- 'Content-type': 'application/json;odata=verbose',
- 'odata-version': '',
- 'IF-MATCH': etag,
- 'X-HTTP-Method': 'DELETE'
- }
- });
- })
- .then((response: SPHttpClientResponse): void => {
- this.message(`Item with ID: ${latestItemId} successfully deleted`);
- }, (error: any): void => {
- this.message(`Error deleting item: ${error}`);
- });
- }
Step 6
In KnockoutCrudWebPart.ts file update the bindings in OnInit method
In KnockoutCrudWebPart.ts file update the bindings in OnInit method
- const bindings: IKnockoutCrudBindingContext = {
- listName: this.properties.listName,
- context: this.context,
- shouter: this._shouter
- };
Step 1
Open KnockoutCrud.template.html under “\src\webparts\knockoutCrud\” folder.
Step 2
Modify HTML template to include buttons for CRUD operations and bind event handlers to each of the button
- <div data-bind="attr: { class:knockoutCrudClass }">
- <div data-bind="attr: { class:containerClass }">
- <div data-bind="attr: { class:rowClass }">
- <div data-bind="attr: { class:columnClass }">
- <span data-bind="attr: { class:titleClass }">Welcome to SharePoint!</span>
- <p data-bind="attr: { class:subTitleClass }">Customize SharePoint experiences using Web Parts.</p>
- <p data-bind="attr: { class:descriptionClass }, text:listName"></p>
- <div data-bind="attr: {class:rowClass}">
- <button data-bind="attr: {class: buttonClass}, click: createItem">
- <label class="attr: {class: labelClass}">Create item</label>
- </button>
- <button data-bind="attr: {class: buttonClass}, click: readItem">
- <label class="attr: {class: labelClass}">Read item</label>
- </button>
- </div>
- <div data-bind="attr: {class:rowClass}">
- <button data-bind="attr: {class: buttonClass}, click: updateItem">
- <label class="attr: {class: labelClass}">Update item</label>
- </button>
- <button data-bind="attr: {class: buttonClass}, click: deleteItem">
- <label class="attr: {class: labelClass}">Delete item</label>
- </button>
- </div>
- <div data-bind="attr: {class:rowClass}">
- <p class="ms-font-l" data-bind="{ css: 'ms-fontColor-white', text: message }"></p>
- </div>
- </div>
- </div>
- </div>
- </div>
- On the command prompt, type “gulp serve”
- Open SharePoint site
- Navigate to /_layouts/15/workbench.aspx
- Add the webpart to page.
- Edit webpart, in the properties pane type the list name
- Click the buttons (Create Item, Read Item, Update Item, and Delete Item) one by one to test the webpart
- Verify the operations are taking place in the SharePoint list.
Create Operation
Read Operation
Update Operation
Delete Operation
Troubleshooting
In some cases SharePoint workbench (https://[tenant].sharepoint.com/_layouts/15/workbench.aspx) shows the below error although “gulp serve” is running.
Open the below url in the next tab of the browser. Accept the warning message.
https://localhost:4321/temp/manifests.js
Summary
Knockout JS is natively supported by SharePoint framework. SPFx generates all needed Knockout templates and bindings for you to get started with the development.
In some cases SharePoint workbench (https://[tenant].sharepoint.com/_layouts/15/workbench.aspx) shows the below error although “gulp serve” is running.
Open the below url in the next tab of the browser. Accept the warning message.
https://localhost:4321/temp/manifests.js
Summary
Knockout JS is natively supported by SharePoint framework. SPFx generates all needed Knockout templates and bindings for you to get started with the development.

Join the conversation! Your thoughts help the community grow.