jsrenderjsviews

JSViews observable arrays are not refreshing the view on removed / adding and sorting


I am trying to move items from one array to another, and sorting them by name.

this part works fine and it seems doing observable.refresh updates the data itself but the view still shows old data, using moveFromTo() will move the items, but will not update their order in the view.

moveFromToType2() will show the items that are added, but will not update the one that removed from.

In both cases doing view.refresh() solves the issue but I don't think this is an intended behavior.

https://jsfiddle.net/y946xhvq/

<body>
<div id="multiselect"></div>

<script id="multiselectTemplate" type="text/x-jsrender">
    <select id="leftSelect" multiple>
        {^{for left}}
            <option data-link="value{:#index}">{^{:name}}</option>
        {{/for}}
    </select>
    <button id="rightButton"> > </button>
    <button id="leftButton"> < </button>
    <select id="rightSelect" multiple>
            {^{for right}}
                <option data-link="value{:#index}">{^{:name}}</option>
            {{/for}}
    </select>
</script>

<script>
    var data = {
        left: [{ "id": 0, "name": "Melendez Garner" }, { "id": 1, "name": "Mara Orr" }, { "id": 2, "name": "Bass Salazar" }, { "id": 3, "name": "Carol Freeman" }, { "id": 4, "name": "Selma Bradford" }, { "id": 5, "name": "Cotton Parrish" }, { "id": 6, "name": "Haley Campbell" }, { "id": 7, "name": "Ruth Wright" }, { "id": 8, "name": "Carmella Blake" }],
        right: []
    }
    data.left.sort((a, b) => a.name.localeCompare(b.name));
    var m = $.templates('#multiselectTemplate').link('#multiselect', data);

    $('#multiselect').on('click', '#leftButton', function () {
        //MOVE FROM RIGHT TO LEFT
        var view = $.view(this);
        var value = $('#rightSelect').val();
        if (value === null) return;
        //moveFromToType2(data.right, data.left, value);
        moveFromTo(data.right, data.left, value);
        //view.refresh();
    }).on('click', '#rightButton', function () {
        //MOVE FROM LEFT TO RIGHT
        var view = $.view(this);
        var value = $('#leftSelect').val();
        if (value === null) return;
        //moveFromToType2(data.left, data.right, value);
        moveFromTo(data.left, data.right, value);
        //view.refresh();
    })

    function moveFromToType2(from, to, index) {
        if (from.length == 0) return;
        if (index !== undefined) {
            let selected = index.map(d => from[d]);
            selected.forEach(d => {
                let i = from.indexOf(d);
                from.splice(i, 1);
            });
            $.observable(to).insert(selected);
            to.sort((a, b) => a.name.localeCompare(b.name));
            from.sort((a, b) => a.name.localeCompare(b.name));
        }
    }

    function moveFromTo(from, to, index) {
        if (from.length == 0) return;
        if (index !== undefined) {
            let selected = index.map(d => from[d]);
            selected.forEach(d => {
                let i = from.indexOf(d);
                $.observable(from).remove(i);
            });
            $.observable(to).insert(selected);
            to.sort((a, b) => a.name.localeCompare(b.name));
            from.sort((a, b) => a.name.localeCompare(b.name));
            $.observable(to).refresh(to);
            $.observable(from).refresh(from);
        }
    }
</script>

Solution

  • The issue there is that you are making a mix of non-observable changes, and observable changes, to to and from. You need to make only observable changes to those arrays, if you want the UI to update correctly, driven by the data-linking.

    Here is a version which corrects this, by first cloning each array, then making non-observable changes to the clones, then finally passing in the clones to refresh(), to make the observable updates (incorporating all the changes):

    https://jsfiddle.net/BorisMoore/qsvhm4x6/4/

    function moveFromTo(from, to, index) {
        if (from.length == 0) return;
        let newFrom = from.slice(0); // Make a clone of from array
        if (index !== undefined) {
            let selected = index.map(d => newFrom[d]);
            selected.forEach(d => {
                let i = newFrom.indexOf(d);
                newFrom.splice(i, 1); // Remove selected objects from clone
            });
            let newTo = to.concat(selected); // Make a clone of to array, plus selected objects
            newTo.sort((a, b) => a.name.localeCompare(b.name)); // Sort
            newFrom.sort((a, b) => a.name.localeCompare(b.name)); // Sort
            $.observable(to).refresh(newTo); // Now observably update 'to', to the new array
            $.observable(from).refresh(newFrom); // Now observably update 'from', the new array
        }
    }
    

    And here is a different approach, using the built-in sort feature on the {^{for}}, rather than sorting the underlying data. It also uses data-linking on the <select> elements:

    https://jsfiddle.net/BorisMoore/f70pnkwa/11/

    <select id="leftSelect" multiple size="10" data-link="leftSelect">
        {^{for left sort="name"}}
            <option data-link="value{:id} {:name}"></option>
        {{/for}}
    </select>