I'm trying to understand/implement two way attribute binding in a Polymer 3 web component. I've got the following code:
import {html, PolymerElement} from '@polymer/polymer/polymer-element.js';
class CustomInputComponent extends PolymerElement {
static get template() {
return html`
<div>
<template is="dom-repeat" items="{{ratings}}">
<input type="radio"
name="group"
id="item_{{item.id}}"
value="{{item.checked}}"
checked$="{{item.checked}}">
</template>
</div>`;
}
static get properties() {
return {
ratings: {
type: Array,
value: [
{ id: 1, checked: true },
{ id: 2, checked: false }
]
}
};
}
}
window.customElements.define('custom-input-component', CustomInputComponent);
As you can see, I have defined a Array property containing a default list of values. This property is a model from which I want to render a radio group. The initial state looks good. But when I click on the unchecked input, the DOM elements don't update correctly.
I'd bet I'm missing something really simple...
The main things are:
You are binding to the checked
attribute ($=
), however I don't think radio inputs dynamically update their checked
attribute. AFAICT, the checked
property is what changes when the input gets selected.
Also, native <input type="radio">
inputs will only fire their change
and input
events when they are selected, not when they are de-selected. Polymer relies on events to trigger property effects like data bindings; this means that an upwards data binding (from the input to your custom element) will only get processed when the checked
property on an input changes from false
to true
. Effectively, once ratings[n].checked
becomes true, it will never be made false because Polymer has no way to know that this has occurred.
Incidentally, to perform two-way binding on a native HTML element, you would also need to include an annotation for the event that the radio input fires when it is selected. So if you did want to capture the changes on selection, it'd be something like checked="{{item.checked::change}}"
.
A couple of options:
Use paper-radio-button
s inside a paper-radio-group
instead of native <input>
s. These elements behave well for two-way data binding.
Listen for the change when a new item gets checked, and manually update ratings[n].checked
to false
for the previously selected item.
A couple more things about your code
(I don't think this is anything to do with your current problem, but it will help avoid future problems) when initializing a default value for an object or array property in a Polymer element, remember to use a function so that each element instance gets its own unique array or object. E.g.:
ratings: {
type: Array,
value: function(){
return [
{ id: 1, checked: true },
{ id: 2, checked: false }
];
}
}
Normally I think, you wouldn't want to change the values of your radio inputs. Conventionally, when the <form>
containing a radio input group is submitted, the value
on the radio input that is currently checked
gets included with the form data, and the other radio input values are ignored. Here's an example on W3Schools. So instead of value="{{item.checked}}"
, something like value="[[item.data]]"
.
So the whole thing might be something like
class CustomInputComponent extends PolymerElement {
static get properties () {
return {
ratings: {
type: Array,
value: function(){
return [
{ id: 1, checked: true, value: 'pie' },
{ id: 2, checked: false, value: 'fries' },
{ id: 3, checked: false, value: 'los dos' }
];
}
},
selected: {
// type: Number or Object, idk
// Keep track of the selected <input>
}
};
}
static get template() {
return html`
<p>do you want pie or fries?</p>
<div id="mydiv">
<template is="dom-repeat" items="{{ratings}}">
<input
type="radio"
name="testgroup"
id="[[item.id]]"
value="[[item.value]]"
checked="{{item.checked::input}}"
on-change="radioChanged"
>[[item.value]]
</input>
</template>
</div>
`;
}
radioChanged(event){
// update ratings[n].checked for selected input
// set selected input to currently selected input
}
}