简体   繁体   中英

n-level deep checkbox tree behavior

I have a n-level checkbox tree implemented using knockout, where selection of a parent level chechbox should select its children (but not the other way around) and once data is submitted the ids of elements selected should be converted to JSON to be sent to the server.

I can't figure out how to the one way checkbox relationship and also can't figure out how to filter my final json in such a way that:

  1. If a parent was selected (as it means all its children are also selected) to not send its child ids in the json

  2. How to not send a parent id when only one or few of children are selected.

The model of the list items is:

marketingListsItem = function (data, parent) {
        var self = this;
        self.Name = ko.observable(data.Name);
        self.Selected = ko.observable(data.Selected);
        self.Parent = ko.observable(parent);
        self.Children = ko.observableArray([]);
        self.Id = ko.observable(data.Id);         
        self.DateAdded = ko.observable(new Date(data.DateAdded));
        self.DateModified = ko.observable(data.DateModified);
        if (data.Children) {
            ko.utils.arrayForEach(data.Children, function (child) {
                self.Children.push(new marketingListsItem(child, this));
            }.bind(this));
        };
    }

Here is the view model section:

marketingListsViewModel = {
        marketingLists: mapping.fromJS([]),
        originatorConnectionName: ko.observable(''),
        selectedMarketingListIds: ko.observableArray([])
    },
    init = function (connectionId, connectionName) {
        marketingListsViewModel.originatorConnectionName(connectionName);
        marketingListsViewModel.getFieldMapping = function () {
            require(['mods/fieldmapping'], function (fieldmapping) {
                fieldmapping.init(connectionId, connectionName);
            });
        };
        // Here I only managed to filter the parent level selections
        marketingListsViewModel.selectedLists = ko.computed(function () {
            return ko.utils.arrayFilter(marketingListsViewModel.marketingLists(), function (item) {
                return item.Selected() == true;
            });
        });
        marketingListsViewModel.saveMarketingListChanges = function () {
            // Which I can filter my JSON to include them but the children are missing
            var latestMarketingListChanges = ko.toJSON(marketingListsViewModel.selectedLists, ["Id"]);
            console.log(latestMarketingListChanges);
            amplify.request("updateExistingMarketingLists", { cid: connectionId, ResponseEntity: { "id": connectionId, "selectedMarketListIds": latestMarketingListChanges } },
                function (data) {
                    console.log(data);
                });
        }
        amplify.request("getExistingMarketingLists", { cid: connectionId }, function (data) {
            showMarketingLists();
            mapping.fromJS(data.ResponseEntity, dataMappingOptions, marketingListsViewModel.marketingLists);
            ko.applyBindings(marketingListsViewModel, $('#marketingLists')[0]);
        });
    };

And finally here is the view:

<div id="marketingListsContainer">
        <ul data-bind="template: {name: 'itemTmpl' , foreach: marketingLists}"></ul>
        <script id="itemTmpl" type="text/html">
            <li>
                <label><input type="checkbox" data-bind="checked: Selected" /><span data-bind='text: Name'></span></label>                    
            <ul data-bind="template: { name: 'itemTmpl', foreach: Children }" class="childList"></ul>
        </script>
    </div>
    <a class="s_button modalClose right" href="#"><span data-bind="click: saveMarketingListChanges">Save and close</span></a><br>

Thanks for the answer. The issue was that with your solution now if a Parent is already checked by data from the server then it's children's checked values do not mutate.

I have added the following to the model to fix this:

self.sync = ko.computed(function () {
       return self.Selected.valueHasMutated();
});

