Search code examples
jsrenderjsviews

JsViews: Update/refresh data on multiple `select's` dynamically simultaneously - using depends=


I am having trouble getting observable data to update/refresh on multiple select's dynamically simultaneously.

When rendering two+ select's with a filter displaying only those option's that are NOT selected in another select. Thus, if an option is selected in one select that same option should not appear in the other select's.

...on initial page load, the options are filtered correctly.

...on select change, the options do not re-filter correctly, I think because the options are filtered before the data value is updated (but I'm not sure they would all refresh anyway).

{^{for selectedVals ~selectedVals=selectedVals ~listVals=listVals}}
     <select data-link="id">
        {^{for ~catFilter(id, ~listVals, ~selectedVals)}}
            <option data-link="value{:id} {:category}"></option>
        {{/for}}
    </select>
{{/for}}
$.views.helpers({
     catFilter: function(id, listVals, selectedVals) {
          return listVals.filter(item1 =>
              !selectedVals.some(item2 => (item2.id) === item1.id && item1.id !== id));
     }
})

I'm not quite sure the best approach to this and have tried using the built in filter=... mapDepends="id", convertBack...with little success.

Now I am listening for changes observably and refreshing the array...but It's not working (not sure if I am doing this right).

$(data[0].selectedVals).on("propertyChange", changeHandler)
function changeHandler(ev, eventArgs) {
     $.observable(data[0].selectedVals).refresh(data[0].selectedVals)
}

Here is a demo: https://jsfiddle.net/alnico/tr6wfn13/

Any help/insight would be appreciated.


Solution

  • The best way to do that would be not to try to trigger arrayChange events on the data, but rather to make just the contents of the {^{for ~catFilter(id, ~listVals, ~selectedVals)}} tag refresh whenever one of the selectedVals items has an observable change to its id.

    You can do that by specifying a depends property on your ~catFilter() helper function (see Declaring dependencies for a computed observable), or on the {^{for}} tag itself.

    In fact your 'depends' path can use the wild card .[] which lets you listen to a given property on any item within an array. In your case it will be depends = "~selectedVals.[].id" - which will listen only to changes in an id property on an item in the array, and will trigger refresh on the content of the {^{for ...}} tag.

    That .[] wild card is lacking detailed documentation (I hope to add it at some point) but you can see it used in samples such as here.

    There are two ways you can add that dependency:

    Declaratively on the {^{for}} tag:

    {^{for ~catFilter(id, ~listVals, ~selectedVals) depends="~selectedVals.[].id"}}
    

    or programmatically on the observable helper function:

    $.views.helpers.catFilter.depends = "~selectedVals.[].id";
    

    The same approach can be used if you set a filter=... function on your {^{for}} tag rather than using a computed helper to filter. You simply specify the depends property on your filter function:

    {^{for ~listVals filter=~listFilter}}
      <option data-link="value{:id} {:category}"></option>
    {{/for}}
    

    with the helper:

    listFilter: function(item1) {
      return !this.ctxPrm("selectedVals").some(
        item2 => (item2.id) === item1.id && item1.id !== this.view.data.id
      );
    }
    

    and then either add the depends="~selectedVals.[].id" to the tag (as with the catFilter() version), or set it on the listFilter function, like this:

    $.views.helpers.listFilter.depends = "~selectedVals.[].id";
    

    Here is a version of your jsfiddle showing these alternative approaches.