SharePoint Framework - CRUD Operations Using Knockout.JS

In the article Develop First Client Side Web Part, we developed basic SharePoint client web part which can run independently without any interaction with SharePoint.

Overview

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.
  1. md spfx-crud-knockoutjs  
Navigate to the above-created directory.
  1. cd spfx-crud-knockout js  
Run Yeoman SharePoint Generator to create the solution.
  1. yo @microsoft/sharepoint  
Yeoman generator will present you with the wizard by asking questions about the solution to be created.

SharePoint Framework - CRUD operations using Knockout JS 
 
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.
 
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).
Selected choice - SharePoint Online only (latest)
 
Location of files
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.
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.
Selected choice - WebPart
 
Web part 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.
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)
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
  1. npm shrinkwrap  
In the command prompt type the below command to open the solution in the code editor of your choice.
  1. code .   
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.

Step 1

Open mystrings.d.ts under \src\webparts\knockoutCrud\loc\ folder

Step 2

Rename DescriptionFieldLabel to ListNameFieldLabel

SharePoint Framework - CRUD operations using Knockout JS 
  1. declare interface IKnockoutCrudWebPartStrings {  
  2.   PropertyPaneDescription: string;  
  3.   BasicGroupName: string;  
  4.   ListNameFieldLabel: string;  
  5. }  
  6.   
  7. declare module 'KnockoutCrudWebPartStrings' {  
  8.   const strings: IKnockoutCrudWebPartStrings;  
  9.   export = strings;  
  10. }  
Step 3

In en-us.js file under \src\webparts\knockoutCrud\loc\ folder set the display name for listName property

SharePoint Framework - CRUD operations using Knockout JS  
  1. define([], function() {  
  2.   return {  
  3.     "PropertyPaneDescription""Description",  
  4.     "BasicGroupName""Group Name",  
  5.     "ListNameFieldLabel""List Name"  
  6.   }  
  7. });  
Step 4

Open main webpart file (KnockoutCrudWebPart.ts) under \src\webparts\knockoutCrud folder.

Step 5

Rename description property pane field to listName

SharePoint Framework - CRUD operations using Knockout JS  
  1. import * as ko from 'knockout';  
  2. import { Version } from '@microsoft/sp-core-library';  
  3. import {  
  4.   BaseClientSideWebPart,  
  5.   IPropertyPaneConfiguration,  
  6.   PropertyPaneTextField  
  7. } from '@microsoft/sp-webpart-base';  
  8.   
  9. import * as strings from 'KnockoutCrudWebPartStrings';  
  10. import KnockoutCrudViewModel, { IKnockoutCrudBindingContext } from './KnockoutCrudViewModel';  
  11.   
  12. let _instance: number = 0;  
  13.   
  14. export interface IKnockoutCrudWebPartProps {  
  15.   listName: string;  
  16. }  
  17.   
  18. export default class KnockoutCrudWebPart extends BaseClientSideWebPart<IKnockoutCrudWebPartProps> {  
  19.   private _id: number;  
  20.   private _componentElement: HTMLElement;  
  21.   private _koDescription: KnockoutObservable<string> = ko.observable('');  
  22.   
  23.   /** 
  24.    * Shouter is used to communicate between web part and view model. 
  25.    */  
  26.   private _shouter: KnockoutSubscribable<{}> = new ko.subscribable();  
  27.   
  28.   /** 
  29.    * Initialize the web part. 
  30.    */  
  31.   protected onInit(): Promise<void> {  
  32.     this._id = _instance++;  
  33.   
  34.     const tagName: string = `ComponentElement-${this._id}`;  
  35.     this._componentElement = this._createComponentElement(tagName);  
  36.     this._registerComponent(tagName);  
  37.   
  38.     // When web part description is changed, notify view model to update.  
  39.     this._koDescription.subscribe((newValue: string) => {  
  40.       this._shouter.notifySubscribers(newValue, 'description');  
  41.     });  
  42.   
  43.     const bindings: IKnockoutCrudBindingContext = {  
  44.       listName: this.properties.listName,  
  45.       shouter: this._shouter  
  46.     };  
  47.   
  48.     ko.applyBindings(bindings, this._componentElement);  
  49.   
  50.     return super.onInit();  
  51.   }  
  52.   
  53.   public render(): void {  
  54.     if (!this.renderedOnce) {  
  55.       this.domElement.appendChild(this._componentElement);  
  56.     }  
  57.   
  58.     this._koDescription(this.properties.listName);  
  59.   }  
  60.   
  61.   private _createComponentElement(tagName: string): HTMLElement {  
  62.     const componentElement: HTMLElement = document.createElement('div');  
  63.     componentElement.setAttribute('data-bind', `component: { name: "${tagName}"params: $data }`);  
  64.     return componentElement;  
  65.   }  
  66.   
  67.   private _registerComponent(tagName: string): void {  
  68.     ko.components.register(  
  69.       tagName,  
  70.       {  
  71.         viewModel: KnockoutCrudViewModel,  
  72.         template: require('./KnockoutCrud.template.html'),  
  73.         synchronous: false  
  74.       }  
  75.     );  
  76.   }  
  77.   
  78.   protected get dataVersion(): Version {  
  79.     return Version.parse('1.0');  
  80.   }  
  81.   
  82.   protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {  
  83.     return {  
  84.       pages: [  
  85.         {  
  86.           header: {  
  87.             description: strings.PropertyPaneDescription  
  88.           },  
  89.           groups: [  
  90.             {  
  91.               groupName: strings.BasicGroupName,  
  92.               groupFields: [  
  93.                 PropertyPaneTextField('listName', {  
  94.                   label: strings.ListNameFieldLabel  
  95.                 })  
  96.               ]  
  97.             }  
  98.           ]  
  99.         }  
  100.       ]  
  101.     };  
  102.   }  
  103. }  
Step 6

Update the ViewModel inside KnockoutCrudViewModel.ts to reflect listName property
  1. import * as ko from 'knockout';  
  2. import styles from './KnockoutCrud.module.scss';  
  3. import { IKnockoutCrudWebPartProps } from './KnockoutCrudWebPart';  
  4.   
  5. export interface IKnockoutCrudBindingContext extends IKnockoutCrudWebPartProps {  
  6.   shouter: KnockoutSubscribable<{}>;  
  7. }  
  8.   
  9. export default class KnockoutCrudViewModel {  
  10.   public listName: KnockoutObservable<string> = ko.observable('');  
  11.   
  12.   public knockoutCrudClass: string = styles.knockoutCrud;  
  13.   public containerClass: string = styles.container;  
  14.   public rowClass: string = styles.row;  
  15.   public columnClass: string = styles.column;  
  16.   public titleClass: string = styles.title;  
  17.   public subTitleClass: string = styles.subTitle;  
  18.   public descriptionClass: string = styles.description;  
  19.   public buttonClass: string = styles.button;  
  20.   public labelClass: string = styles.label;  
  21.   
  22.   constructor(bindings: IKnockoutCrudBindingContext) {  
  23.     this.listName(bindings.listName);  
  24.   
  25.     // When web part description is updated, change this view model's description.  
  26.     bindings.shouter.subscribe((value: string) => {  
  27.       this.listName(value);  
  28.     }, this'listName');  
  29.   }  
  30. }  
Step 7

In the template (KnockoutCrud.template.html) reflects the listName property

SharePoint Framework - CRUD operations using Knockout JS 
  1. <div data-bind="attr: { class:knockoutCrudClass }">  
  2.     <div data-bind="attr: { class:containerClass }">  
  3.       <div data-bind="attr: { class:rowClass }">  
  4.         <div data-bind="attr: { class:columnClass }">  
  5.           <span data-bind="attr: { class:titleClass }">Welcome to SharePoint!</span>  
  6.           <p data-bind="attr: { class:subTitleClass }">Customize SharePoint experiences using Web Parts.</p>  
  7.           <p data-bind="attr: { class:descriptionClass }, text:listName"></p>  
  8.           <a href="https://aka.ms/spfx" data-bind="attr: { class:buttonClass }">  
  9.             <span data-bind="attr: { class:labelClass }">Learn more</span>  
  10.           </a>  
  11.         </div>  
  12.       </div>  
  13.     </div>  
  14.   </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.
 
SharePoint Framework - CRUD operations using Knockout JS 

Configure ViewModel

Step 1

Open KnockoutCrudViewModel.ts, and add the below import statements
  1. import { IWebPartContext } from '@microsoft/sp-webpart-base';  
  2. import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';  
SharePoint Framework - CRUD operations using Knockout JS 
 
Step 2

Add context to interface IKnockoutCrudBindingContext
  1. export interface IKnockoutCrudBindingContext extends IKnockoutCrudWebPartProps {  
  2.   shouter: KnockoutSubscribable<{}>;  
  3.   context: IWebPartContext;  
  4. }  
Step 3

Add an interface for representing SharePoint list item
  1. export interface IListItem {  
  2.   Id: number;  
  3.   Title: string;  
  4. }  
SharePoint Framework - CRUD operations using Knockout JS 
 
Step 4

Add the below property to bind to UI
  1. public message: KnockoutObservable<string> = ko.observable('');  
Step 5

Implement generic method to get latest item id
  1. private getLatestItemId(): Promise<number> {  
  2.     return this._context.spHttpClient.get(this._context.pageContext["web"]["absoluteUrl"]  
  3.       + `/_api/web/lists/GetByTitle('${this._listName}')/items?$orderby=Id desc&$top=1&$select=id`, SPHttpClient.configurations.v1)  
  4.       .then((response: SPHttpClientResponse): Promise<any> => {  
  5.         return response.json();  
  6.       })  
  7.       .then((data: any): number => {  
  8.         this.message("Load succeeded");  
  9.         return data.value[0].ID;  
  10.       },  
  11.       (error: any) => {  
  12.         this.message("Load failed");  
  13.       }) as Promise<number>;  
  14.   }  
Implement Create Operation

We will use the REST API to add the item to list.
  1. private createItem(): void {  
  2.     const body: string = JSON.stringify({  
  3.       'Title': `Item ${new Date()}`  
  4.     });  
  5.   
  6.     this._context.spHttpClient.post(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items`,  
  7.     SPHttpClient.configurations.v1,  
  8.     {  
  9.       headers: {  
  10.         'Accept''application/json;odata=nometadata',  
  11.         'Content-type''application/json;odata=nometadata',  
  12.         'odata-version'''  
  13.       },  
  14.       body: body  
  15.     })  
  16.     .then((response: SPHttpClientResponse): Promise<IListItem> => {  
  17.       return response.json();  
  18.     })  
  19.     .then((item: IListItem): void => {  
  20.       this.message(`Item '${item.Title}' (ID: ${item.Id}) successfully created`);  
  21.     }, (error: any): void => {  
  22.       this.message('Error while creating the item: ' + error);  
  23.     });  
  24.   }  
Implement Read Operation

We will use REST API to read the latest item.
  1. private readItem(): void {  
  2.     this.getLatestItemId()  
  3.       .then((itemId: number): Promise<SPHttpClientResponse> => {  
  4.         if (itemId === -1) {  
  5.           throw new Error('No items found in the list');  
  6.         }  
  7.   
  8.         this.message(`Loading information about item ID: ${itemId}...`);  
  9.           
  10.         return this._context.spHttpClient.get(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${itemId})?$select=Title,Id`,  
  11.           SPHttpClient.configurations.v1,  
  12.           {  
  13.             headers: {  
  14.               'Accept''application/json;odata=nometadata',  
  15.               'odata-version'''  
  16.             }  
  17.           });  
  18.       })  
  19.       .then((response: SPHttpClientResponse): Promise<IListItem> => {  
  20.         return response.json();  
  21.       })  
  22.       .then((item: IListItem): void => {  
  23.         this.message(`Item ID: ${item.Id}, Title: ${item.Title}`);  
  24.       }, (error: any): void => {  
  25.         this.message('Loading latest item failed with error: ' + error);  
  26.       });  
  27.   }  
