Home > Chris Stretton, Work > Using Knockout.js in a SharePoint Context

Using Knockout.js in a SharePoint Context

The excellent Knockout.js library is an MVVM (Model, View, ViewModel) library.

Using it you can completely abstract the logic from the presentation in your web applications, allowing dynamic and responsive UIs to be created without having to manage all of the fiddly UI updates directly.

The Knockout site has a great set of tutorials on how to use it, but I thought I would bring them into a SharePoint context.

Specifically, I am going to cover their 5th tutorial, Loading and Saving Data. In this tutorial they give the example of a simple task list that a user can update and delete dynamically. If you do not know Knockout at all, I recommend you go through this tutorial before continuing. Go ahead, I can wait.

Back with us? Excellent. Hopefully by now you have some idea of the power of Knockout, so lets try and reproduce this using a SharePoint list as the data source.

The first thing to do is to create our list. For this I am using a simple custom list named ‘My Tasks’ with an additional Completed Yes/No field, and I have pre-populated it with some entries.

knockout1

Next we need to consider how we are going to create our view within our site. For my example I have chosen to simply create the view as a file in a document library, then link to it using a Content Editor Web Part. However you could do this a number of ways, you could embed your JavaScript into the master page and put your data bindings directly into a page layout, or you could create a visual web part. The possibilities are endless, this is SharePoint after all! :)

I put the view, along with the script references to Knockout, the ever useful jQuery and my View Model into a single file and save it into the document library. I have also put some style information in too. Here is the view model as I created it:

<style type="text/css">

	#taskContainer {
		width: 500px;
	}
	
	#errorBox {
		text-align: center;
		border: 1px solid #600;
		margin-bottom: 10px;
		padding: 10px;
		background: #f99;
		color: #600;
	}
	
	#saveBox {
		text-align: center;
		border: 1px solid #060;
		margin-bottom: 10px;
		padding: 10px;
		background: #9f9;
		color: #060;
	}
	
	#taskContainer ul {
		padding: 10px;
		background: #eaeaea;
		padding: 0;
	}
	
	#taskContainer ul li {
		list-style: none;
		padding: 3px;
	}
	
	#taskContainer input[type=text] {
		width: 390px;
	}
	
</style>
<div id="taskContainer">
	<h3>Tasks</h3>
	
	<div id="errorBox" data-bind="text: errorMessage, visible: errorMessage"></div>
	<div id="saveBox" data-bind="text: saveMessage, visible: saveMessage"></div>
	
	Add task: <input data-bind="value: newTaskText" placeholder="What needs to be done?"/>
	<button data-bind="click: addTask">Add</button>
	
	<ul id="myTaskBox" data-bind="foreach: tasks, visible: tasks().length > 0">
	    <li>
	        <input type="checkbox" data-bind="checked: Completed" />
	        <input data-bind="value: Title, disable: Completed" />
	        <a href="#" data-bind="click: $parent.removeTask">Delete</a>
	    </li> 
	</ul>
	
	You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s)
	<span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>
	
	<button data-bind="click: save">Save</button>
</div>

<script type="text/javascript" src="../webdevdocuments/knockout.js"></script>
<script type="text/javascript" src="../webdevdocuments/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="../webdevdocuments/ViewModel.js"></script>

Note the addition of some message boxes, the original tutorial does not include them but I felt they were nicer than simple alert boxes.

Also note that, unlike the original, I have not used a form element for the new task area. This is because form elements can cause havoc in SharePoint pages, as the entire page is wrapped in a form for post-backs.

Other than that, our view is mostly unchanged.

Now, on to the JavaScript!

The main differences here lie in the way that we send and receive data from SharePoint. In their examples they use jQuery to query and post data to REST APIs, and while SharePoint 2010 and 2013 do come with their own suite of REST / oData APIs, there are some issues in using them.

  • Update and Delete calls can only update or delete one item per call, to do the bulk updating this system indicates we would have to make a request per item. Not ideal!
  • Due to the way SharePoint handles concurrency, you have to pass around an eTag so that SharePoint can determine if an item has been updated since requested. While this is great for high concurrency systems with multiple people editing the same data, for a personal tasks list this is not a nice feature.

Luckily REST is not our only option. SharePoint has the wonderful Client Side Object Model (CSOM), which can do all of our loading and saving for us!

Here is my View Model, updated to use the CSOM.

(function() {

	function Task(data) {
	    this.Title = ko.observable(data.Title);
	    this.Completed = ko.observable(data.Completed);
	    
	    // An additional reference to store the SharePoint list item id.
	    this.Id = ko.observable(data.Id);
	}
	
	function TaskListViewModel() {
	    // Data
	    var self = this;
	    self.tasks = ko.observableArray([]);
	    self.newTaskText = ko.observable();
	    
	    // Additional bindings to use for error and saved messages.
	    self.saveMessage = ko.observable(false);
	    self.errorMessage = ko.observable(false);
	    
	    self.incompleteTasks = ko.computed(function() {
	        return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.Completed() && !task._destroy});
	    });
	
	    // Operations
	    self.addTask = function() {
	        self.tasks.push(new Task({ Title: this.newTaskText(), Completed: false, Id: "New" }));
	        self.newTaskText("");
	    };
	    
	    self.removeTask = function(task) {
			self.tasks.destroy(task)
	    };
	    
	    self.save = function() {

	    	for (var task in self.tasks()) {
	    	
	    		var createdTasks = [];
	    		
	    		// Build a request up to send with the CSOM.
	    	
	    		if (self.tasks()[task]._destroy) {
					// Handle deleted objects
	    			// Deleted items that are marked "new" have never been saved to SharePoint to start with,
	    			if (self.tasks()[task].Id() != "New") {
			    		var listItem = taskList.getItemById(self.tasks()[task].Id());
			    		listItem.deleteObject();
	    			}
	    		} else if (self.tasks()[task].Id() == "New") {
	    			// Handle new objects to be created.
	    		
	    			var createInfo = new SP.ListItemCreationInformation();
	    			var listItem = taskList.addItem(createInfo);
	    			
	    			listItem.set_item("Title", self.tasks()[task].Title());
	    			listItem.set_item("Completed", self.tasks()[task].Completed());
	    			
	    			listItem.update();
	    			
	    			// Save a reference to both the SP.ListItem object and the KO Object so we can update
	    			// the latter with the former's ID once the object has been created.
	    			createdTasks.push({
	    				spItem: listItem,
	    				koItem: self.tasks()[task]
	    			});
	    			
	    			ctx.load(listItem);
	    		} else {
	    			// The item is neither new nor deleted, handle it as an update.
	    			var listItem = taskList.getItemById(self.tasks()[task].Id());
	    			
	    			listItem.set_item("Title", self.tasks()[task].Title());
	    			listItem.set_item("Completed", self.tasks()[task].Completed());
	    			
	    			listItem.update();
	    		}
	    		
	    	}

			// Nowe we have built our request, send it to the server for processing.	    	
	    	ctx.executeQueryAsync(function() {
	    	
	    		// Our save was successful. Now we need to itterate through our newly
	    		// created items and ensure that Knockout knows that the ID has changed.
	    		for(var item in createdTasks) {
	    			createdTasks[item].koItem.Id(createdTasks[item].spItem.get_id());
	    		}
	    		
	    		// Set our saved message.
	    		self.saveMessage("Saved successfully");
	    		
	    	}, function(sender, args) {
	    	
	    		// Our save failed, set the error message to show then log the actual error
	    		// to the JavaScript console if it exists.
	    		self.errorMessage("Error updating list items");
	    		if (typeof console != "undefined") {
	    			console.log(args.get_message());
	    		}
	    	});
	    	
	    };
	    
	    // Load the data from SharePoint
		// Get a context to the current site.
	    var ctx = new SP.ClientContext(_spPageContextInfo.webServerRelativeUrl);
	    
	    var web = ctx.get_web();
	    var taskList = web.get_lists().getByTitle("My Tasks");
	    
	    // Limit our task list to 50 tasks.
   	    var query = new SP.CamlQuery();
   	    query.set_viewXml("<View><RowLimit>50</RowLimit></View>");
   	    
   	    var taskItems = taskList.getItems(query);
   	    
   	    // Ensure the fields we want to retrieve are returned
   	    ctx.load(taskItems, "Include(ID,Title,Completed)");
   	    
   	    // Send our query to the server for processing.
   	    ctx.executeQueryAsync(function() {
   	    	var tasks = [];
   	    	var taskItemEnumerator = taskItems.getEnumerator();
   	    	
   	    	// Iterate through our retrieved data set and build an array of JSON objects containing
   	    	// the relevent properties.
   	    	while (taskItemEnumerator.moveNext()) {
   	    		tasks.push(
   	    			new Task({
	   	    			Title: taskItemEnumerator.get_current().get_item("Title"),
	   	    			Completed: taskItemEnumerator.get_current().get_item("Completed"),
	   	    			Id: taskItemEnumerator.get_current().get_item("ID")
	   	    		})
	   	    	);
   	    	}
   	    	
   	    	// Update the Knockout tasks array with our data from the server.
   	    	self.tasks(tasks);
   	    });
	    
	}
	
	
	$(document).ready(function() { // I use jQuery for this, but you could add an event listener to the document object instead.
		EnsureScriptFunc("sp.js", "SP.ClientContext", function() {
			ko.applyBindings(new TaskListViewModel());
		});
	});

})();

So there you go, once you put it all together you should end up with something looking like this:

knockout2

Cool, huh? It is by no means perfect. The two biggest issues with the implementation as it stands are:

  • Currently I am limiting the entire system to only showing 50 records, which is not ideal. This would be best handled by adding some pagination.
  • When you hit save it updates every item in the view model, regardless of whether it needs updating or not. This would be resolvable by subscribing to the Task Knockout object and keeping track of which need updating and which do not.

Additionally you could use some jQuery animation and the animated transitions example on the Knockout site to hide the save and error boxes once they are shown. Currently once they are visible they remain visible indefinitely.

Have fun with it, it’s a neat tool and can give quite powerful results.

via Chris on SharePoint http://spchris.com/2013/04/using-knockout-js-in-a-sharepoint-context/

Chris Stretton
SharePoint and Project Server Consultant

  • MCITP – SharePoint Administrator 2010
  • MCTS – Microsoft Project 2010 – Managing Projects, Project Server 2010, Configuration, SharePoint 2010, Configuration
  • Prince 2 – Practitioner

This article has been cross posted from spchris.com (original article)

About these ads
  1. January 7, 2014 at 17:24

    Very nice! I think I’ve tried a million examples but this one works right out of the gate on SP2010. Will definitely help me write my own.

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 1,691 other followers

%d bloggers like this: