-
Notifications
You must be signed in to change notification settings - Fork 130
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
Comments
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. |
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
If 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 Here is a modified version of jsviews.js in which I have made a few changes that may help your scenarios: 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: 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:
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... |
@johan-ohrn : The attached fix above also includes the fix for #439, so both aspects are available for review... |
Sorry for my slow response. I've been overloaded lately and haven't gotten around to test until now. |
Here they are: download.zip |
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! |
I'm away for the holidays. Will be back early January. Cheers! |
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.
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. Given the areaSlider example. Is there a working pattern I could use to get notified when only the X argument is changed? |
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... |
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... |
Great I will give it a try! I'll let you know what I find. |
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). |
I did some testing. I found a situation where the setValue function is called even though it shouldn't.
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:
If only the property used in the setProperty call are initialized with a value then it doesn't work:
|
Excellent, thanks for your thorough testing work! Well I think I have a version that also fixes that scenario. Here it is: Let me know if you this one works for you... |
Thanks! However I'd like to take it a step further if possible. Consider the following.
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. It's possible to work around this by computing the value in the view model like this:
Now it's possible to observably update p1 and p2 separately and setValue will only be called when the respective property is changed. From analyzing the call stack I can see that
Now it becomes obvious why it's working the way it does. All properties are executed immediatelly
Now 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:
|
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 But the This would break a lot of user code. For example it seems to me that your mytag template above: The line As I say, maybe I am missing something, so please let me know if that is the case.... |
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...
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...
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...
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...
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)) |
You understood this part correctly: This however is not what I meant: 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
What I want to do with the lazy getter functions is instead this:
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? |
Perhaps better than getter functions which require () to invoke might be to use real javascript getters. Turning the original
into
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. |
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. 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. :-)...) |
I found an issue. It's not from the recent cache-implementation but from a previous fix I believe.
Notice how I must manually call |
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 my understanding the internal array _bdArgs is meant to track the state of the bind args to determine which of multiple parameters has changed.
|
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... |
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 In your example, the initial call to the setValue handler is 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 ( In your example, you could use that pattern, and add a setValue() call, with the same value (
Now the setProperty change will indeed trigger a call to Console output:
|
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. setValue is a callback function for when a bound property value changes. This is clear as glass.
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? :) |
Thanks Johan. I'll be getting back to you on this soon. |
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.
which works correctly, and uses the pattern we discussed of calling both 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:
To hit the issue, first click on the 'second' tab, then change the linked data ( 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: With this JsViews update the modified sample now works correctly, even though not calling Note that if your control also uses linkedElement, then you do need to call So let me know if this fix works for your scenario, and allows you not to include that 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 See also https://www.jsviews.com/#bindingpatterns@setvalue-updatevalue. |
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! |
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... |
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...
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...
This has been fixed in new release v1.0.7 |
I'm late to the party but I just wanted to give you feedback on the latest change you made regarding setValue(). 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. |
Thanks Johan. I'll take a look at the escaping issue. |
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:
Here it is with the other files: Let me know if you see any issues.... Thanks |
Great thanks! |
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. 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. |
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
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? |
I agree that the output in your example may prove to be problematic from a larger perspective.
In my mind I want to translate this into javascript function something like this:
So an output of My template on the other hand also doesn't execute like I would expect:
In my mind I want to translate this into javascript function something like this:
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. |
The pseudo code you suggest for rendering However, when using JsViews, and data-linking, the connection between the definition 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 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 I am pretty sure I want to stay with version 8a, above, rather than move to the 8b semantics. |
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. 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. |
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? |
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 :) |
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
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
and hlp() will get called only once, thanks to caching. Similarly if you have
hlp() will be called twice, but if you write instead:
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... |
Thanks for commenting. |
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.
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.
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.
This is a stack trace as seen by setting a breakpoint in the
onProp2Change
handlersourceValue = linkFn(source, view, $sub);
which is responsible for re-evaluating all the bindings. In my case it looks like this:mergeCtxs(tag, sourceValue, forceUpdate);
$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:
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.
The text was updated successfully, but these errors were encountered: