Last Saturday, I had the honor to read a report on MVVM and
KnockoutJS on
.NET Saturday in Dneporpetrovsk .
The report was warmly received by the public and many people had interesting questions.
which were not disclosed during the report itself.
As a matter of fact, I decided to write public answers to some of them on Habré.
Today I will answer the question about template-binding. "What should I do if I need to display not all the records, but only those that fit certain conditions."
The answer is under habrakat.
')
I will not talk about MVVM and KnockoutJS. On Habré you can read
this article , there will also be a video from my report.
So, to begin with, let's set ourselves a task - to show only men in the list of people.
We already have a code that allows you to simply display a list of people and their gender (
see work live )
Viewmodel
var Person = function(gender, name) { this.gender = ko.observable(gender); this.name = ko.observable(name); }; var viewModel = { persons: ko.observableArray([ new Person('M', 'John Smith'), new Person('M', 'Mr. Sanderson'), new Person('F', 'Mrs. Sanderson'), new Person('M', 'Agent Ralf'), new Person('F', 'Gangretta Peterson') ]) }; ko.applyBindings(viewModel);
View
<script type="text/html" id="PersonInfo"> <li> <span data-bind="text: gender"></span> <span data-bind="text: name"></span> </li> </script> <div data-bind=" template: { name: 'PersonInfo', foreach: persons}"></div>
The simplest solution to the problem is to filter the array directly in the binding. This is done by modifying the foreach parameter:
foreach: ko.utils.arrayFilter( persons(), function(p){ return p.gender() == 'M';} )
It will work and will work correctly. If you change the persons collection, the list will be kept up to date. At the same time, the template will not be fully re-rendered. New items will be added, and deleted ones will disappear.
View in work .
So, the code needed to solve our problem below:
Viewmodel
var Person = function(gender, name) { this.gender = ko.observable(gender); this.name = ko.observable(name); }; var viewModel = { persons: ko.observableArray([ new Person('M', 'John Smith'), new Person('M', 'Mr. Sanderson'), new Person('F', 'Mrs. Sanderson'), new Person('M', 'Agent Ralf'), new Person('F', 'Gangretta Peterson') ]), addMale: function() { this.persons.push(new Person('M', 'New male')); }, addFemale: function() { this.persons.push(new Person('F', 'New female')); }, removePerson: function(person) { this.persons.remove(person); } }; ko.applyBindings(viewModel);
and the presentation itself:
View
<script type="text/html" id="PersonInfo"> <li> <span data-bind="text: gender"></span> <span data-bind="text: name"></span> <small data-bind="text: new Date()"></small> <a href="#remove" data-bind="click: function() { viewModel.removePerson($data); }">x</a> </li> </script> <div data-bind=" template: { name: 'PersonInfo', foreach: ko.utils.arrayFilter( persons(), function(p){ return p.gender() == 'M';} )}"></div> <a href="#add-male" data-bind="click: addMale">Add male</a> <a href="#add-male" data-bind="click: addFemale">Add female</a>
Now, if you turn on the thought, we understand what they wrote Kaku. We use MVVM to separate presentation logic and business logic, and at the same time in View we write code for filtering. This is nonsense - ViewModel is responsible for this in MVVM.
So, the correct solution to the problem is to create a field in ViewModel in which only men will be. This field must be kept up to date when the persons field is changed. To do this, KnockoutJS are
Dependent Observables . Let's take a look at the code that adds the males field.
viewModel.males = ko.dependentObservable(function() { return ko.utils.arrayFilter(this.persons(), function(p) { return p.gender() == 'M'; }); }, viewModel);
Dependent observables have one non-obvious feature of work. During the first launch, Knockout “remembers” to which observable objects (observables) were accessed and subscribed to their changes. When a change occurs in any of the related observables, KO will over-execute the function described in dependentObservable.
It is also worth paying attention to the second attribute, it indicates that this will be during the execution of the function to get the value.
Work resultIn fact, I was a goof when I said that KO was looking at which observables we were accessing when we first started. In fact, it does this every time a dependent observable value is calculated. I will give the final version of our demo application, in which we can choose whom to display (males or females), as well as change the gender of a person (oh, forgive me a sinner).
Viewmodel
var Person = function(gender, name) { this.gender = ko.observable(gender); this.name = ko.observable(name); this.changeGender = function() { var g = this.gender() == 'F' ? 'M' : 'F'; this.gender(g); } }; var viewModel = { genderToFilter: ko.observable('M'), persons: ko.observableArray([ new Person('M', 'John Smith'), new Person('M', 'Mr. Sanderson'), new Person('F', 'Mrs. Sanderson'), new Person('M', 'Agent Ralf'), new Person('F', 'Gangretta Peterson') ]), addMale: function() { this.persons.push(new Person('M', 'New male')); }, addFemale: function() { this.persons.push(new Person('F', 'New female')); } }; viewModel.males = ko.dependentObservable(function() { var g = this.genderToFilter(); return ko.utils.arrayFilter(this.persons(), function(p) { return p.gender() == g; }); }, viewModel); ko.applyBindings(viewModel);
From the new note:
- In the Person class, a method has been added for reversing gender;
- In the viewModel, the observable field “whom to display” was added;
- In the filtering method, we compare the gender of a person with the value of the field “whom to display”.
View changes are also quite modest:
<script type="text/html" id="PersonInfo"> <li> <a href="#change" data-bind="text: gender, click: changeGender"></a> <span data-bind="text: name"></span> </li> </script> <table width="100%"> <tr valign="top"> <td width="50%"> <label> <input type="radio" value="M" data-bind="checked: genderToFilter" />Males </label> <label> <input type="radio" value="F" data-bind="checked: genderToFilter" />Femails </label> <div data-bind=" template: { name: 'PersonInfo', foreach: males }"></div> </td> <td> <strong>All</strong> <div data-bind=" template: { name: 'PersonInfo', foreach: persons }"></div> </td> </tr> </table> <a href="#add-male" data-bind="click: addMale">Add male</a> <a href="#add-male" data-bind="click: addFemale">Add female</a>
We added a radio-button with a checked binding, which determines which one will be selected according to the value in the genderToFilter field. This is bidirectional binding, so when you change the selected radio, the changes will come in the viewModel. We were accessing the genderToFilter field during filtering, which means filtering will happen again.
Similar will happen with the change of sex. She participated in the filtering method, meaning the list will be re-filtered when changing the gender of any of the people.
In light of this, my admission of slyness was timely. If KO had not rescanned what observables we called during each recomputation dependentObservable - gender changes to people added at runtime would not lead to re-filtering.
The last paragraphs were a bit confusing, but I hope, all the same understandable.
View the final versionConclusion
In fact, this whole article was devoted to using dependentObservable as a value for the foreach parameter in template binding.
As you can see, dependentObservable is a very powerful thing. Monitors all changes to related objects. In real work, you will encounter similar tasks more than once. For this I highly recommend for myself to understand
why the last example works.
Thank you for reading to the end. I will be glad to questions and constructive criticism.