Introduction
In this article, I demonstrate three real-world examples of list form customizations using KnockoutJs and SharePoint Client Side Rendering:
- Read-only field that can be switched to edit mode
- Inline add values to a lookup field
- Dependent fields
Background
Some people think that UX in SharePoint and other enterprise platforms is not important. I strongly disagree.
Bad UX is something what makes everyday work boring and non-effective. People are much more likely to make mistakes when they're dealing with bad interfaces. Even worse, it often happens that if UI is too complex and inconvenient to use, people tend to avoid using it altogether.
But we can change it, and I'm convinced that CSR and KnockoutJs is a great couple of tools for the task.
Client Side Rendering (CSR) is a JavaScript rendering engine that is used by default for displaying list views, list forms and search results in SharePoint 2013.
If you want to learn more about Client Side Rendering, please have a look at these articles:
- SharePoint 2013 Client Side Rendering: List Views
- SharePoint 2013 Client Side Rendering: List Forms
KnockoutJs is a MVVM framework, that implements two-way data binding concept in the field of JavaScript and HTML. You can read more about KnockoutJs on its official site.
This article doesn't aim to explain how KnockoutJs or CSR works, but rather it aims to demonstrate, how these two can be used efficiently together, what are advantages, and what are the gotchas. So if you're not familiar with KnockoutJs or CSR at all, please first visit the links above and get the basics, otherwise the article may be confusing.
Example 1: Read-only field that can be switched to edit mode
Let's say there's a field that is changed very rarely. For example on the list form below, it is obvious that Title field should not be touched on a regular basis:
To emphasize that renaming a city is a bad idea, I'm going to make this field read-only by default, and put "Edit" button next to it, so that when user clicks "Edit", he gets a confirmation dialog with a warning message, like this:
In terms of implementation, this means following those 4 steps:
- Override the field template
- Create read-only mode for the field, which will be shown by default and contains field control in display mode + "Edit button".
- Create edit mode for this field that contains field control in edit mode.
- Implement "Edit" button action, so that when it is clicked, we display confirmation dialog, and if "OK" is clicked, hide read-only mode and show edit mode.
Let's go briefly through each of those steps.
Step 1: Override default field template on Edit form
First step is easy and can be achieved with the following piece of code:
SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
Templates: {
Fields: {
"Title": {
EditForm: function(ctx) {
return 'some html code here';
},
},
},
},
});
Notice: internal name of the field should be used when overriding (in this example it is "Title").
Screenshot of the result:
Step 2: Create read-only mode for the field
In simplest case, we can just use ctx.CurrentFieldValue
- but this works properly only for simple text fields. Better idea is to reuse default display template of the field, that would work for any field type.
Unfortunately, it seems there isn't any completely supported way to reuse default field templates in CSR. Myself, I usually grab those templates from _defaultTemplates property of TemplateManager object, but also there're some other ways to do it - please use whatever suits you best.
So in case of _defaultTemplates, to grab a default template I use this piece of code:
SPClientTemplates._defaultTemplates.Fields.default.all.all[<Field type>][<Control mode>]
, here <Field type> is the type of field, e.g. "Text", "Note", etc.; and <Control mode> can be either one of "EditForm", "NewForm" and "DisplayForm".
The default template is a function, and obviously it accepts ctx
as the only parameter. Knowing all this, now I can easily create the readonly mode for the field:
SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
Templates: {
Fields: {
"Title": {
EditForm: function(ctx) {
var fieldType = ctx.CurrentFieldSchema.FieldType;
var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
return defaultTemplates[fieldType]["DisplayForm"](ctx) + '<button>Edit</button>';
},
},
},
},
});
Screenshot of the result:
Step 3: Create edit mode
Having the knowledge of how to reuse templates, creating edit mode is a primitive exercise:
var fieldType = ctx.CurrentFieldSchema.FieldType;
var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
return defaultTemplates[fieldType]["DisplayForm"](ctx) + '<button>Edit</button>'
+ defaultTemplates[fieldType]["EditForm"](ctx);
Screenshot of the result:
Step 4: Implement "Edit" button action
Now, it's of course possible to do mode switching with jQuery or vanilla JS, but KnockoutJs (KO) and other modern two-way binding JS frameworks provide so much more visual and easier way to create dynamic interfaces!...
In order to use KnockoutJs for this task, I need to do three simple things:
- Deploy KnockoutJs to the page
- Tweak our HTML a little and add data-bind attributes
- Create a javascript object that would represent page model and call ko.applyBindings
Deploying KnockoutJs can be done any way you want - via a ScriptLink custom action, master page, JSLink, etc.
After KnockoutJs is on page and can be used, let's change HTML and add data-bind attributes. Here's what I've got:
var fieldType = ctx.CurrentFieldSchema.FieldType;
var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
return '<div data-bind="visible: !editMode()">'
+ defaultTemplates[fieldType]["DisplayForm"](ctx)
+ '<button data-bind="click: switchToEditMode">Edit</button>'
+ '</div>'
+ '<div data-bind="visible: editMode()">'
+ defaultTemplates[fieldType]["EditForm"](ctx)
+ '</div>';
So here you can see that I wrapped read-only mode and edit mode into separate divs, and they're visible or hidden depending on certain editMode
field. This field is observable, thus it is actually a function and that's why I'm calling it to get it's value.
And also, the "Edit" button got click
binding, so that wherever it is clicked, certain switchToEditMode
method is called.
Now let's create page model object and add editMode
and switchToEditMode
there:
var model = {
editMode: ko.observable(false),
switchToEditMode: function() {
if (confirm('Are you sure want to rename a city!?'))
model.editMode(true);
}
};
This page model should obviously be bound to the HTML using ko.applyBindings
. The applyBindings
method works with DOM, hence HTML that is generated by our template should be rendered before we can call applyBindings
.
So obvious place to put ko.applyBindings is CSR PostRender handler. But it important to understand, that in list forms, rendering process happens for each field control, which means that PostRender will be called multiple times. That's why I usually have additional condition there, checking for last field in the form, so that ko.applyBindings is called only once.
Final code
So here's the final code:
SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
Templates: {
Fields: {
"Title": {
EditForm: function(ctx) {
var fieldType = ctx.CurrentFieldSchema.FieldType;
var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
return '<div data-bind="visible: !editMode()">'
+ defaultTemplates[fieldType]["DisplayForm"](ctx)
+ '<button data-bind="click: switchToEditMode">Edit</button>'
+ '</div>'
+ '<div data-bind="visible: editMode()">'
+ defaultTemplates[fieldType]["EditForm"](ctx)
+ '</div>';
},
},
},
},
OnPostRender: {
if (ctx.ListSchema.Field[0].Name == "Liked")
{
var model = {
editMode: ko.observable(false),
switchToEditMode: function() {
if (confirm('Are you sure want to rename a city!?'))
model.editMode(true);
}
};
ko.applyBindings(model);
}
}
});
And don't forget, that there's always some boiler plate when it comes to correctly including the script to the page and making it work with Minimal Download Strategy.
I usually use this skeleton code for this purpose, which proved to be very reliable:
SP.SOD.executeFunc("clienttemplates.js", "SPClientTemplates", function() {
function init() {
SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
});
}
RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~siteCollection/Style Library/file.js"), init);
init();
});
Note: Don't forget to change the file name and path.
Example 2: Inline add values to a lookup field
It happens, that users need to add values to a certain lookup very often. In this case, inline interface for adding items to lookup can save a lot of time and frustration. So let's say I entered a new city, but I don't have a corresponding country in the lookup:
I don't want to open a new window and navigate to the Countries list and so on. Instead, I would like to have the "Add" button right here in the form:
And whenever this button is clicked, I'd like some simple interface to be displayed, e.g. a text box that would allow me to enter the name of the country + OK and Cancel buttons:
Alright. So let's implement this.
UI
In terms of UI, everything is very similar to the previous example, just instead of editMode, there's addMode.
SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
Templates: {
Fields: {
"Country": {
NewForm: function(ctx) {
var fieldType = ctx.CurrentFieldSchema.FieldType;
var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
return '<div data-bind="visible: !addMode()">'
+ '<table><tr><td>'
+ defaultTemplates[fieldType]["NewForm"](ctx)
+ '</td><td>'
+ '<button data-bind="click: switchToAddMode">Add</button>'
+ '</td></tr></table>'
+ '</div>'
+ '<div data-bind="visible: addMode()">'
+ '<input type="text" />'
+ '<button>OK</button>'
+ '<button>Cancel</button>'
+ '</div>';
},
}
},
},
OnPostRender: function(ctx) {
if (ctx.ListSchema.Field[0].Name == "Liked")
{
var model = {
addMode: ko.observable(false),
switchToAddMode: function() {
model.addMode(true);
}
};
ko.applyBindings(model);
}
}
});
As you can see, it is 90% same code as in previous example.
I added <table> tag because default template for Lookup field renders excessive <br/>, and using table and two td's seems to be the most legitimate way to keep "Add" button on same line with the dropdown.
Now that we have basic UI created, let's make OK and Cancel buttons work.
Adding item to lookup
When OK button is clicked, 3 main things should happen:
- New country should be added to the "Countries" list
- New country should also appear in the dropdown (because we don't want to refresh the whole page!)
- The field should go back to initial mode
Adding item to list is straightforward and can be done e.g. with a small piece of JSOM:
var context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle("Country");
var item = list.addItem(new SP.ListItemCreationInformation());
item.set_item("Title", model.countryName());
item.update();
context.executeQueryAsync(
function() {
alert("item added");
},
function() {
alert("error");
});
Here model.countryName should be a KnockoutJs observable that is bound to the text box value, so it contains whatever user has entered in the field.
Now after item is added, we should also add new country to the dropdown, but unfortunately, there's no existing API or supported way to do that. Default templates are just like black boxes, so the options we have are either to reimplement the whole field (which is the right way, but involves rather big amount of code), or to hack the dropdown via DOM.
Today, to keep the example simple, I'll be doing second approach, but please remember that any DOM hacking is essentially a bad thing to do and after next update (and updates happen pretty often in O365) it may just suddenly stop working and that's it...
Via DOM, adding an element into a dropdown is a primitive task. First, let's have a look at the dropdown element source:
Value is obviously equal to item ID. Now it's easy to create the appropriate code:
var context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle("Countries");
var item = list.addItem(new SP.ListItemCreationInformation());
item.set_item("Title", model.countryName());
item.update();
context.load(item,"ID");
context.executeQueryAsync(
function() {
var option = document.createElement("option");
option.value = item.get_id();
option.innerHTML = model.countryName();
var select = document.querySelector("#countryTD select");
select.appendChild(option);
model.editMode(false);
model.countryName('');
},
function() {
alert("error");
});
Notice few things:
- I added
context.load
call to ensure that ID of the created item is returned - Two lines of code in the end of the success callback switch field back to initial mode and clear the country name textbox. These two lines, obviously, can be used in Cancel button handler as well.
document.querySelector
does same selector work as jQuery, and it's supported natively since IE8, so I use it all the time, but if you have jQuery deployed on page, then of course you can use jQuery for same purpose. #countryTD
refers to id that I added to the <td> element that holds field's default template.
And that's it! Now if you correctly added data-bind attributes, everything should work!
The full code:
SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
Templates: {
Fields: {
"Country": {
NewForm: function(ctx) {
var fieldType = ctx.CurrentFieldSchema.FieldType;
var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
return '<div data-bind="visible: !addMode()">'
+ '<table><tr><td id='countryTD'>'
+ defaultTemplates[fieldType]["NewForm"](ctx)
+ '</td><td>'
+ '<button data-bind="click: switchToAddMode">Add</button>'
+ '</td></tr></table>'
+ '</div>'
+ '<div data-bind="visible: addMode()">'
+ '<input type="text" data-bind="value: countryName" />'
+ '<button data-bind="click: add">OK</button>'
+ '<button data-bind="click: cancel">Cancel</button>'
+ '</div>';
},
}
},
},
OnPostRender: function(ctx) {
if (ctx.ListSchema.Field[0].Name == "Liked")
{
var model = {
addMode: ko.observable(false),
switchToAddMode: function() {
model.addMode(true);
},
countryName: ko.observable(''),
add: function() {
var context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle("Countries");
var item = list.addItem(new SP.ListItemCreationInformation());
item.set_item("Title", model.countryName());
item.update();
context.load(item,"ID");
context.executeQueryAsync(
function() {
var option = document.createElement("option");
option.value = item.get_id();
option.innerHTML = model.countryName();
var select = document.querySelector("#countryTD select");
select.appendChild(option);
model.editMode(false);
model.countryName('');
},
function() {
alert("error");
});
},
cancel: function() {
model.editMode(false);
model.countryName('');
}
};
ko.applyBindings(model);
}
}
});
And here's what you get as a result:
And yes, of course it adds the corresponding item to the Countries list.
Example 3: Dependent fields
Obviously it's possible to control more than one field using same KnockoutJs model. Let's do it and make fields dependent from each other.
So in my example form, I have Visited and Liked fields. Former marks that the city was visited, and in the latter I can select what I liked there. Logically, I shouldn't be able to see "Liked" field until I visited the city.
To implement this logic, I need to override both fields: Liked and Visited. For "Liked" field, customization is very simple: just wrap the field control into a div and bind visibility of this field to visited
field in model (which I will create in a moment):
return '<div data-bind="visible: visited()">' + defaultTemplates[fieldType]["EditForm"] + '</div>';
Here visited
is an observable that should be initialized with the value of the Visited field. It's pretty simple to do it:
var model = {
visited: ko.observable(ctx.ListData.Items[0]["Visited"] == 1)
}
Now let's deal with "Visited" field.
Actually, there's nothing wrong with how the field is displayed, and the only thing that we should do is to track changes of this field in real-time (e.g. user cleared the checkbox => Liked field dissapeared instantly). Unfortunately, again, default field templates are like black boxes, and there's no API that would allow to subscribe to changes of a field. And again, the most correct approach to this would be to re-implement the field, although DOM hack or even a string replace will work.
And this time, let's stick with the right way of doing things and reimplement this field. In this particular case, it's actually very easy to do:
"Visited": {
EditForm: function(ctx) {
ctx.FormContext.registerGetValueCallback(ctx.CurrentFieldSchema.Name, function() {
return model.visited();
});
return '<input data-bind="checked: visited" type="checkbox" />';
}
}
In this case, I'm only providing GetValueCallback, although in perfect case, it's recommended that you also use registerInitCallback, registerHasValueChangedCallback, registerFocusCallback and registerValidationErrorCallback.
But anyway, if you ever created field templates, you certainly noticed, how nicely KnockoutJs fits here and how it can significantly simplify field template creation.
To finish this example off, I moved the code around a bit to make the model variable visible inside the template function, but that's it, it's done, and the fields are now linked together:
Full code for this example:
var model = {
visited: function(){}
};
SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
Templates: {
Fields: {
"Visited": {
EditForm: function(ctx) {
ctx.FormContext.registerGetValueCallback(ctx.CurrentFieldSchema.Name, function() {
return model.visited();
});
return '<input type="checkbox" data-bind="checked: visited" />';
}
},
"Liked": {
EditForm: function(ctx) {
var fieldType = ctx.CurrentFieldSchema.FieldType;
var defaultTemplates = SPClientTemplates._defaultTemplates.Fields.default.all.all;
return '<div data-bind="visible: visited()">' + defaultTemplates[fieldType]["EditForm"](ctx) + '</div>';
}
}
},
},
OnPostRender: function(ctx) {
if (ctx.ListSchema.Field[0].Name == "Liked")
{
model.visited = ko.observable(ctx.ListData.Items[0]["Visited"] == 1);
ko.applyBindings(model);
}
},
});
The source code
As usually, you can browse source code of the examples using Browse code link in the left panel or download it as zip archive using the following link:
Some tips
How to separate HTML markup from code
There's at least one obvious issue with the examples above: HTML markup is managed in JS strings and not in separate files. For small examples it's OK, but for bigger ones it is not that good.
One simple solution for that would be to create KO templates in separate files in SharePoint and then pull them to the page using either Content Editor Web Part, or some other approach.
Alternatively, consider KO components and their external loaders, particularly if you're building many universal field controls and reuse them in different forms.
Why not AngularJs or some other framework?
KnockoutJs is just an example, but of course many modern JavaScript frameworks and libraries can fit its role here. Regarding AngularJs in particular though, for me it's like using a steam-hammer to crack nuts... :)
Why not completely custom form + JSOM?
Good question. In some cases, completely custom form is actually a good idea and makes perfect sense.
However, I think CSR is much better if:
- You only need to customize a couple of fields and leave all other untouched
- You have some fields that are not that easy to re-implement in your form (Taxonomy, User, etc.)
- You cannot guarantee that users will not change settings of fields or add additional fields to this particular list
- You customize fields that are reused in many different lists
How to develop CSR customizations faster
Some time ago I built tool for creating CSR customizations faster. I mean, much faster. Initially it was just a tool that I wrote for myself as I work with CSR a lot. Now it's opensource, and you can use it if you choose so.
The tool is called Cisar and essentially it is a Live Edit for CSR. Meaning, you write code and instantly see how your form or list view transforms according to what you write. There's no delay, and no page reload. Here's how it works:
Conclusion
List forms in SharePoint 2013 are rendered on client side, and that allows us to leverage power of modern frameworks like KnockoutJs to make form customizations simple, readable and more efficient.
Possibilities for reusing default CSR field templates are rather limited, but having power of KnockoutJs, even reimplementing those templates turns out to be relatively easy task.
By using CSR and KnockoutJs, it's possible to create flexible, readable and maintainable solutions that will be fully compatible with SharePoint, which is especially important in SharePoint Online, where updates happen all the time, and using DOM hacks is a very risky approach.