BorisMoore / jsviews

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

Tag.updateValue does not update linkedElement #433

Closed johan-ohrn closed 5 years ago

johan-ohrn commented 5 years ago

Run this fiddle with the console open for a repro.

I'm calling updateValue('new value', 0) from the tag and the value is successfully written back to the viewmodel and tagCtx.props.prop is also updated. The actual input element that the tag argument is linked to is however not updated with the new value. The input element that is two-way bound is updated.

To me this seem like inconsistent behavior.

I notice that if I call setValue("new value!", 0) immediately after updateValue then the linked input element is also updated.

This leads me to wonder why can't updateValue implicitly call setValue or update the linked element on it's own? I assume there are implications if one would always follow a call to updateValue with a call to setValue?

I have a feeling this will boil down to me not understanding the symmetry of updateValue/setValue but to me they seem unintuitive and inconsistent. I'm kind of just wanting to "update this bound tag property" and I'm expecting that everything that is linked to that property is also updated accordingly.

johan-ohrn commented 5 years ago

While I was experimenting with updateValue/setValue I ran into an issue very similar to this issue. And here is a fiddle for the actual repro. The tag calls updateValue() on a named property which triggers a "nested" call to updateValue on an indexed argument. The nested updateValue correctly sets the 0-argument to the new value but immediately afterwards it's overwritten by the outer updateValue as it writes the named property.

BorisMoore commented 5 years ago

Yes, it is common to want to simultaneously update the external data binding, and the internal state of the control. See https://www.jsviews.com/#bindingpatterns@setvalue-updatevalue. (The slider, for example, which calls both updateValue and setValue).

While it could be convenient in some cases to be able to do both in a single call, the separating out as two calls provides better control, and avoids some cases where there could be circularity and stack overflows, or inappropriate re-rendering of a control (maybe leading to losing the focus/insertion point on a text box that is triggering the change, for example) as a result of a control updating itself.

For your new issue, above, yes, another excellent catch! I think I have a fix for it, here. Could you see if it works for you?:

download.zip.

BTW, on another topic, you might like to look at this stack overflow question: https://stackoverflow.com/questions/57261992/jsrender-vs-other-template-based-frameworks. I wondered if you would be interested (and have the time) to add a response, from your experience of JsRender and JsViews...)

johan-ohrn commented 5 years ago

The fix works perfectly! Thanks!

I also posted my thoughts on the SO question.

Regarding the initial question/issue/thing. I just want to give you some feedback to explain why I had these troubles. I think what trips me up is that I'm expecting, or was expecting before your explanations, that linkedElement behaves just like a two way binding.

From the documentation at https://www.jsviews.com/#tagoptions@linkedelement The linkedElement option together with the bindTo option make it easy to provide two-way data binding between such an element (or elements) and a tag argument or property. ... If the HTML element is a form element such as or , or a contenteditable element, then the data-link binding will be two-way.

To me this implies that these two should be quivalent:

myTag: {
  template: "<input>",
  linkedElement: ["input"],
  bindTo: ['prop']
}
myTag: {
  template: "<input data-link="{:~tagCtx.props.prop:}"",
  bindTo: ['prop']
}

In my mind both are two way bound. The first version in a more declarative manner in the tag. In the second version from the template. But updateValue('new value', 0) behaves differently where in the first version the element value is not updated but in the second version it is.

The difference in behaviour seem both inconsistent and non-intuitive because it goes against what I think I know about JsViews from previous experiences with two way bindings. I'm just talking from a learning point of view here. It goes against what I think I've learned even though I know now how to do it correctly.

BorisMoore commented 5 years ago

Thanks very much, Johan, for SO answer. Much appreciated... Thanks for taking the time to write that...

For the discussion about updateValue(), it seems like you had understood "data binding to a tag argument or property" differently than the meaning I had intended.

What I actually meant is "data binding to the data at the path specified in a tag argument or property". (But to express it that way sounds a bit complex or heavy). Maybe we can find a better wording, to explain it more clearly and accurately...?

If you have:

myTag: {
  template: "<input>",
  linkedElement: ["input"],
  bindTo: ['prop']
}

then {^{mytag prop=person.name/}} will be equivalent to <input data-link="person.name"/>.

So the input will be two-way bound not to tag.tagCtx.props.prop but to person.name.

In fact it is close to being equivalent to this:

myTag2: {
  init: function(tagCtx) {
    this.template = "<input data-link='" + tagCtx.params.props.prop + "'/>";
  },
  bindTo: ['prop'] // set value does nothing. update value does data bind
},

Your version, with template: "<input data-link='{:~tagCtx.props.prop:}'", will not have two-way binding back to the data (such as person.name).

As for updateValue() and setValue(), as mentioned here , they provide "a more generic programmatic approach to two-way data-binding of tag controls, particularly for cases where the data-binding does not correspond to data-linked form elements such as <input> or <select>", and so they separate out the code for updating the data (at the path specified in the tag argument or property) and code for updating the UI within the control if that external data changes, (for example for setting a selected tab on a tab control, or the handle position on a slider...).

For the myTag example above you have an <input/> linkedElem, so don't really need to use updateValue or setValue. The user will simply interact with the text box, and no code will be needed...

BorisMoore commented 5 years ago

I am going to close this now, since I am publishing an update, and don't want this to look like an active bug/issue. If you wanted to continue the discussion we can still do so as a closed issue.

johan-ohrn commented 5 years ago

Sorry for the late response. Seem to become a bad habit lately. I'm satisfied with your explanation. Like you said I interpreted the docs a little to literal. I just wanted to give you feedback where I thought there might be room for improvement. I regret I can't suggest anything better and it's a somewhat advanced topic. Anyway I understand your thoughts now that you explained more. Thanks.