Search code examples
javascriptjquerydomjsviews

jsViews $.observable(arr).insert() not triggering DOM update


I am using $.observable(array).insert() to append items to a list. This is updating my view as it should: new list items are rendered to the DOM. However, I would like to issue a click event on the new DOM node (I'm relying on the event to add a class to expand the item and attach another listener to the body so the area can be closed).

I have tried both

$.observable(_model.leadTimes).insert(leadTime);
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();

...and

function watchLeadTimes() {

    var changeHandler = function (ev, eventArgs) {
        if (eventArgs.change === 'insert') {

            $leadTimes.find('.lead-time-data').last().find('.start-editing').click();

        }
    };

    $.observe(_model.leadTimes, changeHandler);

}

And neither of them worked, however, if I wrap the jQuery method in a setTimout, like setTimeout(function () { $leadTimes.find('.lead-time-data').last().find('.start-editing').click(); }, 400);, it does work, leading me to believe this is an issue of timing with the DOM render somehow not finishing before my jQuery click() method is invoked.

Since the odds are decent that you will see this, Borris, thank you for the library and all that you do! I think jsViews is an excellent middle ground between the monolithic frameworks out there and plain old jQuery noodling!


Edit 02/09/17

It turns out my issue was overlapping click events--I was inadvertently handling a click to deselect my element immediately after it was selected. However I took the opportunity to rewrite things to use a more declarative approach following Borris' linked example.

Now in my template I am using a computed observable, isSelected to toggle the .editing class:

{^{for leadTimes}}
    <tr class="lead-time-data" data-link="class{merge:~isSelected() toggle='editing'}">
        <span>{^{:daysLead}}</span>
    </tr>
{{/for}}

And this JS:

function addNewLeadTimeClickHandler() {

    var onNewLeadTimeClick = function () {

        e.stopPropagation();  // this is what I was missing

        var leadTime = {
            daysLead: 1,
            description: ''
        };

        $.observable(_model.activityMapping.leadTimes).insert(leadTime);
        selectLeadtime(_model.activityMapping.leadTimes.length -1);

    }
    $leadTimes.on('click', '.add', onNewLeadTimeClick);

}

function selectLeadtime(index) {

    var addStopEditingClickHandler = function () {

        var onClickHandler = function (event) {
            if ($(event.target).closest('tr').hasClass('editing')) {
                setHandler();
                return;
            }

            selectLeadtime(-1)

        };

        function setHandler() {
            var clickEvent = 'click.ActivityChangeRequestDetailController-outside-edit-row';
            $('html:not(.edit)').off(clickEvent).one(clickEvent, onClickHandler);
        };

        setHandler();

    }

    if (_model.selectedLeadtimeIndex !== index) {
        $.observable(_model).setProperty('selectedLeadtimeIndex', index)
        addStopEditingClickHandler();
    }

}
function isSelected() {
    var view = this;
    return this.index === _model.selectedLeadtimeIndex;
}
// isSelected.depends = ["_model^selectedLeadtimeIndex"];
// for some reason I could not get the above .depends syntax to work
// ...or "_model.selectedLeadtimeIndex" or "_model.selectedLeadtimeIndex"
// but this worked ...
isSelected.depends = function() {return [_model, "selectedLeadtimeIndex"]};

Solution

  • The observable insert() method is synchronous. If your list items are rendered simply using {^{for}}, then that is also synchronous, so you should not need to use setTimeout, or a callback. (There are such callbacks available, but you should not need them for this scenario.)

    See for example http://www.jsviews.com/#samples/editable/tags (code here):

    $.observable(movies).insert({...});
    // Set selection on the added item
    app.select($.view(".movies tr:last").index);
    

    The selection is getting added, synchronously, on the newly inserted item.

    Do you have other asynchronous code somewhere in your rendering?

    BTW generally you don't need to add new click handlers to added elements, if you use the delegate pattern. For example, in the same sample, a click handler to remove a movie is added initially to the container "#movieList" with a delegate selector ".removeMovie" (See code). That will work even for movies added later.

    The same scenario works using {{on}} See http://www.jsviews.com/#link-events: "The selector argument can target elements that are added later"