BorisMoore / jsviews

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

Is it possible to bind an object directly? #427

Closed hkvstore closed 4 years ago

hkvstore commented 5 years ago

I tried the follows:

<script id="myTmpl" type="text/x-jsrender">
  Name (as JSON): <input data-link="{toJson:name:fromJson}"/><br/><br/>

  First: <input data-link="name.first"/>{^{:name.first}}<br/>
  Last: <input data-link="name.last"/>{^{:name.last}}
</script>

<div id="page"></div>
var myTmpl = $.templates("#myTmpl"),
  data = { name: { first: "Jo", last: "Blow" } };

$.views.converters({
  toJson: function(val) {
    return JSON.stringify(val);
  },
  fromJson: function(val) {
    return JSON.parse(val);
  }
});

myTmpl.link("#page", data);

When the page is run, the "Name" textbox can display JSON string, but subsequent changes to the "First" or "Last" text box will not change the value in the "Name" textbox.

Is it possible to bind an object directly? (I understand I can use JsObservable to observe the name but I wonder if I can do it with JsViews only.)

johan-ohrn commented 5 years ago

To explain what you're doing wrong consider the following template:

<script id="myTmpl" type="text/x-jsrender">
  Name (as JSON): <input data-link="{toJson:name || name.first || name.last:fromJson}"/><br/><br/>

  First: <input data-link="name^first"/>{^{:name^first}}<br/>
  Last: <input data-link="name^last"/>{^{:name^last}}
</script>

<div id="page"></div>

There are a couple of issues with your code. Your fromJson converter creates and returns a new object. It does not modify properties on an already existing object. What this means is that when you type into the json textbox your converter will be called and it will return a completely new object. This is why you need to use the name^first expression for your first and last textbox. The ^ character makes the expression react to replacing the name object as well as modifying the property.

Typing into the first or last textbox modifies those properties on the name object but your json textbox does not monitor those properties. It's looking to see if the name object itself is replaced by a new instance which it's not. A hackish solution I just showed you just includes the first and last properties in the expression to trick the template into updating when the properties change as well but return the actual name object.

So basically you're not keeping track on which objects and properties are being monitored and updated by each data link expression.

I hope this explains why you have these issues.

hkvstore commented 5 years ago

Thank you for your explanation. What I'm trying to do was to use JsViews to modify a JSON string with form controls. What will be the best solution if the object has a lot of properties and it becomes hard to put them all in the expression to trick the template?

johan-ohrn commented 5 years ago

You could use a computed observable instead of the converter. Try this:

<script id="myTmpl" type="text/x-jsrender">
  Name (as JSON): <input data-link="json()"/><br/><br/>

  First: <input data-link="name.first"/>{^{:name.first}}<br/>
  Last: <input data-link="name.last"/>{^{:name.last}}
</script>

<div id="page"></div>
var myTmpl = $.templates("#myTmpl"),
  data = { name: { first: "Jo", last: "Blow" } };

  // create a computed observable
  data.json = function() {
    return JSON.stringify(data.name);
  }
  data.json.set = function(val) {
    var x = JSON.parse(val);
    // observably update the individual properties on the name object
    $.observable(data.name).setProperty(x);
  }
  // make the computed observable depend on whatever properties are on the name object
  data.json.depends = ['name.first', 'name.last'];

myTmpl.link("#page", data);

The the idea is to attach all logic to the computed property "json". In this example the get function will be invoked as a result of either "first" or "last" properties being changed as expressed by the data.json.depends = ['name.first', 'name.last']; statement.

Typing into the json textbox will call the set function which will parse the string into a javascript object and the setProperty function will (when passed an argument of type "object") observably copy the values from the properties of this temporary object to the already existing name object. This is what updates the first and last textboxes.

johan-ohrn commented 5 years ago

@BorisMoore as discussed in this issue it was stated that parens at the leaf node is optional for depends and observe paths. Does this apply to data-link expressions in templates as well? This template work correctly <input data-link="json()"/> but <input data-link="json"/> does not. Here's a fiddle. Type into the name textboxes and the second json textbox to see the effect.

hkvstore commented 5 years ago

I understand I can use JsObservable to observe the name but I wonder if I can do it with JsViews only.

Problem of above suggestions is, it is better not necessary to list each property that would trigger changes.

johan-ohrn commented 5 years ago

Use Object.keys(name) to get the list of property names to make it dynamic like so data.json.depends = Object.keys(data.name).map(function(x){return "name."+x});

hkvstore commented 5 years ago

Or data.json.depends = ['name.*'];

johan-ohrn commented 5 years ago

Hah how simple. I didn't even consider using '*' . Excelent!

BorisMoore commented 5 years ago

@johan-ohrn and @hkvstore - thanks for your discussion above.

Johan, in response to your question, above, parens are required, in the case of data-link="someComputed()".

If you don't put the parens, on a <span data-link="someComputed">for example, then it renders as someComputed.toString(). However<input data-link="someComputed"> will evaluate someComputed() because jQuery detects that it is a function, just in the case of <input> and evaluates it. So it is a spurious behavior...

if ( isFunction ) {
    args[ 0 ] = value.call( this, index, self.html() );
}
BorisMoore commented 4 years ago

I'll close this now...