For those who will find this post later and may want to see what the end result was:

    define('mods/marketinglists', ["knockout", "libs/knockout.mapping", "libs/knockout.validation", "datacontext", "mods/campaigner", "text!templates/marketinglists.html", "text!styles/marketinglists.css"],
function (ko, mapping, validation, datacontext, campaigner, html, css) {
    'use strict';
    var
        marketingListsItem = function (data, parent) {
            var self = this;
            self.Name = ko.observable(data.Name);
            self.Selected = ko.observable(data.Selected);
            self.Parent = ko.observable(parent);
            self.Children = ko.observableArray([]);
            self.Id = ko.observable(data.Id);
            self.DateAdded = ko.observable(new Date(data.DateAdded));
            self.DateModified = ko.observable(data.DateModified);
            // If node contains children define each one as a marketingListItem in itself
            // and bind it to the the model
            if (data.Children) {
                ko.utils.arrayForEach(data.Children, function (child) {
                    self.Children.push(new marketingListsItem(child, this));
                }.bind(this));
            };
            // Watch for value changes in parent and check children 
            // if the parent was checked
            self.Selected.subscribe(function (newValue) {
                if (newValue === true) {
                    for (var i = 0; i < self.Children().length; i++) {
                        self.Children()[i].Selected(true);
                    }
                }
                else {
                    if (self.Parent() != null) { self.Parent().Selected(false); }
                }
            });
            // Make sure subscribers have been notified when needed
            self.sync = ko.computed(function () {
                return self.Selected.valueHasMutated();
            });
        },
        dataMappingOptions = {
            key: function (data) {
                return data.Id;
            },
            create: function (options) {
                return new marketingListsItem(options.data, null);
            }
        },
        showMarketingLists = function () {
            campaigner.addStylesToHead(css);
            campaigner.addModalWindow(html, {
                windowSource: "inline",
                width: 700,
                height: '340'
            });
        },
        marketingListsViewModel = {},
        init = function (connectionId, connectionName) {
            // Define marketingLists as an observable array from JS object
            marketingListsViewModel.marketingLists = mapping.fromJS([]);
            marketingListsViewModel.originatorConnectionName = ko.observable('');
            // Set the name for the marketing list
            marketingListsViewModel.originatorConnectionName(connectionName);
            marketingListsViewModel.getFieldMapping = function () {
                require(['mods/fieldmapping'], function (fieldmapping) {
                    fieldmapping.init(connectionId, connectionName);
                });
            };
            marketingListsViewModel.selectedLists = ko.computed(function () {
                var selectedItems = [];
                ko.utils.arrayFilter(
                    marketingListsViewModel.marketingLists(),
                    function (item) {
                        // If a parent a selected its being collected
                        if (item.Selected() == true) selectedItems.push(item);
                        else {
                            // If a child is slected it is collected
                            ko.utils.arrayForEach(item.Children(), function (child) {
                                if (child.Selected()) selectedItems.push(child);
                                else {
                                    ko.utils.arrayForEach(child.Children(),
                                        // Finally if children's child is selected its collected
                                        function (childChildren) {
                                            if (childChildren.Selected())
                                                selectedItems.push(childChildren);
                                        });
                                }
                            })
                        }
                    });
                return selectedItems;
            });
            marketingListsViewModel.saveMarketingListChanges = function () {
                // Pick only the selected elements and parse only the Id
                var latestMarketingListChanges = ko.toJSON
                    (marketingListsViewModel.selectedLists,
                        ["Id"]);
                console.log(latestMarketingListChanges);
                // Send the latest marketing lists changes Ids to the server
                amplify.request("updateExistingMarketingLists",
                    {
                        cid: connectionId,
                        ResponseEntity:
                        {
                            "id": connectionId,
                            "selectedMarketListIds": latestMarketingListChanges
                        }
                    },
                    function (data) {
                        console.log(data);
                    });
            }
            amplify.request("getExistingMarketingLists", { cid: connectionId },
                function (data) {
                    showMarketingLists();
                    mapping.fromJS(
                        data.ResponseEntity,
                        dataMappingOptions,
                        marketingListsViewModel.marketingLists);

                    ko.applyBindings(marketingListsViewModel, $('#marketingLists')[0]);
                });
        };
    return {
        init: init,
        marketingListsViewModel: marketingListsViewModel,
        html: html,
        css: css
    }
});

With this view:

<div id="marketingListsContainer">
            <ul data-bind="template: {name: 'itemTmpl' , foreach: marketingLists}"></ul>
            <script id="itemTmpl" type="text/html">
                <li>
                    <!-- ko if: $data.Parent -->
                    (my parent is: <span data-bind="text: $data.Parent().Name"></span>) 
                    <!-- /ko -->
                    <label><input type="checkbox" data-bind="checked: Selected" /><span data-bind='text: Name'></span></label>                    
                <ul data-bind="template: { name: 'itemTmpl', foreach: Children }" class="childList"></ul>
            </script>
        </div>
        <a class="s_button modalClose right" href="#"><span data-bind="click: saveMarketingListChanges">Save and close</span></a><br>

You could add a subscribe event to the selected binding and do something like this which would mark all the children as selected when you check a parent.

self.Selected.subscribe( function ( newValue ) {
    if ( newValue === true ) { 
        for ( i=0; i<self.Children().length; i++) {
            self.Children()[i].Selected(true);
        }
    }
    else {
         if ( self.Parent() != null ) { 
             self.Parent().Selected(false);
         }
    }
} ) ;

Then on your save method you'll have to do some custom traverse logic of your lists

marketingListsViewModel.saveMarketingListChanges = function (
    var allSelected = ko.toJS( marketingListsViewModel.selectedLists );
    var ids = [];

    for ( i = 0; i < allSelected.length; i++ ) {
        ids.concat ( marketingListsViewModel.getIds( allSelected[i] ) );
    }

    var latestMarketingListChanges = ko.toJSON(ids);
    ...
);

marketingListsViewModel.getIds = function( item ) { 
     var ids = []

     if ( item.Selected === true ) { 
         ids.add( item.Id );
     }
     else if ( item.Children.length > 0 ) {
         for ( i = 0; i < item.Children.length; i++ ) {
             ids.concat( marketingListsViewModel.getIds( item.Children[i] )  )
         }
     }

     return ids;
}

Now bear in mind I've not bothered writing working javascript here but you get the idea. You'll probably need to write an array concatenation function or extend jQuery or something like that.

Hope it helps

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM