...

понедельник, 5 мая 2014 г.

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, но расширить его несложно.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий