Linq-like syntax for knockout

    A year has passed since our team developed the web portal using the MVVM pattern and the Knockout framework in particular. Little by little, experience was accumulating, various solutions appeared, good and bad practices, and now, so to speak, it was ripe. The linq.js library already exists for javascript for linq syntax, and for a long time we were thinking whether to pull it into our project. And even examples of use, coupled with knockout on the Internet there .
    The idea that befell me was to create a computed encapsulate inside Linq methods.
    For comparison, the code from fiddle:
        this.filteredItems = ko.computed(function() {
            var term = this.searchTerm();
            return this.items.where(function(item) { 
                return item.name.indexOf(term) > -1; 
            });
        }, this);
    

    and code that I would like to write instead:
        this.filteredItems = this.items
            .Where(function(item) { return item.name.indexOf(this.searchTerm()) > -1; });
    


    After the computed encapsulation mentioned above, it turned out that the linq.js library is not really needed. Enough tools built into knockout. Moreover, they need to be written only once, and there will be no difference outside, even if it is the most direct and simple for loop.

    So, first we prepare the object with the methods:
    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 });
            },
        };
    

    The second action we hook methods on obsrvableArray and computed:
        for (var i in methods) {
            ko.observableArray.fn[i] = methods[i];
            ko.computed.fn[i] = methods[i];
        }
    

    The dish is ready, use it. Examples:
            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();
    


    And for starters, the Map method, similar to the Select method, but for complex / costly operations, in particular, when for each data model in the input array you need to create a view model in the output. When adding an element to the input array, the Select operation will re-call the “lambda” for all elements of the array, the Map operation will only do this for the newly added element:
    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 });
                },
    

    Using:
    self.Coverages = policy.Coverages.Map(function(coverage) { return new coverageViewModel(coverage); });
    


    PS The list of methods does not yet cover the whole set of LinQ, but it is not difficult to expand it.

    Also popular now: