user1470994
user1470994

Reputation: 311

KnockoutJs Updating Counters post Filter

I have a number of filters:

FilterMessageTime = Filter on message time
FilterCommentTime = Filter on comment time
FilterCommentStatus = Filter on comment status
FilterExcludeWithoutComments = Filter messages without comments

And with these said filters (and their various combinations), I would like to keep a constant count of the messages filteredMessagesTimeCount and comments filteredCommentsTimeCount.

I have the following view:

            <div style="display: inline-block">
                <label class="LabelDropdownPopup" for="FilterMessageTime" data-bind="visible: selectedFilterMessageTime">Message Time: </label>
                <select class="Filters" id="FilterMessageTime" title='Message Time' data-max-options="1"
                        data-bind="
                        options: FilterMessageTime,
                        optionsText : 'name',
                        optionsValue: 'name',
                        value: selectedFilterMessageTime
                        "></select>
            </div>
            <br />
            <div data-bind="visible: $root.filteredMessagesTimeCount() > 0">
                <div style="display: inline-block">
                    <label class="LabelDropdownPopup" for="FilterCommentTime" data-bind="visible: selectedFilterCommentTime">Comment Time: </label>
                    <select class="Filters" id="FilterCommentTime" title='Comment Time' data-max-options="1"
                            data-bind="
                        options: FilterCommentTime,
                        optionsText : 'name',
                        optionsValue : 'name',
                        value: selectedFilterCommentTime
                        "></select>
                </div>
                <br />
                <div style="display: inline-block">
                    <label class="LabelDropdownPopup" for="FilterCommentStatus" data-bind="visible: selectedFilterCommentStatus">Comment Status: </label>
                    <select class="Filters" id="FilterCommentStatus" title='Status' data-max-options="1"
                            data-bind="
                        options: FilterCommentStatus,
                        optionsText : 'name',
                        optionsValue : 'name',
                        value: selectedFilterCommentStatus"></select>
                </div>
                <br />
            </div>
                <div style="display: inline-block">
                    <label class="LabelDropdownPopup" for="FilterExcludeWithoutComments" data-bind="visible: selectedFilterExcludeWithoutComments">Only show messages with comments: </label>
                    <select class="Filters" id="FilterExcludeWithoutComments" title='Only show messages with comments' data-max-options="1"
                            data-bind="
                        options: FilterExcludeWithoutComments,
                        optionsText : 'name',
                        optionsValue : 'name',
                        value: selectedFilterExcludeWithoutComments"></select>
                </div>
                <br />
        </div>

        <div id="allMessages" data-bind="foreach: filteredMessagesTime, updateCounters: filteredMessagesTime">
            <div class="messageHolder" data-bind="visible: $root.showAllComments(MessageComments), afterRender: $root.updateMCCounters">
                <div class="messageSection">
                    /*Message...*/
                </div>
                <div class="commentSection">
                    <ul class="ulIterator" data-bind="foreach: $root.filteredCommentsTime(MessageComments), updateCounters: $root.filteredCommentsTime(MessageComments)">
                            /*Comments...*/
                    </ul>
                </div>
            </div>
        </div>
 <div >
            <p>
                <!-- ko if: filteredMessagesTimeCount() == 1 -->
                Total <span class="h4" data-bind="text: filteredMessagesTimeCount"></span> message
                <!-- /ko -->
                <!-- ko if: filteredMessagesTimeCount() > 1 -->
                Total <span class="h4" data-bind="text: filteredMessagesTimeCount"></span> messages
                <!-- /ko -->
                <!-- ko ifnot: filteredMessagesTimeCount -->
                No messages
                <!-- /ko -->
                <!-- ko ifnot: selectedFilterMessageTime() === 'Any' -->
                in the <span class="h4" data-bind="text: selectedFilterMessageTimeOption().name"></span>.
                <!-- /ko -->
                <br />
                <!-- ko if: filteredCommentsTimeCount() == 1 -->
                Total <span class="h4" data-bind="text: filteredCommentsTimeCount"></span> comment
                <!-- /ko -->
                <!-- ko if: filteredCommentsTimeCount() > 1 -->
                Total <span class="h4" data-bind="text: filteredCommentsTimeCount"></span> comments
                <!-- /ko -->
                <!-- ko ifnot: filteredCommentsTimeCount -->
                No comments
                <!-- /ko -->
                <!-- ko ifnot: selectedFilterCommentTime() === 'Any' -->
                in the <span class="h4" data-bind="text: selectedFilterCommentTimeOption().name"></span>.
                <!-- /ko -->
            </p>
        </div>

Accompanied by the following JS:

    self.FilterMessageTime = [
    { name: 'Any',        include: /./, exclude: null },
    { name: 'Last Hour',  include: /minutes?|hour/i,  exclude: /hours|days?|weeks?|months?/i},
    { name: 'Last Day',   include: /minutes?|hours?|day/i,   exclude: /days|weeks?|months?/i},
    { name: 'Last Week', include: /minutes?|hours?|days?|week/i, exclude: /and|weeks?|months?/i },
    { name: 'Last Month', include: /minutes?|hours?|days?|weeks?|month/i, exclude: /months/i}
];
self.FilterCommentTime = [
    { name: 'Any',       include: /./, exclude: null },
    { name: 'Last Hour', include: /minutes?|hour/i, exclude: /hours|days?|weeks?|months?/i },
    { name: 'Last Day',  include: /minutes?|hours?|day/i, exclude: /days|weeks?|months?/i },
    { name: 'Last Week', include: /minutes?|hours?|days?|week/i, exclude: /and|weeks?|months?/i },
    { name: 'Last Month',include: /minutes?|hours?|days?|weeks?|month/i, exclude: /months/i }
];
self.FilterCommentStatus = [
    { id: 4, name: 'Any' },
    { id: 2, name: 'Read' },
    { id: 3, name: 'Unread' }
];
self.FilterExcludeWithoutComments = [
    { id: 1, name: 'No' },
    { id: 2, name: 'Yes' },
];

self.selectedFilterMessageTime = ko.observable(self.FilterMessageTime[0]);
self.selectedFilterMessageTimeOption = ko.computed(function () {
    return ko.utils.arrayFirst(self.FilterMessageTime, function (item) {
        return item.name === self.selectedFilterMessageTime();
    });
});
self.selectedFilterCommentTime = ko.observable(self.FilterCommentTime[0]);
self.selectedFilterCommentTimeOption = ko.computed(function () {
    return ko.utils.arrayFirst(self.FilterCommentTime, function (item) {
        return item.name === self.selectedFilterCommentTime();
    });
});
self.selectedFilterCommentStatus = ko.observable(self.FilterCommentStatus[0]);
self.selectedFilterExcludeWithoutComments = ko.observable(self.FilterCommentStatus[0]);

    self.filteredMessagesTimeCount = ko.observable('0');
self.filteredCommentsTimeCount = ko.observable('0');
ko.bindingHandlers.updateCounters = {
    update: function (element, valueAccessor) {
        ko.utils.unwrapObservable(valueAccessor());
        self.updateMCCounters();
    }
}

self.updateMCCounters = function () {
    //Messages count
    self.filteredMessagesTimeCount($('.messageSection:visible').length);
    //Comments count
    self.filteredCommentsTimeCount($('.commentHolder:visible').length);
}


self.filteredMessagesTime = ko.pureComputed(function () {
    return self.filterMessageTime(self.selectedFilterMessageTimeOption());
});
self.filterMessageTime = function (filter) {
    var filterToReturn = ko.utils.arrayFilter(self.allMessages(), function (message) {
        var d = message.MessageDate;
        return filter.include && filter.include.test(d) &&
               !(filter.exclude && filter.exclude.test(d));
    });

    return filterToReturn;
};

self.filteredCommentsTime = function (MessageComments) {
    return self.filterCommentTime(self.selectedFilterCommentTimeOption(), MessageComments);
};
self.filterCommentTime = function (filter, MessageComments) {
    var filterToReturn = ko.utils.arrayFilter(MessageComments(), function (comment) {
        var d = comment.CommentDate;
           return filter.include && filter.include.test(d) &&
                   !(filter.exclude && filter.exclude.test(d));
    });

    return filterToReturn;
};

self.filterCommentStatus = function (CommentReadAgent) {
    if (self.selectedFilterCommentStatus() == null) {

        return true;
    }
    else if (self.selectedFilterCommentStatus() == 'Any') {
        $('.publishComment').fadeIn("slow");
        $('.commentHolder').fadeIn("slow")

        return true;
    }
    else if (self.selectedFilterCommentStatus()) {
        if (self.selectedFilterCommentStatus() == 'Read') {
            if (CommentReadAgent() == true) {
                $('.publishComment').fadeIn("slow");
                $('.commentHolder').fadeIn("slow");

                return true;
            }
            else

                return false;
        }
        else if (self.selectedFilterCommentStatus() == 'Unread') {
            if (CommentReadAgent() == false) {
                $('.publishComment').fadeIn("slow");
                $('.commentHolder').fadeIn("slow");

                return true;
            }
            else

                return false;
        }
    }
    return false;
};

self.showAllComments = function (MessageComments) {
    if (self.selectedFilterExcludeWithoutComments() == 'Yes') {
        if (self.filteredCommentsTime(MessageComments).length > 0) {
            return true;
        }
        else {
            return false;
        }
    }
    else {
        return true;
    }
};

Now, everything on the filtering and displaying side of things works great. Also when I apply either the FilterMessageTime or FilterCommentTime filters I receive a correct message and comment count.

The problem is: When I apply the FilterExcludeWithoutComments filter, I receive an inconsistent result. I attempt to explain... the messages filteredMessagesTimeCount and comments filteredCommentsTimeCount counters will display the previously selected result fine but the current is wrong. This means if I have 4 messages and 2 comments showing, filteredMessagesTimeCount will show 6 (I made this up) and filteredCommentsTimeCount will show 4 (I made this up)...during the NEXT filter iteration i.e. I toggle the filter again, I will then get the correct result for the previous selection i.e. filteredMessagesTimeCount = 4 and filteredCommentsTimeCount = 2.

Due to my counter being a 'dumb' CSS element counter, the state of execution is terribly important...so what I believe is happeneing is that the counter is firing before the elements have changed (been hidden).

How would I get the counter to fire after the elements have been hidden? is this completely the wrong way of doing things?

On the latter, I know I can use a pureComputed to return the filteredMessagesTimeCount, i.e.:

    self.filteredMessagesCount = ko.pureComputed(function () {
    return self.filteredMessages().length;
    });

Can I do something similar for the other three filters?

If you have made it this far I thank you, and any feedback is welcome.

:)

**Update, example of my .js setup

//Model

function Message(data) {
     var self = this;
     data = data || {};
     self.MessageComments = ko.observableArray([]);
     if (data.MessageComments) {
        var mappedComments = $.map(data.MessageComments, function (item) {     return new Comment(item); });
        self.MessageComments(mappedComments);
      }}

function viewModel() {
     var self = this;
     //As per my original post, the JS 'section' 
     //goes here with the addition of how my messages are loaded updated below
self.loadMessages = function () {
    var token = $("input[name='__RequestVerificationToken']").val();
    var headers = {};
    headers['__RequestVerificationToken'] = token;
    return $.ajax({
        url: messageUrl,
        dataType: "json",
        contentType: "application/json",
        cache: false,
        type: 'GET',
        headers: headers,
        async: false,
        })
        .done(function (data) {
            var mappedMessages = $.map(data, function (dataItem) {
                return new Message(dataItem);
            });
            self.messages(mappedMessages);
        })
        .fail(function () {
            self.error('unable to load messages');
        });
}}

Update

function Message(data, commentFilterTimeDelegate) {
var self = this;
data = data || {};
self.MessageComments = ko.observableArray([]);


self.filteredCommentsTime = ko.computed(function () {
    var filter = commentFilterTimeDelegate;
    var filterToReturn = ko.utils.arrayFilter(self.MessageComments(), function (comment) {
        var d = comment.CommentDate;
        return filter.include && filter.include.test(d) &&
                !(filter.exclude && filter.exclude.test(d));
    });
    return filterToReturn;
}); 
if (data.MessageComments) {
    var mappedComments = $.map(data.MessageComments, function (item) { return new Comment(item); });
    self.MessageComments(mappedComments);
}}

//ViewModel Snippet

            .done(function (data) {
            var mappedMessages = $.map(data, function (dataItem) {
                //return new Message(dataItem);
                return new Message(dataItem, self.selectedFilterCommentTime());
            });
            self.messages(mappedMessages);
        })

Update

Razor

                    <div style="display: inline-block">
                    <label class="LabelDropdownPopup" for="FilterCommentTime" data-bind="visible: selectedFilterCommentTime">Comment Time: </label>
                    <select class="Filters" id="FilterCommentTime" title='Comment Time' data-max-options="1"
                            data-bind="
                        options: FilterCommentTime,
                        optionsText : 'name',
                        optionsValue : 'name',
                        value: selectedFilterCommentTime
                        "></select>
                </div>

...

<ul class="ulIterator" data-bind="foreach: filteredCommentsTime">

JS

function Message(data, commentFilterTimeDelegate) {
...
    self.FilterCommentTime = [
    { name: 'Any', include: /./, exclude: null },
    { name: 'Last Hour', include: /minutes?|hour/i, exclude: /hours|days?|weeks?|months?/i },
    { name: 'Last Day', include: /minutes?|hours?|day/i, exclude: /days|weeks?|months?/i },
    { name: 'Last Week', include: /minutes?|hours?|days?|week/i, exclude: /and|weeks?|months?/i },
    { name: 'Last Month', include: /minutes?|hours?|days?|weeks?|month/i, exclude: /months/i }
];

self.filteredCommentsTime = ko.computed(function () {
    var test = commentFilterTimeDelegate();
    var filter = ko.utils.arrayFirst(self.FilterCommentTime, function (item) {
        return item.name === commentFilterTimeDelegate().name;
    });

    var filterToReturn = ko.utils.arrayFilter(self.MessageComments(), function (comment) {
        var d = comment.CommentDate;
        return filter.include && filter.include.test(d) &&
                !(filter.exclude && filter.exclude.test(d));
    });
    return filterToReturn;
});
}

function viewModel() {
...
    self.FilterCommentTime = [
    { name: 'Any', include: /./, exclude: null },
    { name: 'Last Hour', include: /minutes?|hour/i, exclude: /hours|days?|weeks?|months?/i },
    { name: 'Last Day', include: /minutes?|hours?|day/i, exclude: /days|weeks?|months?/i },
    { name: 'Last Week', include: /minutes?|hours?|days?|week/i, exclude: /and|weeks?|months?/i },
    { name: 'Last Month', include: /minutes?|hours?|days?|weeks?|month/i, exclude: /months/i }
];
self.selectedFilterCommentTime = ko.observable(self.FilterCommentTime[0]);
...
            .done(function (data) {
            var mappedMessages = $.map(data, function (dataItem) {
                //return new Message(dataItem);
                return new Message(dataItem, self.selectedFilterCommentTime);
            });
            self.messages(mappedMessages);
        })
...
}

Upvotes: 1

Views: 121

Answers (1)

Brett Green
Brett Green

Reputation: 3755

The filtering of the comments belongs at the message level... the problem is getting the filters down that far so you can use them properly without using global variables or something awful.

For that kind of thing, I usually pass 'delegates' into my object that point to observables at the root level viewModel... This is good OO practice because the nested Message object should know nothing about the container it's held in (resist using global variables here... always tempting with JS).

Once you've got that setup, the count of messages is easy (happens right at the root). The count of total comments is the aggregate counts of all your messages.filteredMessageComments which can be done in a computed at the root by iteration.

Here's a simplified example using only text boxes (i.e. no options/dropdowns) but the same approach should apply.

** FIDDLE: **

http://jsfiddle.net/brettwgreen/mh1qax40/

** HTML: **

Name Filter: <input type="text" data-bind="value: MessageNameFilter">
Comment Filter: <input type="text" data-bind="value: MessageCommentFilter">
<br />
Message Count: <div data-bind="text: FilteredMessageCount" ></div>
Comment Count: <div data-bind="text: FilteredCommentCount" ></div>
<br />
<div data-bind="foreach: FilteredMessages">
    <div data-bind="text: MessageName"></div>
    <div data-bind="foreach: FilteredMessageComments">
        <div data-bind="text: Comment" style="padding-left: 10px;"></div>
    </div>
</div>

JS:

var MessagesData = [
    {
    MessageName: 'Message One',
    MessageComments: [
        {Comment: 'Comment One'},
        {Comment: 'Comment Two'},
        {Comment: 'Comment Three'}
        ]
    },
    {
    MessageName: 'Message Two',
    MessageComments: [
        {Comment: 'Comment One'},
        {Comment: 'Comment Two'},
        {Comment: 'Comment Three'}
        ]
    },
    {
    MessageName: 'Message Three',
    MessageComments: [
        {Comment: 'Comment One'},
        {Comment: 'Comment Two'},
        {Comment: 'Comment Three'}
        ]
    }];


var MessageComment = function(msgComment) {
    var self = this;
    self.Comment = ko.observable(msgComment.Comment);
};

var Message = function(msg, commentFilter) {
    var self = this;
    self.MessageName = ko.observable(msg.MessageName);
    self.MessageComments = ko.observableArray();
    $.each(msg.MessageComments, function(i, m) {
        self.MessageComments.push(new MessageComment(m));
    });
    self.FilteredMessageComments = ko.computed(function() {
        var results = [];
        $.each(self.MessageComments(), function(i, mc){
            // using the injected comment filter function (an observable)
            // filter the comments accordingly
            if (mc.Comment().indexOf(commentFilter()) !== -1){
                results.push(mc);
            }
        });
        return results;
    });
};

var vm = function(messages) {
    var self = this;
    self.MessageNameFilter = ko.observable('');
    self.MessageCommentFilter = ko.observable('');
    self.Messages = ko.observableArray();
    $.each(messages, function(i, m) {
        // inject the comment filter so we can filter comments
        // inside the messages object
        self.Messages.push(new Message(m, self.MessageCommentFilter));
    });
    self.FilteredMessages = ko.computed(function() {
        var results = [];
        $.each(self.Messages(), function(i, m){
            if (m.MessageName().indexOf(self.MessageNameFilter()) !== -1){
                results.push(m);
            }
        });
        return results;
    });
    // This is easy, just count your filtered messages:
    self.FilteredMessageCount = ko.computed(function() {
        return self.FilteredMessages().length;
    });
    // For this one, iterate over the filtered messages and count
    // the filtered comments
    self.FilteredCommentCount = ko.computed(function() {
        var val = 0;
        $.each(self.FilteredMessages(), function(i, m){
            val += m.FilteredMessageComments().length;
        });
        return val;
    });

};

var vm = new vm(MessagesData);
ko.applyBindings(vm);

Upvotes: 1

Related Questions