简体   繁体   中英

Knockout JS using computed arrays outside of ViewModel

I would like to display a list of items on a page, and be able to dynamically reposition items by using a dropdown list of all positions. Selecting a position from the dropdown will change the current position of the item and re-shift the position any affected elements in the list.

I do have a working version of this concept, but it is not ideal. For some reason, when I reference my selectedItems computed array (I filter my items by setting the selectedItem observable), the position that is contained in the returned item is the original position value of the item, and not the one that has been set via the dropdowns/my reposition function. This is somewhat odd because the 'items' observableArray does contain the updated value, and the computedArray does return the right item, just not with the most up to date value.

A working JSfiddle is below. However, it does a lot of manual calculation and does not take advantage of the computed array as described above. The issue might be somehow related to setting Knockout observables from outside the ViewModel. To see the issue, uncomment the 2 lines in the 'document ready' block', where I attempt to find the current position of an item, and comment out the for loop where I look for the current item manually.

https://jsfiddle.net/tq1m873m/5/

I'm new to KnockoutJS & JS in general, be gentle :)

$(document).ready(function () {

    $("select[id^='selectName_']").change(function () {

        //Extract the item ID from the select html id attribute
        var curItemIDNum = $(this).attr('id').substring(15);

        var currentPos = 0;

        // myViewModel.selectedItem("item" + curItemIDNum);
        // currentPos = myViewModel.selectedItems()[0].position();  

        // START - really bad code, shield your eyes
        // I can't seem to get the current position via the 2 commented lines above and have to resort to converting the observable array to a regular array and pulling the value that way. Not pretty!
        var itemsJS = ko.toJS(self.items());
        for (var x = 0; x < itemsJS.length; x++) {
            if (("item" + curItemIDNum) == itemsJS[x].name) {
                currentPos = itemsJS[x].position;
                break;
            }
        }
        // END - really bad code

        reposition("item" + curItemIDNum, currentPos, $(this).val());
        refreshDropDowns();
    });

    refreshDropDowns();

});

You were working on this before, and I didn't have a working solution for you. Today, I do. Your use of a jQuery trigger is not going to work out well. Let's do it all with Knockout.

I made items to be just an array of objects that do not have assigned positions. orderedItems is a computed that goes through items in order and creates an observable for position .

A subscription on that position observable calls moveItemTo , which rearranges items, and all the dependencies are updated by Knockout.

 $(function() { ko.applyBindings(viewModel()); }); function item(name) { return { name: name }; } var viewModel = function() { var self = {}; self.items = ko.observableArray([ item('item1'), item('item2'), item('item4'), item('item5'), item('item3') ]); function moveItemTo(item, pos) { var oldPos = self.items.indexOf(item), newPos = pos - 1, items; if (oldPos < newPos) { items = self.items.slice(oldPos, newPos + 1); items.push(items.shift()); self.items.splice.bind(self.items, oldPos, items.length).apply(self.items, items); } else { items = self.items.slice(newPos, oldPos + 1); items.unshift(items.pop()); self.items.splice.bind(self.items, newPos, items.length).apply(self.items, items); } } self.orderedItems = ko.computed(function() { return ko.utils.arrayMap(self.items(), function(item, index) { var pos = ko.observable(index + 1); pos.subscribe(moveItemTo.bind(null, item)); return { name: item.name, position: pos }; }); }); return self; }; //end of viewmodel 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script> <div>Set the order of the item by selecting a new position from the dropdown: <ul data-bind="foreach: orderedItems"> <li> <div> <span data-bind="text: name"></span> <select data-bind="options: $root.orderedItems, optionsValue:'position', value: position"></select> </div> </li> </ul> </div>ITEMS CONTENTS: <BR> <span data-bind="text: JSON.stringify(ko.toJS(items), null, 4)"></span> 

One way I can think of doing this would be to use a computed value to hold the current state of the position. This would allow you to do your reshuffling when a new position is set on an item like below :

ko.utils.arrayForEach(self.items(), function (x) {
        x.newPosition = ko.computed({
            read: function () {
                return x.position();
            },
            write: function (val) {
                //get the item in the prev position
                var temp = ko.utils.arrayFirst(self.items(), function (y) {
                    return y.position() == val;
                });
                //swap positons here
                if (temp) {
                    temp.position(x.position());
                    x.position(val);
                }
            }
        });
    });

in the Mark up it would be

<select data-bind="options:positions,value:newPosition"></select>

so on computed "write" ... the script swaps the position values. I left your original binding to the orderedItems. you can find a working sample here https://jsfiddle.net/p1yhmvcr/2/ ... the one thing worth noting here would be the sorting is not really physical. the observable array items are still on their original index in the array and the code is only changing the position values.

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