⬆️ ⬇️

KnockoutJS: Ajax grid view from scratch to 40 lines

Recently on Habré there are more and more references to KnockoutJS, and I will not stay away from this trend.

Today I will talk about how to do your own Ajax Grid View with filtering and navigating through the pages by writing, with very little code.

Starting to write this article, I felt a little awkward, and even now the feeling did not go away. The thing is that the library itself is simple, the MVVM pattern is simple, and I will tell simple things. I am sure that in the near future Knockout will receive a fairly large spread. And it is embarrassing to me that in a year or so, someone stumbling upon this article will be discouraged by the simplicity of the material presented. Just like any of you are now, who opened the article on jQuery from 2007.



Who was not afraid of the alleged accordion, you are welcome under Habrakat.





')

As expected, let's set ourselves a task that we need to solve.

Imagine yourself as a front-end developer who needs to display a list of people (name, gender, age) and let them search by these parameters. A list of people is displayed page by page. And this thing looks like this:



Interface for viewing information about people



For us, the entire backend has already been created and all interfaces are already known. So I just bring them here.



ActionResult List(FilterParams filterParams, int pageNumber = 1); 


The output is a ListResult object consisting of an array of “search result” and data for switching pages (the number of the current page and how many pages there are).

In the code, it all looks like this:

 public class FilterParams { public int? AgeFrom { get; set; } public int? AgeTo { get; set; } public bool ShowMale { get; set; } public bool ShowFemale { get; set; } } public enum Gender { Male, Female } public class PagingData { public int PageNumber { get; set; } public int TotalPagesCount { get; set; } } public class Person { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public Gender Gender { get; set; } } public class ListResult { IEnumerable<Person> Data { get; set; } PagingData Paging { get; set; } } 




Now we can concentrate on solving our problem. I propose to begin our research with markup. There is nothing new for those already familiar with Knockout, everything should be clear for beginners too.

 <script type="text/html" id="TableRow"> <tr> <td data-bind="text: FirstName"></td> <td data-bind="text: LastName"></td> <td data-bind="text: Gender"></td> <td data-bind="text: Age"></td> </tr> </script> <table> <thead> <tr> <th> First name</th> <th> Last name</th> <th> Gender</th> <th> Age</th> </tr> </thead> <tbody data-bind="template: { name: 'TableRow', foreach: rows }"> </tbody> </table> 




So, we have a template for one record. In the table, we have a static header, and tbody is filled in from the rows array. Now we need to describe the view model, which can fill this table with data. In the beginning I will give the code, and then I will explain it.

 var viewModel = { rows: ko.observableArray() }; ko.dependentObservable(function () { $.ajax({ url: '/AjaxGrid/List', type: 'POST', context: this, success: function (data) { this.rows(data.Data); } }); }, this); ko.applyBindings(viewModel); 




ViewModel contains only one field - rows. At the beginning it is empty. Then we create a dependentObservable that will be executed upon initialization. At run time, it will make an AJAX request to the server, and the Data field values ​​from the response will be assigned to the rows field. KO will track the change in the rows field and fill the table with incoming entries. You can read more about dependentObservable in the official documentation or in this comment .



The next step is to add a page switch. Let's start with the viewModel

 var viewModel = { rows: ko.observableArray(), paging: { PageNumber: ko.observable(1), TotalPagesCount: ko.observable(0), next: function () { var pn = this.PageNumber(); if (pn < this.TotalPagesCount()) { this.PageNumber(pn + 1); } }, back: function () { var pn = this.PageNumber(); if (pn > 1) { this.PageNumber(pn - 1); } } } }; 




In this example, the essence of the ViewModel in the MVVM pattern is very clearly visible. The model consists of two properties PageNumber and TotalPagesCount . And in the view of this model, there are already methods next () and back () . If we need the isFirstPage or isLastPage properties , they will also be declared in the viewModel. Thus, the king (Model) is surrounded by an obliging and changeable suite (ViewModel).



Displaying the page selector is trivial.

 <script type="text/html" id="PagingPanel"> Page <span data-bind="text: PageNumber" /> of <span data-bind="text: TotalPagesCount" />. <br /> <a href="#next" data-bind="click: back"><</a> <a href="#next" data-bind="click: next">></a> </script> <div data-bind="template: { name: 'PagingPanel', data: paging }"></div> 




Thus, we will simply display which page of how many are displayed and the buttons forward and back. It remains for a little, to teach our grid view to update the data when switching pages.



To do this, we need to modify our dependentObservable a bit:

 ko.dependentObservable(function () { $.ajax({ url: '/AjaxGrid/List', type: 'POST', data: {pageNumber: this.paging.PageNumber()} context: this, success: function (data) { this.rows(data.Data); } }); }, this); 




We have added the value of the PageNumber field to an AJAX request. Now Knockout knows that our dependentObservable should be “recalculated” for any change in the PageNumber () property. Thus, when the user presses the button further, the viewModel catches this event (data-bind = "click: next"), and simply increments the PageNumber value by one. After that, KO sees that a PageNumber change has occurred, which means you need to exceed dependentObservable. That, in turn, sends an AJAX request, and the incoming data is put in viewModel.rows, which causes a complete redraw of the table contents.



Now it's time to add filtering. We will use an approach similar to page switching. All search parameters will be observable and their values ​​will be sent when a query is sent. Those. any change in filtering conditions will result in a request to the server.



 var viewModel = { filterParams: { ShowMale: ko.observable(true), ShowFemale: ko.observable(true), AgeFrom: ko.observable(), AgeTo: ko.observable() }, rows: ko.observableArray(), paging: { PageNumber: ko.observable(1), TotalPagesCount: ko.observable(0), next: function () { var pn = this.PageNumber(); if (pn < this.TotalPagesCount()) { this.PageNumber(pn + 1); } }, back: function () { var pn = this.PageNumber(); if (pn > 1) { this.PageNumber(pn - 1); } } } }; 




And the actual view of the filter panel:

 <script type="text/html" id="FiltrationPanel"> Age from <input type="text" size="3" data-bind="value: AgeFrom" /> to <input type="text" size="3" data-bind="value: AgeTo" /> <br /> <label><input type="checkbox" data-bind="checked: ShowMale" />Show male</label> <br /> <label><input type="checkbox" data-bind="checked: ShowFemale" />Show female</label> </script> <div data-bind="template: { name: 'FiltrationPanel', data: filterParams }"></div> 




And we need to support our dependentObservable a bit:

 ko.dependentObservable(function () { var data = ko.utils.unwrapObservable(this.filterParams); // Dependent observable will react only on page number change. data.pageNumber = this.paging.PageNumber(); $.ajax({ url: url, type: 'POST', data: data, context: this, success: function (data) { this.rows(data.Data); this.paging.PageNumber(data.Paging.PageNumber); this.paging.TotalPagesCount(data.Paging.TotalPagesCount); } }); }, this); ko.dependentObservable(function () { var data = ko.toJS(this.filterParams); // Reset page number when any filtration parameters change this.paging.PageNumber(1); }, this); 




A little explanation is needed here. String var data = ko.toJS (this.filterParams); gets a JS object from the filterParams field, and the values ​​of all observables are obtained. Thus, KO will recalculate our dependentObservable if any filter condition is changed. I also added a second dependentObservable, which will reset the current page number to 1 when filtering conditions change. Thus, when changing filters, we must request the first page.



In fact, in a strikethrough paragraph, the decision was made that led to two requests to the server when the filter conditions changed. The first was caused by the change itself, the second - by setting pageNumber to one. To correct the situation, we fixed the line

 var data = ko.toJS(this.filterParams); 


on

 var data = ko.utils.unwrapObservable(this.filterParams); 




In this unwrapObservable will give the same result as the toJS method, except that dependentObservable will be recalculated when filterParams changes.



Thus, only the change of the page number initiates a request to the server, and it can be caused by switching pages or by changing filter conditions.



Also, when we receive a response from the server, we update the values ​​in the paging field in case the number of pages or the current number has changed (for example, we have requested page 10, and there are only 5 of them).



In fact, we have already decided the task we have set ourselves. However, I propose to abstract a bit from it and think. We solved quite a specific task. But our ViewModel almost does not know about the task itself. She knows only about the URL, where to get the data and filtering options. Everything. This means that our code can be made reusable. I will turn our viewModel into a class with url and filtrationParams arguments:



 var AjaxGridViewModel = function(url, filterParams) { this.rows= ko.observableArray(); this.filterParams = filterParams; this.paging = { PageNumber: ko.observable(1), TotalPagesCount: ko.observable(0), next: function () { var pn = this.PageNumber(); if (pn < this.TotalPagesCount()) this.PageNumber(pn + 1); }, back: function () { var pn = this.PageNumber(); if (pn > 1) this.PageNumber(pn - 1); } }; ko.dependentObservable(function () { var data = ko.utils.unwrapObservable(this.filterParams); // Dependent observable will react only on page number change. data.pageNumber = this.paging.PageNumber(); $.ajax({ url: url, type: 'POST', data: data, context: this, success: function (data) { this.rows(data.Data); this.paging.PageNumber(data.Paging.PageNumber); this.paging.TotalPagesCount(data.Paging.TotalPagesCount); } }); }, this); ko.dependentObservable(function () { var data = ko.toJS(this.filterParams); // Reset page number when any filtration parameters change this.paging.PageNumber(1); }, this); }; 




All this code takes exactly 39 lines. If we recall the title, we are left alone to initialize:

 ko.applyBindings(new AjaxGridViewModel('/Ajax/List', { ShowMale: ko.observable(true), ShowFemale: ko.observable(true), AgeFrom: ko.observable(), AgeTo: ko.observable() }); 




As you can see, the whole picture spoils the second argument. Instead of writing an object in one line, let's think about the nature of it. Actually, this is a copy-paste of the FilterParams object described in C #. Its fields are used only in View, and in ViewModel we obviously do not explicitly use them. This allows us to select this class from our ViewModel.



In this example, I used ASP.NET MVC. And I solved this problem very simply:

 C#: public ActionResult Index() { return View(new FilterParams()); } CSHTML: ko.applyBindings(new AjaxGridViewModel('@Url.Action("List")', @Html.ToJSON(Model))) 




That is, I simply pass an instance of the class with default filtering settings to View, and View serializes it into a JS object. Thus, we have simplified the task of supporting the code. There remains only one uncompiled place where this object is used - template FiltrationPanel.



But that's not all. Initially, the filtrationParams field contained observable values. And now we fed him a simple JS object. We need to wrap all the fields of this object in ko.observable (). For this there is a plugin ko.mapping .



We use this plugin in the second line of our class AjaxGridViewModel:



 var AjaxGridViewModel = function(url, filterParams) { this.rows= ko.observableArray(); this.filterParams = ko.mapping.fromJS(filterParams); ... 




This is exactly all.



And now why was it even necessary when there is a jqGrid and others. The bottom line is that these are all heavyweight controls adapted for displaying tables. They have a lot of opportunities, but they are quite narrowly focused. And we created a reusable viewModel and an absolutely lightweight view. We can use tables, lists, anything. However, only the server code and html knows what data is displayed. And that is flexibility. We got a handy tool for displaying data with filtering and leafing pages. It is not enough code and we will fully understand how it works. Great, isn't it?



Thanks to everyone who mastered the article. I hope it was interesting and useful. If you are interested, you can download the source code of the final version of the example on ASP.NET MVC 3 via this link .



Thanks again for your attention. I will be glad to questions and constructive criticism.



UPDATE : fixed a problem with two requests to the server when filtering conditions changed.

Source: https://habr.com/ru/post/125148/



All Articles