The Polymer 1.0 documentation states:
The path syntax doesn’t support array-style accessors (such as users[0].name). However, you can include indexe
Yes, it is true that Polymer 1.0 no longer supports myObject[key]
in binding expressions. However, in your particular use-case, there are ways to sidestep this problem.
It is fairly simple to overcome this limitation when it comes to one-way data-binding. Simply use a computed property that accepts both the object and the key in question:
getValue: function(obj, key) {
return obj[key];
}
In the case of two-way data-binding, it is still possible to create a functional alternative to the binding expression {{obj[key]}}
in Polymer 1.0. However, it will require taking into consideration the particular use-case in which you are hoping to implement the binding.
Taking into account the example in your question, it seems that you are doing work with some sort of table or fieldset. For the purposes of the example here, I will use a slightly different, yet very similar structure.
Let's assume that we have a fieldset
object, and that this object is structured as such:
{
"fields": [
{ "name": "Name", "prop": "name" },
{ "name": "E-mail", "prop": "email" },
{ "name": "Phone #", "prop": "phone" }
],
"rows": [
{
"name": "John Doe",
"email": "jdoe@example.com",
"phone": "(555) 555-1032"
},
{
"name": "Allison Dougherty",
"email": "polymer.rox.1337@example.com",
"phone": "(555) 555-2983"
},
{
"name": "Mike \"the\" Pike",
"email": "verypunny@example.com",
"phone": "(555) 555-7148"
}
]
}
If we want to create some sort of table-like output which represents this object, we can use two nested repeating templates: the first to iterate through the different rows, and the second to iterate through the different fields. This would be simple using the one-way data-binding alternative above.
However, achieving two-way data-binding is very different in this case. How is it do-able?
In order to understand how to come up with a solution, it is important to break down this structure in a way that will help us figure out what observation flow we should implement.
It becomes simple when you visualize the fieldset
object like this:
fieldset
--> rows (0, 1, ...)
--> row
--> fields (name, email, phone)
--> value
And becomes even simpler when you factor in the workflow of your element/application.
In this case, we will be building a simple grid editor:
+---+------+--------+-------+
| | Name | E-mail | Phone |
+---+------+--------+-------+
| 0 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
| 1 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
| 2 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
There are two basic ways that data will flow in this app, triggered by different interactions.
Pre-populating the field value: This is easy; we can easily accomplish this with the nested templates mentioned earlier.
User changes a field's value: If we look at the application design, we can infer that, in order to handle this interaction, whatever element we use as an input control, it must have some sort of way of knowing:
Therefore, first let's create a repeating template which will use a new custom element that we will call basic-field
:
-
[[item.name]]
[[rowIndex]]
(In the snippet above, you'll notice that I've also added a separate repeating template to generate the column headers, and added a column for the row index - this has no influence on our binding mechanism.)
Now that we've done this, let's create the basic-field
element itself:
We have no need to modify the element's field from within the element itself, so the field
property does not have notify: true
. However, we will be modifying the contents of the row, so we do have notify: true
on the row
property.
However, this element is not yet complete: In its current state, it doesn't do any of the work of setting or getting its value. As discussed before, the value is dependent on the row and the field. Let's add a multi-property observer to the element's prototype that will watch for when both of these requirements have been met, and populate the value
property from the dataset:
observers: [
'_dataChanged(row.*, field)'
],
_dataChanged: function(rowData, field) {
if (rowData && field) {
var value = rowData.base[field.prop];
if (this.value !== value) {
this.value = value;
}
}
}
There are several parts to this observer that make it work:
row.*
- If we had just specified row
, the observer would only be triggered if we set row
to an entirely different row, thus updating the reference. row.*
instead means that we are watching over the contents of row
, as well as row
itself.rowData.base[field.prop]
- When using obj.*
syntax in an observer, this tells Polymer that we are using path observation. Thus, instead of returning just the object itself, this means that rowData
will return an object with three properties:
path
- The path to the change, in our case this could be row.name
, row.phone
, etc.value
- The value that was set at the given pathbase
- The base object, which in this case would be the row itself.This code, however, only takes care of the first flow - populating the elements with data from the fieldset
. To handle the other flow, user input, we need a way to catch when the user has inputted data, and to then update the data in the row.
First, let's modify the binding on the element to
{{value::input}}
:
Polymer does not fully automate binding to native elements as it doesn't add its own abstractions to them. With just {{value}}
, Polymer doesn't know when to update the binding. {{value::input}}
tells Polymer that it should update the binding on the native element's input
event, which is fired whenever the value
attribute of the element is changed.
Now let's add an observer for the value
property:
value: {
type: String,
observer: '_valueChanged'
}
...
_valueChanged: function(value) {
if (this.row && this.field && this.row[this.field] !== value) {
this.set('row.' + this.field.prop, value);
}
}
Notice that we aren't using this.row[this.field.prop] = value;
. If we did, Polymer would not be aware of our change, as it does not do path observation automatically (for the reasons described earlier). The change would still be made, but any element outside of basic-field
that may be observing the fieldset
object would not be notified. Using this.set
gives Polymer a tap on the shoulder that we are changing a property inside of row
, which allows it to then trigger all the proper observers down the chain.
Altogether, the basic-field
element should now look something like this (I've added some basic styling to fit the element to the
basic-field
element):
Let's also go ahead and take the templates we've made before and throw them into a custom element as well. We'll call it the fieldset-editor
:
-
[[item.name]]
[[rowIndex]]
[[_previewFieldset(fieldset.*)]]
You'll notice we're using the same path observation syntax as we did inside of basic-field
to observe changes to fieldset
. Using the _previewFieldset
computed property, we will generate a JSON preview of the fieldset whenever any change is made to it, and display it below the data entry grid.
And, using the editor is now very simple - we can instantiate it just by using:
And there you have it! We have accomplished the equivalent of a two-way binding using bracket-notation accessors using Polymer 1.0.
If you'd like to play with this setup in real-time, I have it uploaded on Plunker.