BorisMoore / jsviews

Interactive data-driven views, MVVM and MVP, built on top of JsRender templates
http://www.jsviews.com/#jsviews
MIT License
856 stars 130 forks source link

template does not react to array changes #420

Closed johan-ohrn closed 5 years ago

johan-ohrn commented 5 years ago

Please see the following fiddle

What I'm trying to do is have a function execute when an array is changed observably. I.e. adding or removing elements from the array. The example is very simple. I want to aggregate information from the array and present it. Here I have used a function on the viewmodel but it might as well have been a converter.

As demonstrated the following template expression does not pick up the observable insert: {^{:highestNumber(array)}} but this expression does: {^{:array.length && highestNumber(array)}}

I assume that the thought process is that you would normally iterate over arrays such as {^for{ array}} but in my case I want to present the array by passing it through a function or a converter.

I'm able to make it work by adding .length to the template expression and this causes jsviews to re-evaluate the whole expression as a result of the length changing.

BorisMoore commented 5 years ago

Yes, that is by design. {^{someTag a some.b fn(c)/}} will update by default when a b or c change (property change) but will not update (be default) if a b or c is an array and there is an observable array change.

This is because usually you don't want the whole tag to refresh whenever one of the arrays changes - especially with {{for arrayA}}, which is optimized to do incremental rendering when the array changes.

So the design is largely driven by perf considerations.

On the other hand, depends paths, and observe paths do trigger changes if arrays change.

So in your sample if you write

  function highestNumber() {
    var max = 0;
    for(var i =0; i <this.array.length; i++) {
        max = Math.max(max, this.array[i].value);
    }
    return max;
  }

  highestNumber.depends = "array"

then {^{:highestNumber()}} will update on array change. (Or you could write {^{:highestNumber() depends="array"}} so the tag declares the dependency, rather than the function.

But that said, this is not very intuitive, and as a fix for your scenario might be considered a bit hacky...

So once again your feedback has led me to some interesting considerations, and here:

jsviews_1.0.3_candidateE.zip

you will find an update which includes a new feature. On any tag, you can set onArrayChange=true and it will respond to array changes.

In your case you can write

{^{:highestNumber(array) onArrayChange=true}}`

Or you could create a custom tag

$.views.tags("mytag", {
  render: function(val) {return val;},
  onArrayChange: true
});

with

{^{mytag highestNumber(array)/}}`

Let me know if it works for you, and if you find any issues.

johan-ohrn commented 5 years ago

Would it be possible to have onArrayChange set to true by default for colon/print tags (:) such as {^{:highestNumber(array)}}. I can understand how it would be a bad idea for block tags such as {^{for}} or any other tag for that matter that have content. Given my current understanding I don't see a situation where you would include an array in a colon/print tag expression and not wanting the expression to re-evaluate if the array changes.

BorisMoore commented 5 years ago

Yes, I agree that makes sense... I had some concerns about backward compat since previously be default tags didn't observe array changes. Also we need some level of consistency between different tags. But on reflection I decided on the following approach:

By default any tag (or data-link binding) will 'observe' array changes as well as property changes. However you can set {^{foo onAfterChange=false}} on any tag or data-link expression, and you can also set it as an option onAfterChange: false on custom tags. The direct setting on the tag markup overrides the setting in the tag declaration, so you can have a custom tag which defaults to false, but has a tag instance which overrides it: {^{foo onAfterChange=true}}

Any tag which has onAfterChange set to a function (at the tag markup level or the tag declaration level) is assumed to handle its own onAfterChange behavior, so the built-in default of updating the tag whenever there is an array change will be suppressed for that tag.

So by default {^{:}} {^{>}} {^{if}} {^{include}} will observe array changes but {^{for}} and {^{props}} will not (but will use their built in incremental onArrayChange behavior). {^{on}} is also set not to observe array changes.

Here is the code: jsviews_1.0.3_candidateF.zip

johan-ohrn commented 5 years ago

Initial testing is positive. I'll keep monitoring to see if anything shows up.

BorisMoore commented 5 years ago

The above changes are included in the new v1.0.3 release. (Let me know if see any issues!)