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

linked tag property changed handler called when it shouldn't #440

Closed johan-ohrn closed 4 years ago

johan-ohrn commented 4 years ago

Run this fiddle with the console open and you'll see that externally updating one of the two bound properties actually triggers setValue to be called for both properties even though only one of them was changed. What's worse is that I can't determine if the value was actually changed or not so any code I put in setValue would execute needlessly.

$.views.tags({
  mytag: {
    bindTo:["prop1", "prop2"],
    template: "",
    setValue: function(value, index, elseBlock) {
      if (index == 0)
        console.log("prop1 changed from " + value + " to " +this.tagCtx.props.prop1);
      else if (index == 1)
        console.log("prop2 changed from " + value + " to " +this.tagCtx.props.prop2);
     }
  }
});

var vm = {
  prop1:"a",
  prop2:"b"
};
$.templates("{^{mytag prop1=prop1 prop2=prop2 /}}").link("#ph", vm);

setTimeout(function() {
  console.log("timeout..")
  $.observable(vm).setProperty("prop1", "blah")
});

Usually I prefer to manually attach/detach $.observe listeners like this fiddle. Here you'll notice that only the event for the changed property is triggered and I have access to both the old value and the new value.

$.views.tags({
  mytag: {
    bindTo:["prop1", "prop2"],
    template: "",
    init: function() {
      $.observe(this.tagCtx.props, 'prop1', this.onProp1Change);
      $.observe(this.tagCtx.props, 'prop2', this.onProp2Change);
    },
    onProp1Change: function(e, ev) {
      console.log("prop1 changed from " + ev.oldValue + " to " + ev.value);
    },
    onProp2Change: function(e, ev) {
      console.log("prop2 changed from " + ev.oldValue + " to " + ev.value);
    }
  }
});

var vm = {
  prop1:"a",
  prop2:"b"
};
$.templates("{^{mytag prop1=prop1 prop2=prop2 /}}").link("#ph", vm);

setTimeout(function() {
  console.log("timeout..")
  $.observable(vm).setProperty("prop1", "blah")
});

I know I've asked about this part before but I want to reiterate again, this time with some ideas. Anyway I'm taking option 2 one step further as in this fiddle you can see how I introduced a helper function on prop2. I've deliberately named it scramble to make it obvious that it does have side effects. When prop1 is changed observably it triggers the whole tag to re-link and re-evaluate all it's bindings. This is giving me quite some headache.

$.views.tags({
  mytag: {
    bindTo:["prop1", "prop2"],
    template: "",
    init: function() {
      $.observe(this.tagCtx.props, 'prop1', this.onProp1Change);
      $.observe(this.tagCtx.props, 'prop2', this.onProp2Change);
    },
    onProp1Change: function(e, ev) {
      console.log("prop1 changed from " + ev.oldValue + " to " + ev.value);
    },
    onProp2Change: function(e, ev) {
      console.log("prop2 changed from " + ev.oldValue + " to " + ev.value);
    }
  }
});

$.views.helpers({
  scramble: function(v) {
    return v.split("").sort(function(){return Math.random() <0.5 ? -1 : 1}).join("");
  }
});

var vm = {
  prop1:"hello",
  prop2:"world"
};
$.templates("{^{mytag prop1=prop1 prop2=~scramble(prop2) /}}").link("#ph", vm);

setTimeout(function() {
  console.log("timeout..")
  $.observable(vm).setProperty("prop1", "blah")
});

This is a stack trace as seen by setting a breakpoint in the onProp2Change handler

onProp2Change ((index):45)
onDataChange (jsviews.js:3087)
dispatch (jquery-1.9.1.js:3074)
elemData.handle (jquery-1.9.1.js:2750)
trigger (jquery-1.9.1.js:2986)
triggerHandler (jquery-1.9.1.js:3683)
_trigger (jsviews.js:3882)
batchTrigger (jsviews.js:3223)
setProperty (jsviews.js:3802)
mergeCtxs (jsviews.js:6425)
onDataLinkedTagChange (jsviews.js:4524)
handler (jsviews.js:5925)
onDataChange (jsviews.js:3087)
dispatch (jquery-1.9.1.js:3074)
elemData.handle (jquery-1.9.1.js:2750)
trigger (jquery-1.9.1.js:2986)
triggerHandler (jquery-1.9.1.js:3683)
_trigger (jsviews.js:3882)
_setProperty (jsviews.js:3862)
setProperty (jsviews.js:3813)
(anonymous) ((index):64)
setTimeout (async)
(anonymous) ((index):62)
dispatch (jquery-1.9.1.js:3074)
elemData.handle (jquery-1.9.1.js:2750)
load (async)
add (jquery-1.9.1.js:2796)
(anonymous) (jquery-1.9.1.js:3622)
each (jquery-1.9.1.js:648)
each (jquery-1.9.1.js:270)
on (jquery-1.9.1.js:3621)
jQuery.fn.<computed> (jquery-1.9.1.js:7402)
jQuery.fn.load (jquery-1.9.1.js:7543)
(anonymous) ((index):31)

1) onDataLinkedTagChange(ev, eventArgs) is called and eventArgs contains the name of the property that was actually changed. In this case prop1. 2) onDataLinkedTagChange executes this sourceValue = linkFn(source, view, $sub); which is responsible for re-evaluating all the bindings. In my case it looks like this:

function anonymous(data,view,j,u) {
 // unnamed 1 mytag
return [
{view:view,content:false,tmpl:false,
  params:{args:[],
  props:{'prop1':'prop1','prop2':'~scramble(prop2)'}},
  args:[],
  props:{'prop1':data.prop1,'prop2':view.ctxPrm("scramble")(data.prop2)}}]
}

3) onDataLinkedTagChange goes on to call mergeCtxs(tag, sourceValue, forceUpdate); 4) mergeCtxs calls $observable(tagCtx.props).setProperty(newTagCtx.props); which will trigger calling onProp2Change because onDataLinkedTagChange re-evaluated the links in step 2 and the scramble method returned a new value.

One thing that I'm hoping would be easy to fix is that onDataLinkedTagChange could pass along the name of the prop that actually did change when calling mergeCtxs such that it could update the property that did change and nothing else. At least this prevents any change handlers from executing where you don't expect them to.

Next thing is the fact that all linked properties are re-evaluated and not just the one that changed. One way to prevent needless execution would be to change the way the compiled link function is generated so that links are evaluated lazily such as this:

function anonymous(data,view,j,u) {
 // unnamed 1 mytag
return [
{view:view,content:false,tmpl:false,
  params:{args:[],
  props:{'prop1':'prop1','prop2':'~scramble(prop2)'}},
  args:[],
  props:{'prop1':() =>data.prop1, 'prop2':() =>view.ctxPrm("scramble")(data.prop2)}}]
}

I'm hoping such a change wouldn't be all to hard to implement. I can't imagine that the added overhead of calling a function would give any performance penalty nor should it add much to the code size.

The benefit would be way more efficient templates. Some of the problems I've noticed during the course of our project is related to this. The fact that you expect one change handler to be triggered but some other change handler also trigger. If these change handlers in turn trigger other change handlers the code quickly becomes very complex and I find that we have to write quite a lot of code to try to ignore false change events and the like. Aside from less spaghetti code related to the change handlers, templates would also become more performant. In this contrived example nothing happens when prop2 is changed falsily but imagine that this tag was a root tag which contains a whole tree of other tags and changing prop2 as happens in this example would cause the whole tag tree to re-render and execute a bunch of logic.

So whatever minute performance impact adding lazyness to the compiled link function would incur it will most definitely be gained back by more efficient templates.

BorisMoore commented 4 years ago

Thanks for all this work, Johan. I'll be travelling in the coming period (not sure yet quite how long), so I won't be able to investigate this until after my return. I appreciate your feedback and ideas, so I hope to dig in to this after when I get back.

BorisMoore commented 4 years ago

Hi Johan. I finally managed to explore these questions.

From my point of view, there are life-cycle events that you can provide handlers for, namely onBeforeChange, onAfterChange, onUpdate, which are used to track when data on which a tag depends changes. But setValue is not really a life-cycle event - it is a different category of handler.

For the life-cycle events, the ev, and eventArgs arguments let you know what data changed. It is not based on specific tag args or properties that changed. Indeed, you may have a single data change that involves two different properties changing, e.g. if you have

{^{mytag prop1=x+y prop2=x-y depends='z'/}}

If x changes, onAfterChange etc. will be called just once (not twice, even though two tag properties are updated). Same if z changes, but here there are no tag property changes associated.

But setValue on the other hand is not a life-cycle event, but rather an override, to let you make changes to the tag state associated with specific tag properties (specified by bindTo). See for example the the areaSlider.

So setValue is called whenever you might want to set the internal state (such as the drag-handle position on a slider tag). That means it is called for each property every time the tag re-renders. Note that you can set the onUpdate to false if you don't want it to completely rerender when a bindTo property changes, but just re-link. So setting onUpdate to false (or returning false) may reduce the number of calls to setValue.

In addition to observable changes to data, there are observable changes to tagCtx.props, as you mention, in mergeCtxs. This is primarily so that you can include things like {^{:~tagCtx.props.prop1}} in a template, and have it update dynamically.

Here is a modified version of jsviews.js in which I have made a few changes that may help your scenarios:

jsviews.js.txt

This version provides additional parameters to setValue, so you can determine what data changed. (Not specifically what tag property changed, but what data changed, though for your specific tag you may be able to determine the tag property too, based on your usage...)

Your signature can now be: setValue: function(value, index, elseBlock, ev, eventArgs) { ... }

In addition this version does reduce the number of calls to setValue. If onUpdate is false it is called once for each property during initial rendering. Then, if there is an observable change to a tag property, setValue (for this new version of jsviews.js) will only be called just once, for that specific property.

It also includes a couple of other bug fixes that I came across.

Here is some test code:

$.views.tags({
  mytag: {
    bindTo:["prop1", "prop2"],
    template: "",
    init: function() {
      $.observe(this.tagCtx.props, 'prop1', this.onProp1Change);
      $.observe(this.tagCtx.props, 'prop2', this.onProp2Change);
    },
    onProp1Change: function(ev, eventArgs) {
      console.log("prop1 changed from " + eventArgs.oldValue + " to " + eventArgs.value);
    },
    onProp2Change: function(ev, eventArgs) {
      console.log("prop2 changed from " + eventArgs.oldValue + " to " + eventArgs.value);
    },
    onBeforeChange: function(ev, eventArgs) { 
      console.log("onBefore " + eventArgs.path + " changed from " + eventArgs.oldValue + " to " + eventArgs.value);
      return true;
    },
    onAfterChange: function(ev, eventArgs) { 
      console.log("onAfter " + eventArgs.path + "changed from " + eventArgs.oldValue + " to " + eventArgs.value);
    },
    setValue: function(value, index, elseBlock, ev, eventArgs) {
      console.log("setValue " + this.bindTo[index] + "=" + value + (eventArgs ? (". " + eventArgs.path + " changed from " + eventArgs.oldValue + " to " + eventArgs.value) : ""));
    },
    onUpdate: function(ev, eventArgs, tagCtxs) {
      console.log("onUpdate " + eventArgs.path + " changed from " + eventArgs.oldValue + " to " + eventArgs.value);
      return true;
    }
  }
  });

var vm = {
  a0: "A",
  p1: "P1",
  p2: "P2"
};

$.templates("{^{mytag a0 prop1=p1 prop2=p2 /}}<br/><input data-link='p1'/> <input data-link='p2'/> <input data-link='a0'/>").link("#ph", vm);

This update tries to address your issues without making major internal changes or introducing significant breaking changes. Let me know how it works for you...

BorisMoore commented 4 years ago

@johan-ohrn : The attached fix above also includes the fix for https://github.com/BorisMoore/jsviews/issues/439, so both aspects are available for review...

johan-ohrn commented 4 years ago

Sorry for my slow response. I've been overloaded lately and haven't gotten around to test until now. Would you happen to have this version as separate files (jsviews, jsobservable, jsrender)?

BorisMoore commented 4 years ago

Here they are: download.zip

BorisMoore commented 4 years ago

Hi @johan-ohrn - I want to publish an update soon with these changes, so let me know if there are still issues for you... (or give me a sense of when you think you'll be able to validate that....) Thanks!

johan-ohrn commented 4 years ago

I'm away for the holidays. Will be back early January. Cheers!

johan-ohrn commented 4 years ago

Hi,

I managed to test the new version. I didn't find any obvious issues. Our code seem to continue to work fine.

Although I'm not helped much by the extra arguments passed to the setValue function. All I can determine from the extra arguments is that some data changed. Not which property/properties is linked to this data. This same problem is present in the areaSlider example as well, although it's not apparent because of trivial code in the setValue function. To demonstrate what I mean you can modify the setValue function from the Try it tab to the following:

  setValue: function(val, index) {
    // Move the handle to the new x-position or y-position
    var tagCtx = this.tagCtx,
      xMin = tagCtx.props.xMin,
      xMax = tagCtx.props.xMax,
      yMin = tagCtx.props.yMin,
      yMax = tagCtx.props.yMax;
      metrics = tagCtx.metrics;
    if (index) { // Change in y-position
      val = clamp(val, yMin, yMax);
      tagCtx.handle.offset({top: (val-yMin)*metrics.yScale + metrics.top - metrics.handleHeight});
console.log("y changed");
    } else { // Change in x-position
      val = clamp(val, xMin, xMax);
      tagCtx.handle.offset({left: (val-xMin)*metrics.xScale + metrics.left - metrics.handleWidth});
console.log("x changed");
    }
  },

Click the Run code button and start typing in the X or Y textbox. You'll notice in the console that it logs changes to both the X and Y tag arguments even though you only modify one of them. It doesn't matter in this example because the code is trivial but consider that a change in X or Y value instead would calculate the millionth digit of pi. Now all of a sudden it's a major issue because needless work is executed.

Given the areaSlider example. Is there a working pattern I could use to get notified when only the X argument is changed?

BorisMoore commented 4 years ago

Hi Johan, sorry about the delay. I did some more work on your scenarios, and hope to get you an updated version soon. I'll keep you posted...

BorisMoore commented 4 years ago

Hi Johan, I have an update which does now I think call setValue only when the corresponding value changes - for example with the areaSlider.

Here is the code: download.zip

Let me know if this finally resolves your concern...

johan-ohrn commented 4 years ago

Great I will give it a try!

I'll let you know what I find.

BorisMoore commented 4 years ago

Hi Johan, did you manage to test the fix above?

I have included this fix in the new update that I uploaded to here: https://github.com/BorisMoore/jsviews/issues/442#issuecomment-586102978.

johan-ohrn commented 4 years ago

I did some testing. I found a situation where the setValue function is called even though it shouldn't. Try the following:

<div class="container"></div>

$.views.tags({
    mytag: {
        template: "{^{:~tagCtx.props.p1}} {^{:~tagCtx.props.p2}}",
        bindTo:["p1","p2"],
        setValue:function(val, idx) {
            console.log("setValue called", val, idx);
        }
    }
});

var vm = {p1:undefined, p2:undefined};
$.templates("{^{mytag p1=p1 p2=p2 /}}").link(".container", vm);

setTimeout(function() {
    $.observable(vm).setProperty("p2", 11)
 })

setValue called undefined 1
setValue called undefined 0
// timeout
setValue called 11 1
setValue called undefined 0

The relevant bits is that the view model properties are initialized to undefined. This cause the setProperty call to trigger the setValue tag function for both tag properties "p1" and "p2".

However if all? properties except the one specified in the setProperty call is initialized with a value !== undefined then it works:

var vm = {p1:1, p2:undefined};

setTimeout(function() {
    $.observable(vm).setProperty("p2", 22)
 })

setValue called undefined 1
setValue called 1 0
// timeout
setValue called 22 1

If only the property used in the setProperty call are initialized with a value then it doesn't work:

var vm = {p1:undefined, p2:2};
setTimeout(function() {
    $.observable(vm).setProperty("p2", 22)
 })

setValue called 2 1
setValue called undefined 0
//timeout
setValue called 22 1
setValue called undefined 0
BorisMoore commented 4 years ago

Excellent, thanks for your thorough testing work!

Well I think I have a version that also fixes that scenario. Here it is: download.zip

Let me know if you this one works for you...

johan-ohrn commented 4 years ago

Thanks! It seem to work correctly now. setValue is called when the bound property changes. As far as I can tell it's working well enough for release.

However I'd like to take it a step further if possible. Consider the following.

$('body').prepend('<div class="container"></div>');

$.views.tags({
    mytag: {
        template: "{^{:~tagCtx.props.p1.v}} {^{:~tagCtx.props.p2}}",
        bindTo:["p1","p2"],
        setValue:function(val, idx) {
            console.log("setValue called", val, idx);
        }
    }
});

var vm = {p1:1, p2:2, fn:function(v) {return {v:v}}};
$.templates("{^{mytag p1=fn(p1) p2=p2 /}}").link(".container", vm);

setTimeout(function() {
    $.observable(vm).setProperty("p2", 22)
 })

setValue called 2 1
setValue called {v: 1, jQuery112409610440004349561: {…}} 0
// timeout
setValue called 22 1
setValue called {v: 1, jQuery112409610440004349561: {…}} 0  // do not expect setValue to be called

I changed the tag so that it now expects property p1 to be an object rather than a simple value. It's not desirable that updating p2 should trigger a call to fn(p1) because it returns a new object instance wrapping the unchanged value p1. This new object instance cause setValue to be executed for p1 even though it was property p2 that changed. I realize this is happening due to fn returning a different object instance but it would be desireable that the fn(p1) call is never made in the first place. The function fn might perform heavy computational work, or it might be that the implementation of setValue when p1 changes performs heavy computational work on an unchanged value. Or as happens frequently in our code: setValue is called with a new object instance but the data is really the same. The tag can't know that the data is the same and it re-render it's content with visible disturbance of the page as a result.

It's possible to work around this by computing the value in the view model like this:

var vm = {p1:1, p2:2, computedP1: undefined, fn:function(v) {return {v:v}}};
vm.computedP1 = vm.fn(vm.p1);
$.observe(vm,"p1",function() {
    $.observable(vm).setProperty("computedP1", vm.fn(vm.p1));
});
// unobserve logic here...
$.templates("{^{mytag p1=computedP1 p2=p2 /}}").link(".container", vm);

Now it's possible to observably update p1 and p2 separately and setValue will only be called when the respective property is changed. However this work around is less elegant and it requires a lot of code to maintain the computed values as the source changes. It would be much nicer if the view could do it for me without hurting the performance.

From analyzing the call stack I can see that $.observable(vm).setProperty("p2", 22) makes its way into the function onDataLinkedTagChange which executes the line sourceValue = linkFn(source, view, $sub);. linkFn is generated from the template and looks like this:

(function anonymous(data,view,j,u) {
 // unnamed 1 mytag
return [
{view:view,content:false,tmpl:false,
    params:{args:[],
    props:{'p1':'fn(p1)','p2':'p2'}},
    args:[],
    props:{'p1':data.fn(data.p1),'p2':data.p2}}]
})

Now it becomes obvious why it's working the way it does. All properties are executed immediatelly props:{'p1':data.fn(data.p1),'p2':data.p2}}]. Here's an idea. What if that line of code instead looked like this: props:{'p1':()=>data.fn(data.p1),'p2':()=>data.p2}}]. Now it's lazy execution and it would be possible for the caller to invoke all bindings or just the one that changed (p2 in the above example). Or perhaps make the generated function accept more input parameters instructing it which bindings to execute. Something like:

(function anonymous(data,view,j,u,props) {
 // unnamed 1 mytag
return [
{view:view,content:false,tmpl:false,
    params:{args:[],
    props:{'p1':'fn(p1)','p2':'p2'}},
    args:[],
    props:{'p1': props['p1'] ? data.fn(data.p1) : null, 'p2': props['p2'] ? data.p2 : null}}]
})

Now onDataLinkedTagChange could do something like sourceValue = linkFn(source, view, $sub, {'p2':true});.

If it would make things easier perhaps it would be possible to cache the result of previously executed bindings on the view or tag instance? If that's possible the generated view function could return the previously cached value instead of executing the binding again. Something like this:

(function anonymous(data,view,j,u,props) {
 // unnamed 1 mytag
return [
{view:view,content:false,tmpl:false,
    params:{args:[],
    props:{'p1':'fn(p1)','p2':'p2'}},
    args:[],
    props:{'p1': props['p1'] ? data.fn(data.p1) : view._.cache['p1'], 'p2': props['p2'] ? data.p2 : view._.cache['p2']}}]
})
BorisMoore commented 4 years ago

Hi Johan,

Thank you for this research and these ideas.

I may not have understood your suggestions correctly, so please correct me if I am wrong, but here is my immediate take:

It seems to me that the kind of changes you are suggesting above would imply radical rewriting of much of the JsRender/JsViews code and would imply a lot of breaking changes in the APIs.

For example, you suggest that the line sourceValue = linkFn(source, view, $sub); could return objects on which the property values are replaced by what are in effect getter functions for the values - which can be executed in a 'lazy' fashion.

But the sourceValue in that line is fact the array of tagCtxobjects, so your suggested 'lazy' API would mean that tagCtx.props.p1 would no longer be the value of the p1 property, but instead would be a getter function. To get the p1 property value you would have to call tagCtx.props.p1().

This would break a lot of user code. For example it seems to me that your mytag template above: template: "{^{:~tagCtx.props.p1.v}} {^{:~tagCtx.props.p2}}", would not work as is and would need to be rewritten as template: "{^{:~tagCtx.props.p1().v}} {^{:~tagCtx.props.p2()}}".

The line mergeCtxs(tag, sourceValue, forceUpdate, noUpdate); is supposed to update the properties, observably, using $observable(tagCtx.props).setProperty(newTagCtx.props); but now tagCtx.props are getter functions for the props so of course that line would not work correctly as things stand....

As I say, maybe I am missing something, so please let me know if that is the case....

BorisMoore commented 4 years ago

The previously discussed issues are fixed in v1.0.6. Closing this issue for now. (Let me know if you want to discuss further based on https://github.com/BorisMoore/jsviews/issues/440#issuecomment-589919896)

johan-ohrn commented 4 years ago

You understood this part correctly: For example, you suggest that the line sourceValue = linkFn(source, view, $sub); could return objects on which the property values are replaced by what are in effect getter functions for the values - which can be executed in a 'lazy' fashion.

This however is not what I meant: But the sourceValue in that line is fact the array of tagCtx objects, so your suggested 'lazy' API would mean that tagCtx.props.p1 would no longer be the value of the p1 property, but instead would be a getter function. To get the p1 property value you would have to call tagCtx.props.p1().

I don't want the end user aware of any change at all. User code would be unaffected. This is something that I want handled internally by jsviews.

Given the example with the template {^{mytag p1=fn(p1) p2=p2 /}} and the observable update $.observable(vm).setProperty("p2", 22) this is what is happening in pseudo code:

1) call linkFn() - this executes fn(p1) and evaluates p2 2) for each key/value in the returned props object, call $.observable(tagCtx.props).setProperty(key, value)

What I want to do with the lazy getter functions is instead this: 1) call linkFn() - this simply returns getter functions 2) for each key/value in the returned props object that was changed, call $.observable(tagCtx.props).setProperty(key, value()) (notice the () since value is a getter and not the actual value)

In my example it's p2 that is being changed and so only the getter for p2 would be executed.

Do I manage to explain more clearly what I'm suggesting?

johan-ohrn commented 4 years ago

Perhaps better than getter functions which require () to invoke might be to use real javascript getters.

Turning the original

(function anonymous(data,view,j,u) {
 // unnamed 1 mytag
return [
{view:view,content:false,tmpl:false,
    params:{args:[],
    props:{'p1':'fn(p1)','p2':'p2'}},
    args:[],
    props:{'p1':data.fn(data.p1),'p2':data.p2}}]
})

into

(function anonymous(data,view,j,u) {
 // unnamed 1 mytag
return [
{view:view,content:false,tmpl:false,
    params:{args:[],
    props:{'p1':'fn(p1)','p2':'p2'}},
    args:[],
    props:{
        get 'p1'() { return data.fn(data.p1) }, 
        get 'p2'() { return data.p2 }
    }}]
})

Using real getter functions on it's own should hopefully not affect anything. What's great however is that calling linkFn() does not invoke the user code! User code is only invoked when mergeCtxs observably updates properties from the props object returned by linkFn. If jsviews could be complemented such that it update only those properties that were changed rather than all properties.

johan-ohrn commented 4 years ago

I managed to create a demo to show you what I'm asking for. Could you please look at the modified files jsrender.js and jquery.views.js. (Apologies for butchering your code). Compare the files I provided with the newly released 1.0.6 files.

I made a couple of changes to jquery.views.js

to jsrender.js

This is just a POC and I don't consider this a great implementation. In particular the way I cache values from the template expressions in the _viewCache function. I "temporarily" override all getters/setters and functions on the data object. A better way would be if jsviews internally could read values from a data object using an intermediate method such as this to avoid tampering with the data object like I'm doing:

function getDataValue(/*object*/data, /*string*/propertyName) {
 return propertyName in cache
  ? value from cache
  : value from object AND store in cache
}

The cache should be emptied after each render cycle.

Here is some test code that can be used to see my change in action:

$.views.helpers({
 fn:function(x) {console.log("~fn("+x+")");return x;}
});

$.views.tags({
  mytag: {
    bindTo:["prop1", "prop2"],
    template: "{{include tmpl=#content/}}",
    setValue: function(value, index, elseBlock) {
      if (index == 0)
        console.log("prop1 changed from " + value + " to " +this.tagCtx.props.prop1);
      else if (index == 1)
        console.log("prop2 changed from " + value + " to " +this.tagCtx.props.prop2);
     }
  }
});

var vm = {
  _p1: "a",
  get p1(){console.log("get p1"); return this._p1},
  set p1(v){console.log("set p1"); this._p1 = v;},
  _p2: "b",
  get p2(){console.log("get p2"); return this._p2},
  set p2(v){console.log("set p2"); this._p2 = v;},
  fn:function(x) {console.log("fn("+x+")");return x;}
};
$.templates("{^{mytag prop1=~fn(p1) prop2=~fn(p2) /}}").link("body", vm);

setTimeout(function() {
  console.log("timeout..")
  $.observable(vm).setProperty("p1", "A")
}, 2000);

output BEFORE my change is this:

get p1
~fn(a)
get p2
~fn(b)
get p1
~fn(a)
get p1
get p2
~fn(b)
get p2
prop2 changed from b to b
prop1 changed from a to a
timeout..
get p1
set p1
get p1
~fn(A)
get p1
~fn(A)
get p2
~fn(b)
prop1 changed from A to A

output AFTER my change is this:

get p2
~fn(b)
get p1
~fn(a)
prop2 changed from b to b
prop1 changed from a to a
timeout..
get p1
set p1
get p1
~fn(A)
prop1 changed from A to A

What I wasn't fully able to achieve is to "tie" the cache to a particular view. Let me try to explain using the following template:

{^{mytag prop1=fn(p1)}}
  {^{mytag prop1=fn(p1) /}}
{{/mytag}}

When the template is rendered I cache the value returned by executing fn() from the outer mytag. When the nested mytag is rendered it will not execute fn() and will instead use the cached value returned by fn() from the outer mytag. I would want the cache key to include the tag or view instance so that fn() is executed again when the nested if tag is rendered.

Also I haven't done anything to unnamed arguments or context parameters.

I hope that if you look at this you might get some ideas on how these ideas could be implemented. Basically it comes down to this: During any given render cycle, don't call into the same user code more than once.

I might add that other frameworks such as Vue do this type of internal caching.

BorisMoore commented 4 years ago

Hi Johan, thanks for your POC. I'm very short of time these days, and so limited in how much time I can work on JsViews issues. So this is just a heads up that I will try to follow up on the above, but it may take a while before I manage to get back to you....

johan-ohrn commented 4 years ago

All good. Let me know if you need anything. Thanks

BorisMoore commented 4 years ago

Hi Johan, Well it has been a long time, but I have been working on a possible update that is a fairly significant rewrite of parts of the compilation code, and which seeks to address some related issues to those you speak about above:

jsviews.zip

Basically it is intended by default to provide caching for all computed properties and helpers, so that for any user-defined function, foo() or ~foo() used in template or data-link expressions (e.g. {^{:foo() }}... ) foo() will not get called multiple times as now. Instead it will get called once during rendering, once during data-linking/binding, and then once for each time an observable change triggers a new evaluation. The additional calls made in the current implementation will, in the new version, instead use cached return values.

You can switch off the caching by setting $.views.settings.advanced({cache: false}).

Of course this is not the same as your proposed caching process above, and does not address exactly the same concerns, though it is somewhat related...

Can you test it in your environment, and let me know how it works for you.

Thanks for helping...

Boris

johan-ohrn commented 4 years ago

I'll be happy to test it! Gonna have time this weekend. I'll get back to you.

BorisMoore commented 4 years ago

Sounds good - thanks for helping...

johan-ohrn commented 4 years ago

I've been doing some tests with the new version. So far I haven't found anything obvious that breaks our templates. I found some inconsistencies where the cache is not being used:

$.views.helpers({
 fn:function(x) {console.log("~fn("+x+")");return x;}
});

$.views.tags({
  mytag: {
    bindTo:["prop1", "prop2"],
    template: "{{include tmpl=#content/}}"
  }
});

var vm = {
  _p1: "a",
  get p1(){console.log("get p1"); return this._p1},
  set p1(v){console.log("set p1"); this._p1 = v;},
  fn:function(x) {console.log("fn("+x+")");return x;}
};

console.log("test 1");
$.templates("{{:~fn(1)}}{{:~fn(1)}}").link("body", vm);
// ~fn(1)
// ~fn(1)

console.log("test 2");
$.templates("{^{:~fn(1)}}{^{:~fn(1)}}").link("body", vm);
// ~fn(1)

console.log("test 3");
$.templates("{{:p1}}{{:p1}}").link("body", vm);
// get p1
// get p1

console.log("test 4");
$.templates("{^{:p1}}{^{:p1}}").link("body", vm);
// get p1
// get p1
// (2) get p1

console.log("test 5");
$.templates("{{:fn(1)}}{{:fn(1)}}").link("body", vm);
// fn(1)
// fn(1)

console.log("test 6");
$.templates("{^{:fn(1)}}{^{:fn(1)}}").link("body", vm);
// fn(1)

console.log("test 7");
$.templates("{{mytag prop1=~fn(1) prop2=p1 /}}").link("body", vm);
// ~fn(1)
// get p1

console.log("test 8");
$.templates("{^{mytag prop1=~fn(1) prop2=p1 /}}").link("body", vm);
// ~fn(1)
// get p1
// get p1
BorisMoore commented 4 years ago

Yes, the caching does not apply to all cases. In fact I included the caching as a by-product of some improvements I made in compiling templates, when using computed observables with data binding. So you currently get caching for {^{:fn()}} {^{:fn()}}, for example, but not for the unbound version {{:fn()}} {{:fn()}}. Also you the changes come from the parsing and compilation code when you have function calls using parens fn() in the template or data-link expression, but not when using JavaScript getters, prop2=p1, (since no parens).

I could probably add support for the unbound case, but I'm not sure if it is possible for the JavaScript getters.

First thing of course is to make sure the current update does not have significant bugs or breaking changes, since it was a fairly significant rewrite. So I look forward to hearing whether you do hit any important issues a that level. Currently my own tests all pass.

If all that looks good, then we could discuss the relative importance of adding caching for additional scenarios...

(BTW - we have collaborated a lot, and your input has be very valuable, but I don't even know in what country you live, or what site or apps you have been building using JsViews. If you want to reply you can email me direct at email, borismoore@gmail.com. :-)...)

johan-ohrn commented 4 years ago

I found an issue. It's not from the recent cache-implementation but from a previous fix I believe. Check this out:


$.views.tags({mytag: {
 bindTo:['prop'],
 init: function() {
  //debugger;
  window.mytag = this;
  setTimeout(this.test.bind(this))
 },
 setValue:function(x) { console.log("setValue", x) },
 test: function() {
  // write back changes to the viewmodel
  this.updateValue(!this.tagCtx.props.prop, 0);
  console.log("vm.value", vm.value); // should be false - OK!
  // setValue will not be called during the setProperty call unless we do this
  delete this.tagCtx._bdArgs[0];

  // simulate the view model being modified externally - the tag should react to the change and call setValue
  $.observable(vm).setProperty('value', !vm.value);
 }
}});
var vm = {value:true};
$.views.templates("{^{mytag prop=value /}}").link('body', vm)

Notice how I must manually call delete this.tagCtx._bdArgs[0]; to keep the internal state of the tagCtx in sync with the external data.

BorisMoore commented 4 years ago

Thanks Johan. I haven't yet found a fix for that one (other than reverting an earlier proposed fix for avoiding unnecessary multiple calls to setValue). Not sure if I can find an ideal fix, here. I'll let you know of any progress...

johan-ohrn commented 4 years ago

From my understanding the internal array _bdArgs is meant to track the state of the bind args to determine which of multiple parameters has changed. So isn't the solution that the updateValue function should also update _bdArgs to keep it in sync? I.e.:

function updateValue(newValue, bindIndex) {
  ...
  this.tagCtx._bdArgs[bindIndex] = newValue;
}
BorisMoore commented 4 years ago

It's actually a bit more complex, I think.

I'll try and clarify the issue as I see it, when I have investigated a bit further. Thanks...

BorisMoore commented 4 years ago

Hi Johan, sorry not to have responded sooner on this.

The behavior of setValue() was changed in v1.0.6, following our discussions about multiple/inappropriate calls. https://github.com/BorisMoore/jsviews/issues/440#issuecomment-587498386  

With that update setValue(x) is only called when the corresponding data value, x, is different from the last call. So for example if the setValue handler on a slider control moves the slider handle to the appropriate position for x, then setValue won't be called again until there is a new modified value of x - so it will not unnecessarily re-render the previous handle position.

In your example, the initial call to the setValue handler is setValue(true). The call to updateValue(false) will change the view model value to false. Calling setProperty() is then setting the view model value back to true. But this does not trigger a call to setValue(true), since it was called with the value true previously, so this is not a new value.

A common pattern is for a tag implementation to call both updateValue() and setValue() - in order to update the 'external' view model and also make the corresponding change to the tag itself - such as moving the slider handle to the corresponding position. (See for example the slider (moveTo: function(x)) and color picker (function updateHslaValues(tag, hsla))  

In your example, you could use that pattern, and add a setValue() call, with the same value (false) you are passing to updateValue():

test: function() {
  // write back changes to the viewmodel
  this.updateValue(false, 0);

  // provide corresponding change to tag itself
  this.setValue(false, 0);

  // simulate the view model being modified externally
  // the tag should react to the change and call setValue,
  // but only if the new value is different from the value 
  // provided to setValue in the previous call.
  $.observable(vm).setProperty('value', true);
}

Now the setProperty change will indeed trigger a call to setValue(true) since now the previous value passed to setValue was false, and the new value, true, is different:

Console output:

setValue true (initial call)
setValue false (this.setValue() call)
setValue true (setProperty call)
johan-ohrn commented 4 years ago

Ah yes it work now. Thanks.

I find it somewhat complex and not immediately straight forward. In fact I'm having a hard time expressing my gripes at all so please bare with me. I just assume that if I'm struggling then others might as well so all this is just feedback for your consideration.

setValue is a callback function for when a bound property value changes. This is clear as glass. setValue also executes "hidden logic" to update the tags internal state with the updated values. This is not obvious:

myTag: {
  setValue: function() {
  }
  someFn: function() {
    this.setValue(...); 
  }
}

Reading the code it looks like this should just execute setValue directly but in reality jsviews has overridden setValue and also update this internal state. Or perhaps it's my implementation that's actually overriding a base setValue method?

Next is the symmetry of calling updateValue(...) from the tag and $.observable(...).setProperty(...) externally. Both updates the external view model and both update the tagCtx.props.xxx property with the new value and the external setProperty call also triggers setValue to be invoked.

Like I said it looks like setValue is just a simple callback - not something that's critical for the tag to function. I find this dual nature of setValue hard to grasp because it puts extra responsibility on me as a developer to call it myself to update the tags internal state.

Could setValue as a callback and setValue as jsviews logic be separate functions? I.e. setValue is the jsviews logic and onSetValue is the callback... That way calling updateValue from within the tag could also automatically call setValue to update the tags internal state without triggering onSetValue. And calling setProperty from externally could call both setValue and trigger onSetValue.

Do any of this make sense or am I just rambling? :)

BorisMoore commented 4 years ago

Thanks Johan. I'll be getting back to you on this soon.

BorisMoore commented 4 years ago

I did some investigation, in order to reply to your discussion and comments above, and I actually hit the same issue that you called out above.

I was using this Tabs Control jsfiddle as an example (and I included a linkedElement and linkedCtxParam for testing purposes).

The sample has the following setValue/setTab implementation.

  setValue: function(val, index, elseTag) {
    console.log("setValue " + val);
    if (!elseTag) {
      $.observable(this).setProperty("pane", +val); // Update tag.pane
    }
  },

  setTab: function(index) {
    // OnClick for a tab
    this.setValue(index); // Update UI: select tab pane 'index' and linkedElem
    this.updateValue(index); // Update external data, through two-way binding
  }

which works correctly, and uses the pattern we discussed of calling both this.updateValue() and this.setValue() from the setTab handler.

But if you replace the setTab code by the following (which does not respect that pattern) then you can hit a similar issue to the one you called out:

  setTab: function(index) {
    // OnClick for a tab
    $.observable(this).setProperty("pane", index); // Update tag.pane
    this.updateValue(index); // Update external data, through two-way binding
  }

To hit the issue, first click on the 'second' tab, then change the linked data (select) value back to 0 using the 'Select' text box.

The tab does not switch back as expected to the 'first' tab. This is basically the same scenario that you had called out.

And I agree that this is not very easy for a user/developer to understand. So I did some more investigating of the JsViews implementation, to try to fix this issue. Here is a proposed fix: jsviews5.zip

With this JsViews update the modified sample now works correctly, even though not calling this.setValue() from the setTab handler.

Note that if your control also uses linkedElement, then you do need to call this.setValue() from setTab in order to trigger the update on the linkedElement. (Indeed that is one of the ways in which calling this.setValue() does more than just call your setValue implementation).

So let me know if this fix works for your scenario, and allows you not to include that this.setValue() call.

And in relation to your comments above, I agree with some of your concerns. Yes, setValue is used to refer both to the method and the handler, so that might lead to confusion. I considered calling the handler onSetValue, but finally opted for the 'simplicity' of using a single name for both.

One thing to bear in mind, concerning the symmetry between setValue and updateValue is that the design also needs to work with two-way binding using distinct bindFrom and bindTo targets. Some of your suggestions for simplifying would only be possible if we require bindFrom and bindTo to be the same. See https://www.jsviews.com/#tagoptions@bindfrom and the sample https://www.jsviews.com/#samples/tag-controls/colorpicker@bindfrom.

See also https://www.jsviews.com/#bindingpatterns@setvalue-updatevalue.

johan-ohrn commented 4 years ago

Just to give you a quick update. I haven't had time to experiment with your latest update yet. I'll try to get to it in the coming days. Cheers! Sorry for being so slow :)

