Justin Nafe
Justin Nafe

Reputation: 3052

knockout toggle visibility of nested items

I am trying to toggle visibility of nested items using Knockout. On the initial display of the page, I don't want the nested items to be visible. If the user clicks a link to show the nested items, then I want those nested items to show (not all nested items).

For example, I have a list of products, and for each product I have a list of star ratings. By default or for initial page load, the ratings are not shown. If the user clicks "Ratings", then the ratings for that item are shown. If the user clicks "Ratings" for the other item, then that product's ratings are shown as well as the other one.

To illustrate, I have two products:

Samsung Ratings

iPhone Ratings

If I click iPhone's Ratings, then iPhones ratings are shown:

Samsung Ratings       
iPhone Ratings
    1203: 3
    1204: 2

If I click Samsung's Ratings, then Samsung's ratings are shown also:

Samsung Ratings
    1201: 5
    1202: 4
iPhone Ratings
    1203: 3
    1204: 2

Then if I click the Ratings of either, the ratings for that product becomes non visible.

My sample HTML is as follows:

<ul data-bind="foreach: products">
    <li><span data-bind="text: ProductName"></span>
        <a href="#" data-bind="click: $parent.toggleVisibility">Ratings</a> 
        <ul data-bind="foreach: StarRatings, visible: $parent.shouldShowRatings">
            <li><span data-bind="text: RatingId"></span>: <span data-bind="text: RatingValue"></span></li>
        </ul>
        </li>
</ul>

The sample javascript using Knockout.js 3.0.0 is as follows:

var initialProducts = [{ 
    ProductName: "Samsung", 
    StarRatings: [{ 
        RatingId: 1201, 
        RatingValue: 5
        },
        { 
            RatingId: 1202, 
            RatingValue: 4
        }
    ]
}, {
    ProductName: "iPhone", 
    StarRatings: [{ 
        RatingId: 1203, 
        RatingValue: 3
        },
        { 
            RatingId: 1204, 
            RatingValue: 2
        }
    ]
}];

(function (ko) {
    function products(data) {

        var self = this;
        data = data || {};

        // Persisted properties
        self.ProductId = data.ProductId;       
        self.StarRatings = data.StarRatings;
    }    
})(ko);

var viewModel = (function(ko){
    var products = ko.observableArray(initialProducts),
        showRatings = ko.observableArray(),
        toggleVisibility = function(item) {
            if(showRatings.indexOf(item) < 0){
                showRatings.push(item);
            } 
            else{
                showRatings.remove(item);
            }
        },
        shouldShowRatings = function(item) {
            if( showRatings.indexOf(item) >= 0){
                return true;
            }
            {
                return false;
            }
        };

    return {
        products: products,
        showRatings: showRatings,
        toggleVisibility: toggleVisibility,
        shouldShowRatings: shouldShowRatings
    };
})(ko);

ko.applyBindings(viewModel);

I've been able to show all ratings or show none. I've also been able to set it up to where only one is shown at a time, but I think this code is the closest to the solution I need.

It looks like after the click event, shouldShowRatings for the visibility is not called for the StarRatings visibility.

The sample code is also on jsFiddle at http://jsfiddle.net/justinnafe/866my/4/

Upvotes: 0

Views: 919

Answers (2)

user1375096
user1375096

Reputation:

The better way to fix the initial evaluation is to use standard ko way: http://jsfiddle.net/866my/10/

change

<a href="#" data-bind="click: $parent.toggleVisibility($data)">Ratings</a>

to

<a href="#" data-bind="click: $parent.toggleVisibility">Ratings</a>

now ko gets a function object ( the toggleVisibility function ), it would not call the function until you click it.

ko by default passes current context $data as first parameter to the target function, so explicit call($data) is unnecessary.

Upvotes: 1

ArrayKnight
ArrayKnight

Reputation: 7356

Just update your bindings to call the functions with the $data as the first parameter: http://jsfiddle.net/866my/6/

<ul data-bind="foreach: products">
    <li><span data-bind="text: ProductName"></span>
        <a href="#" data-bind="click: $parent.toggleVisibility($data)">Ratings</a> 
        <ul data-bind="foreach: StarRatings, visible: $parent.shouldShowRatings($data)">
            <li><span data-bind="text: RatingId"></span>: <span data-bind="text: RatingValue"></span></li>
        </ul>
    </li>
</ul>

The important part is that you use the function notation in order to get the binding to check for updates.

Upvotes: 2

Related Questions