Giter Club home page Giter Club logo

Comments (51)

BorisMoore avatar BorisMoore commented on July 17, 2024

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.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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)?

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

Here they are: download.zip

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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!

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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?

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

Great I will give it a try!

I'll let you know what I find.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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']}}]
})

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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?

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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.

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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

  • onDataLinkedTagChange detects which tag property was actually changed and removes unchanged properties to prevent re-evaluating those link expressions

to jsrender.js

  • The generated template/link functions create getter/setters on the props object. The idea with getter/setter is to facilitate lazy execution of user code.
  • the generated template/link function calls a function _viewCache which purpose is to cache the values from the link expressions to prevent evaluating the same user code multiple times during a render cycle.

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.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

Sounds good - thanks for helping...

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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, [email protected]. :-)...)

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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;
}

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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)

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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? :)

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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.

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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 :)

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

This has been fixed in new release v1.0.7

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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?

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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.

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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.

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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?

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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 :)

from jsviews.

BorisMoore avatar BorisMoore commented on July 17, 2024

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

from jsviews.

johan-ohrn avatar johan-ohrn commented on July 17, 2024

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.

from jsviews.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.