Linq-подобный синтаксис для knockout
Прошел год с тех пор, как наша команда разрабатывает web portal используя паттерн MVVM и фреймворк Knockout в частности. Понемногу копился опыт, появлялись различные решения, хорошие и плохие практики, и вот, так сказать, назрело. Для linq-синтаксиса в javascript уже существует библиотека linq.js, и долгое время мы думали, затянуть ли ее к нам в проект. И даже примеры использования вкупе с knockout в интернетах есть.Идея же, которая меня постигла, была в том, чтобы создание computed инкапсулировать внутрь Linq-методов.Для сравнения, код из fiddle: this.filteredItems = ko.computed (function () { var term = this.searchTerm (); return this.items.where (function (item) { return item.name.indexOf (term) > -1; }); }, this); и код, который хотелось бы писать вместо этого: this.filteredItems = this.items .Where (function (item) { return item.name.indexOf (this.searchTerm ()) > -1; }); После вышеобозначенного инкапсулирования computed, оказалось, что библиотека linq.js не особо то и нужна. Достаточно средств встроенных в knockout. Тем более, что их написать нужно всего один раз, и снаружи не будет никакой разницы, даже если это будет самый прямой и простой цикл for.Итак, сначала подготавливаем объект с методами:
var methods = { First: function (predicate) { return ko.computed (function () { return ko.utils.arrayFirst (this (), predicate); }, this, { deferEvaluation: true }); },
Select: function (func) { return ko.computed (function () { return ko.utils.arrayMap (this (), function (item) { return ko.utils.unwrapObservable (func (item)); }); }, this, { deferEvaluation: true }); },
SelectMany: function (func) { return ko.computed (function () { var result = []; ko.utils.arrayForEach (this (), function (item) { result = result.concat (ko.utils.unwrapObservable (func (item))); }); return result; }, this, { deferEvaluation: true }); },
Where: function (predicate) { return ko.computed (function () { return ko.utils.arrayFilter (this (), predicate); }, this, { deferEvaluation: true }); },
Distinct: function (func) { if (! func) { return this.DistinctValue (); } return ko.computed (function () { var obj = {}; return ko.utils.arrayFilter (this (), function (item) { var val = ko.utils.unwrapObservable (func (item)); return obj[val] ? false: (obj[val] = true); }); }, this, { deferEvaluation: true }); },
DistinctValue: function () { return ko.computed (function () { var obj = {}; return ko.utils.arrayFilter (this (), function (val) { return obj[val] ? false: (obj[val] = true); }); }, this, { deferEvaluation: true }); },
Sum: function (func) { return func? this.Select (func).Sum () : this.SumValue (); },
SumValue: function () { return ko.computed (function () { var result = 0; ko.utils.arrayForEach (this (), function (item) { result = result + (+item); }); return result; }, this, { deferEvaluation: true }); },
StringJoin: function (joinString) { joinString = joinString || ', '; return ko.computed (function () { return this ().join (joinString); }, this, { deferEvaluation: true }); }, }; Вторым действием навешиваем методы на obsrvableArray и на computed: for (var i in methods) { ko.observableArray.fn[i] = methods[i]; ko.computed.fn[i] = methods[i]; } Блюдо готово, пользуемся. Примеры: self.DistinctEntities = policy.Coverages .SelectMany (function (item) { return item.Entities; }) .Distinct (function (item) { return item.Name; }); self.EmployeeCount = policy.CoveredTotalCurrentYear .Sum (function (item) { return item.Quantity; }); self.LineOfCoverageColumnName = policy.Coverages .Select (function (item) { return item.LineOfCoverage.ShortDisplayName; }) .StringJoin (); И на закуску, метод Map, аналогичный методу Select, но для сложных/затратных операций, в частности, когда на каждую модель данных во входном массиве нужно создать вью-модель в выходном. При добавлении элемента во входной массив, операция Select перевызовет «лямбду» для всех элементов массива, операция Map же сделает это только для вновь добавленного элемента:
Map: function (converter) { var oldValues = []; var oldResults = [];
return ko.computed (function () { var values = this ().slice (); var results = []; ko.utils.arrayForEach (values, function (item) { var index = oldValues.indexOf (item); results.push (index > -1? oldResults[index] : converter (item)); }); oldValues = values; oldResults = results; return results; }, this, { deferEvaluation: true }); }, Использование: self.Coverages = policy.Coverages.Map (function (coverage) { return new coverageViewModel (coverage); }); PS список методов пока не покрывает все множество LinQ, но расширить его несложно.