BorisMoore commented 4 years ago

Thanks, Johan. In fact I have been preparing to publish as v1.0.7. So I will probably go ahead without your results, unless you end up managing to check it out sooner. It is probably good though...

BorisMoore commented 4 years ago

This has been fixed in new release v1.0.7

johan-ohrn commented 3 years ago

I'm late to the party but I just wanted to give you feedback on the latest change you made regarding setValue(). It does indeed work for my scenario!

I also found a small issue with the new expression cache. See the following fiddle. Run it with the console open. You'll see it printing the message to the console two times instead of just one. If you uncomment the debugger statement in the log helper function and step back in the call stack you can see that the getCache method is called with key="~log(\'cache fails for 2nd lookup\')" the first time and key="~log('cache fails for 2nd lookup')" the second time. Notice the backslashes.

BorisMoore commented 3 years ago

Thanks Johan. I'll take a look at the escaping issue. I'm glad the setValue() update is good for your scenario....

BorisMoore commented 3 years ago

Hi, Johan

I have a fix for the escaping issue - a one-liner changing this line https://github.com/BorisMoore/jsviews/blob/master/jquery.views.js#L3879 to:

linkExprStore[params] = linkFn = $sub.tmplFn(params.replace(rEscapeQuotes, "\\$&"), view.tmpl, true);

Here it is with the other files:

jsviews8a.zip

Let me know if you see any issues....

Thanks

johan-ohrn commented 3 years ago

Great thanks! I'll try it out on Monday when I'm back from vacation ;(

johan-ohrn commented 3 years ago

The fix for the quotes work like a charm.

I found an other issue where the cache is cleared mid-render due to the global cache counter being increased by a call to $.observable.setProperty. See this fiddle. Open the console and see how doSomething is being called twice. Both this.prop = 1 and $.observable(this).setProperty('prop', 1) has the effect that "1" is being rendered in the same render cycle. The observable update however invalidates the cache and so doSomething is called twice.

It would be nice if setProperty calls "inside" or "during" a render cycle does not increase the counter. I.e. only after the outermost render has completed may the cache counter increase.

BorisMoore commented 3 years ago

We could do your suggestion of preventing cache clearing during the rendering process. Here is a potential update which implements that.

https://github.com/BorisMoore/jsviews/files/5065529/jsviews8b.zip

However I have a concern that we may be privileging 'performance' over 'correctness'.

Take for example the following sample

<div id="result"></div>

<script id="myTmpl" type="text/x-jsrender">
  {^{:bar}}
  {^{:getBar()}}
|
  {^{incrementBar/}}
  {^{:bar}}
  {^{:getBar()}}
|
  {^{incrementBar/}}
  {^{:bar}}
  {^{:getBar()}}
</script>

<script>
  $.views.tags("incrementBar", {
    render: function() {
      $.observable(data).setProperty("bar", ++ct);
      return ct;
    }
  });

  var data = {
    bar: 1,
    getBar: function(){
      return this.bar;
    }
  },

  ct = 1,
  tmpl = $.templates("#myTmpl");

  tmpl.link("#result", data);
</script>

This will output the following under the current JsViews version (1.0.7):

1 1 | 2 2 2 | 3 3 3

but with the updated JsViews above it will output the following:

1 1 | 2 2 1 | 3 3 1

Do you have strong scenarios that would justify making this change? It is true that the correctness issue may not arise very easily or naturally in real-life situations. But conversely, there may be few real-life situations where observable update calls happen during render and the resulting emptying of the cache somehow creates a problem. In fact additional calls to non-cached computed functions may actually be appropriate, rather than problematic, as in my somewhat contrived example above.

Thoughts?

johan-ohrn commented 3 years ago

I agree that the output in your example may prove to be problematic from a larger perspective. I feel that in the example you provided the template code does not do what you'd expect:

  {^{:bar}}
  {^{:getBar()}}
|
  {^{incrementBar/}}
  {^{:bar}}
  {^{:getBar()}}
|
  {^{incrementBar/}}
  {^{:bar}}
  {^{:getBar()}}

In my mind I want to translate this into javascript function something like this:

void render() {
  var bar = 1;
  print(bar);  // 1
  print(bar);  // 1
  print("|");
  print(++bar);   // 2
  print(bar);  // 2
  print(bar);  // 2
  print("|");
  print(++bar);  // 3
  print(bar);  // 3
  print(bar);  // 3
}

So an output of 1 1 | 2 2 1 | 3 3 1 definitively feels wrong.

My template on the other hand also doesn't execute like I would expect:

    {^{include ~scs=doSomething(12345) }}
    {^{if ~scs}}
    blah
    {{/if}}
    {{/include}}

In my mind I want to translate this into javascript function something like this:

void render() {
  var scs=doSomething(12345);
  if (scs) {
    print("blah");
  }
}

So when the doSomething method is invoked multiple times it definitely does not behave as I would expect.

Would it perhaps instead be possible to cache the context parameter so that it's evaluated one time and that value is used when the context parameter is referenced? Instead of evaluating the expression each time the context parameter is referenced?

My desire to have this caching is driven by observed performance problems in our code base where expensive code is executed multiple times and/or templates re-render needlessly. I have no contrived example I could show that demonstrates this in effect. It doesn't take a lot to make the UI non-responsive. Hence my obsession with performance.

BorisMoore commented 3 years ago

The pseudo code you suggest for rendering {^{include ~scs=doSomething() }}...{{/include}} is close to what does happen when using tmpl.render() to render the template (so JsRender only, with no data-binding/linking and no observable updates). The description here: https://www.jsviews.com/#contextualparams corresponds to that scenario. doSomething is in that case called only once, and the references to the contextual parameter, such as {{if ~scs}} or {{:~scs}} will simply insert the value returned from that call.

However, when using JsViews, and data-linking, the connection between the definition ~scs=someexpression and the references ~scs is much more than just passing the value. In fact it sets up a link between the reference and the somexpression which supports dynamic updating in both directions (two-way binding). It is a bit as if the reference provides 'proxy' access to the outer observable context, for evaluating someexpression. That's partly why it is called a 'contextual parameter'. It's true that the documentation of this is not so extensive for the general case of contextual parameters, but See https://www.jsviews.com/#bindingpatterns@ctxparams. The fuller explanation is in the sections documenting linked contextual parameters.

Here is a sample which shows the dynamic interplay between the references and the definition: https://jsfiddle.net/BorisMoore/f782zact/.

With version 1.0.7 in this example getBar() is called twice, once for bar=1 and a second time after bar is updated observably during rendering, by {{incrementBar/}}. But in some ways the observable call during rendering is not a typical scenario, since the render call will generally be without side effects, and the template will not be expected to depend on document order rendering. For example if you include a <span data-link="bar"></span> before the {{incrementBar/}}, it will update after the initial rendering, during the linking phase, with the value 2.

In your environment, what are reasons for observable updates being called during reendering. Are they within your own functions or helpers, or is it from JsViews features such as sort and filtering on {{for}}?

I am pretty sure I want to stay with version 8a, above, rather than move to the 8b semantics.

johan-ohrn commented 3 years ago

Unfortunately I don't have the actual code any longer since I worked around the issue. But here is an example of what I was doing which illustrates how setProperty might be called in the middle of a render cycle. What the demo tries to illustrate is some sort of panel being made visible by the user and so the template renders. The template calls a function to lazy load data that it want's to display. If the data is already cached as in this example it calls setProperty without delay in the middle of a render cycle.

If it's possible to do something about this it then it would be great. I do agree however that the 8b solution is not it because it could break things.

BorisMoore commented 3 years ago

Worst case, in that not very common scenario you will get a refreshed copy of cached functions. If that is a performance issue, you can presumably do your own caching of the function, or of the slow part of the function as a secondary call.

But I won't do more to add built-in support for this, since I think the scenario (along with significant associated performance cost) is not common enough to warrant additional work, or the resulting additional code/complexity. Does that make sense?

johan-ohrn commented 3 years ago

It makes sense. I was hoping for some easy fix.

So my next question: would it be easy enough to extend the cache to non-linked tags? You hinted at it earlier :)

BorisMoore commented 3 years ago

It looks as if would not be very easy, and would bring also some additional complexity (and perhaps resulting small perf hit during compilation of the templates). And it would bring risk of introducing new bugs, or destabilizing the current implementation.

In fact if you have

{{:~hlp('foo') }}
{{:~hlp('foo') }}

then hlp() gets called twice (whether you are using JsRender render() method or JsViews link() method. But in either case if you want you can instead write

{^{:~hlp('foo') }}
{^{:~hlp('foo') }}

and hlp() will get called only once, thanks to caching.

Similarly if you have

{{include ~f=~hlp('foo') }}
    {{:~f}}
    {{:~f}}
{{/include}}

hlp() will be called twice, but if you write instead:

{^{include ~f=~hlp('foo') }}
    {{:~f}}
    {{:~f}}
{{/include}}

then it will be called only once.

So one could say that there is a simple workaround, (though not very discoverable).

Also, I don't have time for investing significant time into JsViews at the moment, other than for major concerns or bugs...

So I would vote right now for leaving as is...