Implement Update Operation

We will use REST API to update the latest item.
  1. private updateItem(): void {  
  2.     let latestItemId: number = undefined;  
  3.     this.message('Loading latest item...');  
  4.   
  5.     this.getLatestItemId()  
  6.       .then((itemId: number): Promise<SPHttpClientResponse> => {  
  7.         if (itemId === -1) {  
  8.           throw new Error('No items found in the list');  
  9.         }  
  10.   
  11.         latestItemId = itemId;  
  12.         this.message(`Loading information about item ID: ${itemId}...`);  
  13.           
  14.         return this._context.spHttpClient.get(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${latestItemId})?$select=Title,Id`,  
  15.           SPHttpClient.configurations.v1,  
  16.           {  
  17.             headers: {  
  18.               'Accept''application/json;odata=nometadata',  
  19.               'odata-version'''  
  20.             }  
  21.           });  
  22.       })  
  23.       .then((response: SPHttpClientResponse): Promise<IListItem> => {  
  24.         return response.json();  
  25.       })  
  26.       .then((item: IListItem): void => {  
  27.         this.message(`Item ID1: ${item.Id}, Title: ${item.Title}`);  
  28.   
  29.         const body: string = JSON.stringify({  
  30.           'Title': `Updated Item ${new Date()}`  
  31.         });  
  32.   
  33.         this._context.spHttpClient.post(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${item.Id})`,  
  34.           SPHttpClient.configurations.v1,  
  35.           {  
  36.             headers: {  
  37.               'Accept''application/json;odata=nometadata',  
  38.               'Content-type''application/json;odata=nometadata',  
  39.               'odata-version''',  
  40.               'IF-MATCH''*',  
  41.               'X-HTTP-Method''MERGE'  
  42.             },  
  43.             body: body  
  44.           })  
  45.           .then((response: SPHttpClientResponse): void => {  
  46.             this.message(`Item with ID: ${latestItemId} successfully updated`);  
  47.           }, (error: any): void => {  
  48.             this.message(`Error updating item: ${error}`);  
  49.           });  
  50.       });  
  51.   }  
Implement Delete Operation

We will use REST API to delete the latest item.
  1. private deleteItem(): void {  
  2.     if (!window.confirm('Are you sure you want to delete the latest item?')) {  
  3.       return;  
  4.     }  
  5.   
  6.     this.message('Loading latest items...');  
  7.     let latestItemId: number = undefined;  
  8.     let etag: string = undefined;  
  9.     this.getLatestItemId()  
  10.       .then((itemId: number): Promise<SPHttpClientResponse> => {  
  11.         if (itemId === -1) {  
  12.           throw new Error('No items found in the list');  
  13.         }  
  14.   
  15.         latestItemId = itemId;  
  16.         this.message(`Loading information about item ID: ${latestItemId}...`);  
  17.         return this._context.spHttpClient.get(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${latestItemId})?$select=Id`,  
  18.           SPHttpClient.configurations.v1,  
  19.           {  
  20.             headers: {  
  21.               'Accept''application/json;odata=nometadata',  
  22.               'odata-version'''  
  23.             }  
  24.           });  
  25.       })  
  26.       .then((response: SPHttpClientResponse): Promise<IListItem> => {  
  27.         etag = response.headers.get('ETag');  
  28.         return response.json();  
  29.       })  
  30.       .then((item: IListItem): Promise<SPHttpClientResponse> => {  
  31.         this.message(`Deleting item with ID: ${latestItemId}...`);  
  32.         return this._context.spHttpClient.post(`${this._context.pageContext["web"]["absoluteUrl"]}/_api/web/lists/getbytitle('${this._listName}')/items(${item.Id})`,  
  33.           SPHttpClient.configurations.v1,  
  34.           {  
  35.             headers: {  
  36.               'Accept''application/json;odata=nometadata',  
  37.               'Content-type''application/json;odata=verbose',  
  38.               'odata-version''',  
  39.               'IF-MATCH': etag,  
  40.               'X-HTTP-Method''DELETE'  
  41.             }  
  42.           });  
  43.       })  
  44.       .then((response: SPHttpClientResponse): void => {  
  45.         this.message(`Item with ID: ${latestItemId} successfully deleted`);  
  46.       }, (error: any): void => {  
  47.         this.message(`Error deleting item: ${error}`);  
  48.       });  
  49.   }  
Step 6

In KnockoutCrudWebPart.ts file update the bindings in OnInit method
  1. const bindings: IKnockoutCrudBindingContext = {  
  2.       listName: this.properties.listName,  
  3.       context: this.context,  
  4.       shouter: this._shouter  
  5. };  
Add Controls to Knockout template

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
  1. <div data-bind="attr: { class:knockoutCrudClass }">  
  2.     <div data-bind="attr: { class:containerClass }">  
  3.       <div data-bind="attr: { class:rowClass }">  
  4.         <div data-bind="attr: { class:columnClass }">  
  5.           <span data-bind="attr: { class:titleClass }">Welcome to SharePoint!</span>  
  6.           <p data-bind="attr: { class:subTitleClass }">Customize SharePoint experiences using Web Parts.</p>  
  7.           <p data-bind="attr: { class:descriptionClass }, text:listName"></p>  
  8.             
  9.           <div data-bind="attr: {class:rowClass}">  
  10.               <button data-bind="attr: {class: buttonClass}, click: createItem">  
  11.                 <label class="attr: {class: labelClass}">Create item</label>  
  12.               </button>  
  13.               <button data-bind="attr: {class: buttonClass}, click: readItem">  
  14.                 <label class="attr: {class: labelClass}">Read item</label>  
  15.               </button>  
  16.           </div>  
  17.   
  18.           <div data-bind="attr: {class:rowClass}">  
  19.               <button data-bind="attr: {class: buttonClass}, click: updateItem">  
  20.                 <label class="attr: {class: labelClass}">Update item</label>  
  21.               </button>  
  22.               <button data-bind="attr: {class: buttonClass}, click: deleteItem">  
  23.                 <label class="attr: {class: labelClass}">Delete item</label>  
  24.               </button>  
  25.           </div>  
  26.   
  27.           <div data-bind="attr: {class:rowClass}">  
  28.             <p class="ms-font-l" data-bind="{ css: 'ms-fontColor-white', text: message }"></p>  
  29.           </div>  
  30.         </div>  
  31.       </div>  
  32.     </div>  
  33.   </div>  
Test the WebPart
  1. On the command prompt, type “gulp serve”
  2. Open SharePoint site
  3. Navigate to /_layouts/15/workbench.aspx
  4. Add the webpart to page.
  5. Edit webpart, in the properties pane type the list name
  6. Click the buttons (Create Item, Read Item, Update Item, and Delete Item) one by one to test the webpart
  7. Verify the operations are taking place in the SharePoint list.
Create Operation

SharePoint Framework - CRUD operations using Knockout JS 
 
Read Operation
 
SharePoint Framework - CRUD operations using Knockout JS 
 
Update Operation
 
SharePoint Framework - CRUD operations using Knockout JS 
 
Delete Operation
 
SharePoint Framework - CRUD operations using Knockout JS 
 
Troubleshooting

In some cases SharePoint workbench (https://[tenant].sharepoint.com/_layouts/15/workbench.aspx) shows the below error although “gulp serve” is running.

SharePoint Framework - CRUD operations using Knockout JS 

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.