Click here to Skip to main content
15,867,835 members
Articles / Productivity Apps and Services / Sharepoint / SharePoint 2013

SharePoint 2013 Client Side Rendering: List Forms + KnockoutJs

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
21 Mar 2015CPOL12 min read 114.6K   353   14   49
Three examples of using CSR with KnockoutJs for customizing SharePoint 2013 list forms.

Introduction

In this article, I demonstrate three real-world examples of list form customizations using KnockoutJs and SharePoint Client Side Rendering:

  1. Read-only field that can be switched to edit mode
  2. Inline add values to a lookup field
  3. 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:

  1. SharePoint 2013 Client Side Rendering: List Views
  2. 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:

Image 1

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:

Image 2

In terms of implementation, this means following those 4 steps:

  1. Override the field template
  2. Create read-only mode for the field, which will be shown by default and contains field control in display mode + "Edit button".
  3. Create edit mode for this field that contains field control in edit mode.
  4. 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:

JavaScript
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:

Image 3

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:

JavaScript
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:

JavaScript
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:

Image 4

Step 3: Create edit mode

Having the knowledge of how to reuse templates, creating edit mode is a primitive exercise:

JavaScript
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:

Image 5

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:

  1. Deploy KnockoutJs to the page
  2. Tweak our HTML a little and add data-bind attributes
  3. 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:

JavaScript
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:

JavaScript
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:

JavaScript
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") // this is the last field on the form
    {
        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:

JavaScript
SP.SOD.executeFunc("clienttemplates.js", "SPClientTemplates", function() {


  function init() {

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
        
        // overrides go here
        
    });
  }

  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:

Image 6

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:

Image 7

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:

Image 8

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.

JavaScript
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:

  1. New country should be added to the "Countries" list
  2. New country should also appear in the dropdown (because we don't want to refresh the whole page!)
  3. 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:

JavaScript
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:

Image 9

Value is obviously equal to item ID. Now it's easy to create the appropriate code:

JavaScript
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() {
      
      // add new country to dropdown
      var option = document.createElement("option");
      option.value = item.get_id();
      option.innerHTML = model.countryName();
    
      var select = document.querySelector("#countryTD select");
      select.appendChild(option);
      
      // turn off the edit mode and clear input
      model.editMode(false);
      model.countryName('');
    },
    function() {
      alert("error");
    });

Notice few things:

  1. I added context.load call to ensure that ID of the created item is returned
  2. 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.
  3. 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.
  4. #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:

JavaScript
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") // last field of the form
    {
      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() {

                  // add new country to dropdown
                  var option = document.createElement("option");
                  option.value = item.get_id();
                  option.innerHTML = model.countryName();

                  var select = document.querySelector("#countryTD select");
                  select.appendChild(option);

                  // turn off the edit mode and clear input
                  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:

Image 10

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):

JavaScript
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:

JavaScript
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:

JavaScript
"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:

Image 11

Full code for this example:

JavaScript
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:

  1. You only need to customize a couple of fields and leave all other untouched
  2. You have some fields that are not that easy to re-implement in your form (Taxonomy, User, etc.)
  3. You cannot guarantee that users will not change settings of fields or add additional fields to this particular list
  4. 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:

Image 12

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Finland Finland
Full stack developer, enterprise web software. Microsoft MVP, open source person, speaker, online expert.

Comments and Discussions

 
QuestionExample 2 question Pin
Member 1196833617-Apr-17 14:32
Member 1196833617-Apr-17 14:32 
AnswerRe: Example 2 question Pin
Member 1196833623-Apr-17 16:09
Member 1196833623-Apr-17 16:09 
QuestionShould this technique work for a ListView that is presented on a Site Page? Pin
Member 126241187-Jul-16 14:15
Member 126241187-Jul-16 14:15 
AnswerRe: Should this technique work for a ListView that is presented on a Site Page? Pin
Andrei Markeev2-Dec-16 11:54
Andrei Markeev2-Dec-16 11:54 
QuestionExample 3: Dependent fields Pin
Member 1239632616-Mar-16 3:46
Member 1239632616-Mar-16 3:46 
AnswerRe: Example 3: Dependent fields Pin
Andrei Markeev2-Dec-16 12:04
Andrei Markeev2-Dec-16 12:04 
QuestionChild List on Save button Pin
Alejandro Saldarriaga30-Sep-15 4:40
Alejandro Saldarriaga30-Sep-15 4:40 
AnswerRe: Child List on Save button Pin
Andrei Markeev30-Sep-15 5:33
Andrei Markeev30-Sep-15 5:33 
GeneralRe: Child List on Save button Pin
Alejandro Saldarriaga30-Sep-15 5:47
Alejandro Saldarriaga30-Sep-15 5:47 
GeneralRe: Child List on Save button Pin
Andrei Markeev30-Sep-15 9:45
Andrei Markeev30-Sep-15 9:45 
GeneralRe: Child List on Save button Pin
Alejandro Saldarriaga30-Sep-15 9:49
Alejandro Saldarriaga30-Sep-15 9:49 
QuestionCompleted Checkbox Column Pin
Member 1200860424-Sep-15 0:43
Member 1200860424-Sep-15 0:43 
AnswerRe: Completed Checkbox Column Pin
Andrei Markeev29-Sep-15 22:14
Andrei Markeev29-Sep-15 22:14 
Questionshow or hide columns on click of link buttons Pin
Maruthi Kiran18-Sep-15 9:55
Maruthi Kiran18-Sep-15 9:55 
AnswerRe: show or hide columns on click of link buttons Pin
Andrei Markeev19-Sep-15 21:44
Andrei Markeev19-Sep-15 21:44 
GeneralRe: show or hide columns on click of link buttons Pin
Maruthi Kiran20-Sep-15 22:59
Maruthi Kiran20-Sep-15 22:59 
GeneralRe: show or hide columns on click of link buttons Pin
Andrei Markeev21-Sep-15 0:23
Andrei Markeev21-Sep-15 0:23 
Hi, how about something like this? Does it make sense?
HTML
<a data-bind="click: doSomething()">my link</a>
.. and then in the model:
JavaScript
var pageModel={
  ...
  doSomething: function() {
    alert('clicked!');
  }
  ...
}

Also, KnockoutJs has excellent documentation, so if you have any further questions please don't hesitate to check it out: http://knockoutjs.com/documentation/click-binding.html[^]
GeneralRe: show or hide columns on click of link buttons Pin
Maruthi Kiran21-Sep-15 0:44
Maruthi Kiran21-Sep-15 0:44 
GeneralMessage Closed Pin
21-Sep-15 2:41
Maruthi Kiran21-Sep-15 2:41 
GeneralRe: show or hide columns on click of link buttons Pin
Maruthi Kiran21-Sep-15 4:23
Maruthi Kiran21-Sep-15 4:23 
GeneralRe: show or hide columns on click of link buttons Pin
Andrei Markeev21-Sep-15 6:18
Andrei Markeev21-Sep-15 6:18 
GeneralRe: show or hide columns on click of link buttons Pin
Maruthi Kiran21-Sep-15 6:58
Maruthi Kiran21-Sep-15 6:58 
GeneralRe: show or hide columns on click of link buttons Pin
Maruthi Kiran21-Sep-15 7:15
Maruthi Kiran21-Sep-15 7:15 
GeneralRe: show or hide columns on click of link buttons Pin
Andrei Markeev21-Sep-15 7:23
Andrei Markeev21-Sep-15 7:23 
GeneralRe: show or hide columns on click of link buttons Pin
Maruthi Kiran22-Sep-15 23:51
Maruthi Kiran22-Sep-15 23:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.