Pull to refresh

Linq-подобный синтаксис для knockout

Reading time3 min
Views4.8K
Прошел год с тех пор, как наша команда разрабатывает 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, но расширить его несложно.
Tags:
Hubs:
Total votes 21: ↑17 and ↓4+13
Comments9

Articles