Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
johan-ohrn opened this issue Oct 30, 2019 · 51 comments
Closed

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

johan-ohrn opened this issue Oct 30, 2019 · 51 comments

Comments

@johan-ohrn
Copy link

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)}}]
}
  1. onDataLinkedTagChange goes on to call mergeCtxs(tag, sourceValue, forceUpdate);
  2. 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.

@johan-ohrn johan-ohrn changed the title hard to determine which bound tag property has changed bound tag property changed handler called when it shouldn't Oct 30, 2019
@johan-ohrn johan-ohrn changed the title bound tag property changed handler called when it shouldn't linked tag property changed handler called when it shouldn't Oct 30, 2019
@BorisMoore
Copy link
Owner

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
Copy link
Owner

BorisMoore commented Dec 2, 2019

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
Copy link
Owner

@johan-ohrn : The attached fix above also includes the fix for #439, so both aspects are available for review...

@johan-ohrn
Copy link
Author

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
Copy link
Owner

Here they are: download.zip

@BorisMoore
Copy link
Owner

BorisMoore commented Dec 21, 2019

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
Copy link
Author

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

@johan-ohrn
Copy link
Author

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
Copy link
Owner

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
Copy link
Owner

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
Copy link
Author

Great I will give it a try!

I'll let you know what I find.

@BorisMoore
Copy link
Owner

BorisMoore commented Feb 14, 2020

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

I have included this fix in the new update that I uploaded to here: #442 (comment).

@johan-ohrn
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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 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().

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 added a commit to BorisMoore/jsviews.com that referenced this issue Feb 27, 2020
Some minor bug fixes, including:

- BorisMoore/jsviews#444
  linking of selected value on select element does not work
  correctly with all jquery versions

- BorisMoore/jsviews#442
  for tag with range and data-linked tr give error
  when removing object from array

- BorisMoore/jsviews#440
  linked tag property changed handler called when it shouldn't

- BorisMoore/jsviews#439
  observable ev.data.observeAll helper broken when path is "*"

Small corrections or improvements to documentation
and some additional unit tests...
BorisMoore added a commit to BorisMoore/jsviews.com that referenced this issue Feb 27, 2020
Some minor bug fixes, including:

- BorisMoore/jsviews#444
  linking of selected value on select element does not work
  correctly with all jquery versions

- BorisMoore/jsviews#442
  for tag with range and data-linked tr give error
  when removing object from array

- BorisMoore/jsviews#440
  linked tag property changed handler called when it shouldn't

- BorisMoore/jsviews#439
  observable ev.data.observeAll helper broken when path is "*"

Small corrections or improvements to documentation
and some additional unit tests...
BorisMoore added a commit to BorisMoore/jsviews.com that referenced this issue Feb 28, 2020
Some minor bug fixes, including:

- BorisMoore/jsviews#444
  linking of selected value on select element does not work
  correctly with all jquery versions

- BorisMoore/jsviews#442
  for tag with range and data-linked tr give error
  when removing object from array

- BorisMoore/jsviews#440
  linked tag property changed handler called when it shouldn't

- BorisMoore/jsviews#439
  observable ev.data.observeAll helper broken when path is "*"

Small corrections or improvements to documentation
and some additional unit tests...
BorisMoore added a commit that referenced this issue Feb 29, 2020
Some minor bug fixes, including:

- #444
  linking of selected value on select element does not work
  correctly with all jquery versions

- #442
  for tag with range and data-linked tr give error
  when removing object from array

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

- #439
  observable ev.data.observeAll helper broken when path is "*"

Small corrections or improvements to documentation
and some additional unit tests...
@BorisMoore
Copy link
Owner

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 #440 (comment))

@johan-ohrn
Copy link
Author

johan-ohrn commented Feb 29, 2020

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
Copy link
Author

johan-ohrn commented Feb 29, 2020

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.

@BorisMoore
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

johan-ohrn commented Jun 1, 2020

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
Copy link
Owner

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

BorisMoore commented Jun 21, 2020

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

@BorisMoore
Copy link
Owner

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
Copy link
Author

johan-ohrn commented Jul 7, 2020

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
Copy link
Owner

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 added a commit to BorisMoore/jsviews.com that referenced this issue Jul 12, 2020
Feature improvements for JsRender on Node.js:

  (See issue BorisMoore/jsrender#357)

- File based templates for JsRender on Node.js now allow
  absolute paths

  See the updated documentation topic:
  https://www.jsviews.com/#node/filetmpls@htmlfile

- The renderFile() method for JsRender on Node.js
  now allows passing of context and helpers as well
  as data

  See the new documentation topic:
  https://www.jsviews.com/#node/filetmpls@renderfilehlelpers

Performance optimization changes:

- BorisMoore/jsviews#448
  $.observe/$.unobserve performance degradation over time

- BorisMoore/jsviews#447
  removeView performance

Several bug fixes, including:

- BorisMoore/jsviews#446
  Observable change error, when loading jquery.observable.js before
  jsrender.js, and using contextual parameters and sorted {{for}}

- BorisMoore/jsviews#440
  linked tag property changed handler called when it shouldn't

- BorisMoore/jsviews#445
  jsrender.min.js.map truncated

Additional small corrections or improvements to documentation
and some additional unit tests...
BorisMoore added a commit that referenced this issue Jul 12, 2020
Feature improvements for JsRender on Node.js:

  (See issue BorisMoore/jsrender#357)

- File based templates for JsRender on Node.js now allow
  absolute paths

  See the updated documentation topic:
  https://www.jsviews.com/#node/filetmpls@htmlfile

- The renderFile() method for JsRender on Node.js
  now allows passing of context and helpers as well
  as data

  See the new documentation topic:
  https://www.jsviews.com/#node/filetmpls@renderfilehlelpers

Performance optimization changes:

- #448
  $.observe/$.unobserve performance degradation over time

- #447
  removeView performance

Several bug fixes, including:

- #446
  Observable change error, when loading jquery.observable.js before
  jsrender.js, and using contextual parameters and sorted {{for}}

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

- #445
  jsrender.min.js.map truncated

Additional small corrections or improvements to documentation
and some additional unit tests...
@BorisMoore
Copy link
Owner

This has been fixed in new release v1.0.7

@johan-ohrn
Copy link
Author

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
Copy link
Owner

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

@BorisMoore
Copy link
Owner

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
Copy link
Author

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

@johan-ohrn
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

johan-ohrn commented Aug 18, 2020

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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...

@johan-ohrn
Copy link
Author

Thanks for commenting.
Yes it's more of a "nice to have" than something really important. Better leave it as is if complexity is high and time is low.
I think you can close this issue now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants