In many scenarios we need to display page children as a list. Usually the list is not just a static HTML. It has to support additional features like sorting, filtering or switching between different item layouts.
To implement this we could use several techniques for example server side markup rendering. But calling server method to sort the collection is not always the best solution. Also implementing simple JavaScript functions for all client-side features could be difficult to maintain.
The elegant and efficient way of implementing advanced list could be using one of the client-side MVVM framework. There are many libraries like Angular, React, Knockout and others. Usually the problem is that most of the markup for the list will be rendered through Javascript. The robots could have problems when crawling the content. Duplicating all functionality for disabled JavaScript scenario is also problematic, because we have to support two parallel code versions.
I’ll try to show how to implement SEO friendly list using KnockouJs library.
In this post I will describe how to implement the list of child articles with sorting by title functionality.
The Knockout ArticleListViewModel will has observable array of cards. Each card represents the child article with title, description, image and URL properties.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var ArticleListViewModel = function () { this.cards = ko.observableArray(); var compare = function(left, right) { return left == right ? 0 : (left < right ? -1 : 1); } this.orderByAscending = function() { this.cards.sort(function (left, right) { return compare(left.title(), right.title()); }); }; this.orderByDescending = function() { this.cards.sort(function (left, right) { return compare(right.title(), left.title()); }); }; this.createCard = function() { return new ArticleCardViewModel(); }; }; var ArticleCardViewModel = function () { this.title = ko.observable(); this.description = ko.observable(); this.imageUrl = ko.observable(); this.url = ko.observable(); }; |
With this viewmodel we could initialize Knockout on a specific node in DOM.
1 2 3 4 5 6 |
<script type="text/javascript"> $(document).ready(function() { var articleListViewModel = new ArticleListViewModel(); ko.applyBindings(articleListViewModel, $('#main-content')[0]); }); </script> |
By default Knockout use foreach binding to render array of items. To render the list of cards from the image above we could use:
1 2 3 4 5 6 7 |
<ul class="cards-list" data-bind="foreach: cards"> <li> <h3><a data-bind="text: title, attr: { href: url }"></a></h3> <img data-bind="attr: { src:imageUrl }" /> <p data-bind="text: description"></p> </li> </ul> |
After running the application, end user should see the list of articles. But if we view page source then there won’t be any articles included, just the knockout template.
Fortunately there is great knockout-pre-rendered extension for prerendering content. The library introduce new foreachInit and init bindings which are used on initial binding.
Now we will render cards list directly in MVC view using Razor foreach statement. Initially the model observableArray will be populated from View. It means that we don’t need to render collection of cards as serialized array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<div id="main-content"> <a data-bind="click: orderByAscending">Order ascendig</a> <a data-bind="click: orderByDescending">Order descending</a> <ul class="cards-list" data-bind="foreachInit: { name: 'cardTemplate', data: cards, createElement: createCard }"> @foreach (var card in this.Model.CurrentPage.GetCards()) { <li> <h3><a href="@card.Url" data-bind="init: { title: '@card.Title', url: '@card.Url' }, text: title, attr: { href: url }">@card.Title</a></h3> <img data-bind="init, attr: { src:imageUrl }" src="@card.ImageUrl"/> <p data-bind="init, text: description">@card.Description</p> </li> } </ul> </div> |
The last missing element is the item template. It will be used during sorting, when the list is rendered by Knockout library.
1 2 3 4 5 6 7 |
<script type="text/ko-template" id="cardTemplate"> <li> <h3><a href="#" data-bind="text: title"></a></h3> <img data-bind="attr: { src:imageUrl }" /> <p data-bind="text: description"></p> </li> </script> |
Now after running the application we can see that content is available without executing JavaScript.