Polymer 1.0 'array-style' path accessors,

2019-02-19 17:32发布

The Polymer 1.0 documentation states:

The path syntax doesn’t support array-style accessors (such as users[0].name). However, you can include indexes directly in the path (users.0.name).

How would one get around this in setting the path dynamically, and obtain the same behavior as the following example using Polymer 0.5? This is specifically in the context of generating forms for a model defined by an Object.

<template repeat="{{row in fieldset.rows}}">
<div layout horizontal flex>
    <template repeat="{{field in row}}" flex>
        <paper-field field="{{model.fields[field]}}" value="{{obj[field]}}">
        </paper-field>
    </template>
</div>
</template>

edit:

Per https://github.com/Polymer/polymer/issues/1504:

No near-term plans to support this. Polymer 0.5 had a complex expression parser used for bindings that we have eliminated for simplicity and performance. There are alternate patterns you can use today to achieve similar results that just require you to be more explicit.

What the alternate pattern would be to achieve two way data binding remains unclear.

2条回答
Animai°情兽
2楼-- · 2019-02-19 17:58

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.

One-way data-binding

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:

<my-element value="[[getValue(obj, key)]]"></my-element>
getValue: function(obj, key) {
  return obj[key];
}

Two-way data-binding

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:

    • The row it will affect
    • The field it represents

Therefore, first let's create a repeating template which will use a new custom element that we will call basic-field:

<div class="layout horizontal flex">
  <div>-</div>
  <template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
      <div class="flex">[[item.name]]</div>
  </template>
</div>
<template is="dom-repeat" items="{{fieldset.rows}}" as="row" index-as="rowIndex">
  <div class="layout horizontal flex">
    <div>[[rowIndex]]</div>
    <template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
      <basic-field class="flex" field="[[item]]" row="{{row}}"></basic-field>
    </template>
  </div>
</template>

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

<dom-module>
  <template>
    <input value="{{value}}">
  </template>
</dom-module>

<script>
  Polymer({
    is: 'basic-field',
    properties: {
      row: {
        type: Object,
        notify: true
      },
      field: {
        type: Object
      },
      value: {
        type: String
      }
    }
  });
</script>

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 path
    • base - 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 <input> element to {{value::input}}:

<input value="{{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 <input> 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 <input> element to the basic-field element):

<link rel="import" href="components/polymer/polymer.html">

<dom-module id="basic-field">
  <style>
    input {
      width: 100%;
    }
  </style>
  <template>
    <input value="{{value::input}}">
  </template>
</dom-module>

<script>
  Polymer({
    is: 'basic-field',
    properties: {
      row: {
        type: Object,
        notify: true
      },
      field: {
        type: Object
      },
      value: {
        type: String,
        observer: '_valueChanged'
      }
    },
    observers: [
      '_dataChanged(row.*, field)'
    ],
    _dataChanged: function(rowData, field) {
      if (rowData && field) {
        var value = rowData.base[field.prop];
        if (this.value !== value) {
          this.value = value;
        }
      }
    },
    _valueChanged: function(value) {
      if (this.row && this.field && this.row[this.field] !== value) {
        this.set('row.' + this.field.prop, value);
      } 
    }
  });
</script>

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:

<link rel="import" href="components/polymer/polymer.html">
<link rel="import" href="components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="basic-field.html">

<dom-module id="fieldset-editor">
  <style>
    div, basic-field {
      padding: 4px;
    }
  </style>
  <template>
    <div class="layout horizontal flex">
      <div>-</div>
      <template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
          <div class="flex">[[item.name]]</div>
      </template>
    </div>
    <template is="dom-repeat" items="{{fieldset.rows}}" as="row" index-as="rowIndex">
      <div class="layout horizontal flex">
        <div>[[rowIndex]]</div>
        <template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
          <basic-field class="flex" field="[[item]]" row="{{row}}"></basic-field>
        </template>
      </div>
    </template>
    <pre>[[_previewFieldset(fieldset.*)]]</pre>
  </template>
</dom-module>

<script>
  Polymer({
    is: 'fieldset-editor',
    properties: {
      fieldset: {
        type: Object,
        notify: true
      }
    },
    _previewFieldset: function(fieldsetData) {
      if (fieldsetData) {
        return JSON.stringify(fieldsetData.base, null, 2);
      }
      return '';
    }
  });
</script>

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:

<fieldset-editor fieldset="{{fieldset}}"></fieldset-editor>

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.

查看更多
Deceive 欺骗
3楼-- · 2019-02-19 18:04

You can make a computed binding. https://www.polymer-project.org/1.0/docs/migration.html#computed-bindings

<paper-field field="{{_computeArrayValue(model.fields, field)}}" value="{{_computeArrayValue(obj, field}}"></paper-field>

<script>
  Polymer({
    ...
    _computeArrayValue: function(array, index) {
      return array[index];
    },
    ...
  });
</script>

As an aside, you also need to update your repeat to dom-repeat https://www.polymer-project.org/1.0/docs/devguide/templates.html#dom-repeat

Edit: Here is my ugly solution to the 2-way binding. The idea is that you have a calculated variable get the initial value and then update this variable upon updates with an observer.

<!-- Create a Polymer module that takes the index and wraps the paper field-->
<paper-field field="{{fieldArrayValue}}" value="{{objArrayValue}}"></paper-field>

<script>
  Polymer({
    ...
    properties: {
            fields: { //model.fields
                type: Array,
                notify: true
            },
            obj: {
                type: Array,
                notify: true
            },
            arrayIndex: {
                type: Number,
                notify: true
            },
            fieldArrayValue: {
                type: String,
                computed: '_computeInitialValue(fields, number)'
            },
            objArrayValue: {
                type: String,
                computed: '_computeInitialValue(obj, number)'
            }
        },
    _computeInitialValue: function(array, index) {
      return array[index];
    },
    observers: [
            'fieldsChanged(fields.*, arrayIndex)',
            'objChanged(fields.*, arrayIndex)'
    ],
    fieldsChanged: function (valueData, key) {
       this.set('fieldArrayValue', this.fields[this.arrayIndex]);            
    },
    objChanged: function (valueData, key) {
       this.set('objArrayValue', this.obj[this.arrayIndex]);            
    },
    ...
  });
</script>

Edit 2: Updated the code in Edit 1 to reflect the obeserver changes pointed out by Vartan Simonian

查看更多
登录 后发表回答