I am trying to bind a Polymer property to the disabled attribute of a CheckBox, but the CheckBox only get disabled/enabled at the beginning.
I have to mention that the property I am binding to is a sub-property of a Polymer property.
Component's code:
<link rel="import" href="../polymer/polymer.html"/>
<dom-module id="checkbox-disabled-example">
<template>
<button
on-click="onClick" >
Toggle Disabled Enabled </button>
<template is="dom-repeat" items="{{elementsObject.items}}" as="item">
<input
type="checkbox"
disabled$="{{item.disabled}}" >
{{item.name}} <br>
</template>
</template>
<script>
Polymer({
is: 'checkbox-disabled-example',
properties:{
elementsObject:{
type: Object,
value: {}
},
},
onClick: function(){
this.elementsObject.items.forEach(
function(item, index, array){
item.disabled = !item.disabled;
}
);
}
});
</script>
</dom-module>
index.html code:
<body>
<template id="just-for-demo" is="dom-bind" >
<checkbox-disabled-example elements-object={{ckData}} >
</checkbox-disabled-example>
</template>
<script>
window.addEventListener('WebComponentsReady', function() {
var template = document.querySelector('template[is=dom-bind]');
var data =
{
items:
[
{ disabled: true, name:"Element 01"},
{ disabled: false, name:"Element 02"},
{ disabled: false, name:"Element 03"},
{ disabled: false, name:"Element 04"},
{ disabled: false, name:"Element 05"},
{ disabled: true, name:"Element 06"}
]
};
template.ckData = data;
});
</script>
</body>
As you can see I create several Check Boxes, each one of them bound to the disabled property of its item.
When I click on the button I toggle the disabled value of each item, however the Check boxes states don't change.
What am I doing wrong?
Changes that imperatively mutate an object or array are not observable.
If you have a simple property like:
this.name = 'Jane';
Polymer will automatically create a setter and it will automatically pick up any changes on that property.
However, changes on an object subproperty or an array item will not work:
this.address.street = 'Elm Street';
The setter on address will not be called and the change will not be detected.
Polymer provides specific methods for making observable changes to subproperties and arrays, in the example above you would need to call:
this.set('address.street', 'Elm Street');
The address.street part is called path. It's a string that identifies a property or subproperty relative to a scope. In most cases, the scope is a host element.
If you are creating a path to an array item, you will need to pass the index:
this.set('items.5', 'Changed index 5');
In your specific example, a simple change to the click event handler would be enough:
onClick: function() {
var that = this;
this.elementsObject.items.forEach(function(item, index, array) {
that.set('elementsObject.items.'+index+'.disabled', !item.disabled);
});
}
Or you can use the cool new ES6 arrow function syntax to make it more readable and remove the ugly that = this:
onClick: function() {
this.elementsObject.items.forEach((item, index, array) => {
this.set('elementsObject.items.'+index+'.disabled', !item.disabled);
});
}
Related
I'm trying to build a treeview component comporting inputs in order to change my source json.
The binding part seems to work fine but the hide/show action on branches is broken :
HTML :
<div id="app">
<tree :data="json" :link="json"></tree>
<p>Outside component :</p>
<pre>{{json}}</pre>
</div>
JS :
let json = {
nodeA: {
nodeA1 : "valueA1",
nodeA2 : "valueA2"
},
nodeB: "valueB",
nodeC: {
nodeC1 : "valueC1",
nodeC2 : "valueC2"
}
};
Vue.component('tree', {
name: 'treeview',
props: [
'data',
'link'
],
template: `<ul>
<li v-for="(val, key) in data">
<input type="text" v-if="isLeaf(val)" v-model=link[key]>
<span #click="toggle">{{key}}</span>
<tree v-if="!isLeaf(val)" v-show="show" :data="val" :link="link[key]">
</tree>
</li>
</ul>`,
data: function() {
return {
show: false
};
},
methods: {
isLeaf: function(node) {
return typeof node != 'object';
},
toggle: function() {
this.show = !this.show;
}
}
});
new Vue({
el: '#app',
data: {
json: json
}
});
https://codepen.io/anon/pen/EZKBwL
As you can see, a click on the first branch ("nodeA") activate both the first and the third branches...
I think the problem comes from the click that occurs on the parent component but I can't find a way to fix my code.
Your all the branches are hiding/showing together as you are using single variable show to hide and show both of those, You have to use different variable for each on node.
It will be impractical to have as many variables as number of nodes, but you can have a hash like following:
data: function() {
return {
show: {}
};
},
and change the toggle method to set the variable for each node by creating a key in this show hash for that node. You can use vm.$set for this which sets a property on an object. If the object is reactive, ensure the property is created as a reactive property and trigger view updates.
toggle: function(node) {
if(this.show[node]){
this.$set(this.show, node, false)
} else {
this.$set(this.show, node, true)
}
}
You need to do corresponding changes in HTML as well, which can be viewed in the working codepen here.
It happens because you bind all elements at the same parameter.
To toggle visibility individually for each element you need to store element states at it's own place like object's field or array.
But i guess better solution is toggle class on a target element by click and control visibility by css via class.
You may need a show field for each node to toggle their visibility separately, in my improved example, I'm using a data structure like this:
{
"nodeA": {
"value": {
"nodeA1": {
"value": "valueA1",
"show": false
},
"nodeA2": {
"value": "valueA2",
"show": false
}
},
"show": true
},
"nodeB": {
"value": "valueB",
"show": true
}
}
my template:
<ul>
<li v-for="(val, key) in data" v-show='val.show'>
<input type="text" v-if="isLeaf(val)" v-model='link[key].value'>
<span #click="toggle(val.value)">{{key}}</span>
<tree v-if="!isLeaf(val)" :data="val.value" :link="val.value">
</tree>
</li>
</ul>
methods:
{
isLeaf: function(node) {
return typeof node.value != 'object';
},
toggle: function(value) {
for (const nodeName in value) {
value[nodeName].show = !value[nodeName].show;
}
}
}
I have an application that is using Polymer. In this application, I am binding an array of items to the UI. The user can click a button. When that button is clicked, a task associated with a third-party library is called. When that task is completed, it returns a status. I need to bind that status to a property of an item in my array. The third-party library allows me to use a callback function. For that reason, I'll demonstrate my challenge using JavaScript's baked in setTimeout function.
my-component.html
<dom-module id="view-tests">
<template>
<table>
<tbody>
<template is="dom-repeat" items="{{ items }}" as="item">
<tr>
<td>[[ item.name ]]</td>
<td><item-status status="[[ item.status ]]"></item-status></td>
</tr>
</template>
</tbody>
</table>
<button on-click="bindClick">Bind</button>
</template>
<script>
Polymer({
is: "my-component",
properties: {
items: {
type: Array,
notify: true,
value: function() {
return [
new Item({ name:'Item 1', status:'In Stock' }),
new Item({ name:'Item 2', status:'Sold Out' })
];
}
},
},
bindClick: function() {
var items = items;
setTimeout(function() {
this.set('items.1.status', 'In Stock');
}, 1000);
}
});
</script>
</dom-module>
As shown in the code snippet above, there is another component item-status.
item-status.html
<dom-module id="test-status">
<template>
<span class$="{{ statusClass }}">{{ status }}</span>
</template>
<script>
Polymer({
is: "item-status",
properties: {
status: {
type: String,
value: '',
observer: '_statusChanged'
}
},
_statusChanged: function(newValue, oldValue) {
alert(newValue);
if (newValue === 'In Stock') {
this.statusClass = 'green';
} else if (newValue === 'Sold Out') {
this.statusClass = 'red';
} else {
this.statusClass = 'black';
}
}
});
</script>
</dom-module>
When a user clicks the "Bind" button, the status does not get updated in the UI. I noticed the alert that I added for debugging purposes appears when the view initially loads. However, the alert window does not appear when the "Bind" button is clicked. This implies that the observer function is not firing. My callback actually looks something like this:
getStatus(1, function(status) {
this.set('items.1.status', status);
});
How do I set the property of an array item from a callback?
setTimeout has its own scope. '.bind(this)' can be used to bind the Polymer element scope to the callback function. Below bindClick function should work
bindClick: function() {
setTimeout(function() {
this.set('items.1.status', 'In Stock');
}.bind(this), 1000);
}
Working jsbin: http://jsbin.com/mehovu/edit?html,output
So I have this Polymer element with dom-repeat. It binds correctly. However, when the array is modified, it doesn't reflect back to DOM. Nothing changed when I click on the button.
<dom-module id="my-element">
<template>
<template is="dom-repeat" id="allRules" items="{{allRules}}">
<span class="tag" rule-enabled$={{item.enabled}}>{{item.name}}</span>
</template>
<button on-click="click">Click me</button>
</template>
</dom-module>
<script>
Polymer({
is: "my-element",
properties: {
allRules: {
type: Array,
notify: true
}
},
click: function() {
this.allRules[0].name = "three";
},
ready: function() {
this.allRules = [
{
name: "one",
enabled: true
}, {
name: "two",
enabled: false
}
]
},
setBind: function(bind) {
this.bind = bind;
}
});
</script>
Is there a method like notifyArrayUpdated to tell the DOM to update binding data?
When you change a subproperty inside an array, you need to do
this.set('allRules.0.name', 'three');
Check out array binding for details.
Here is a plunker for it.
I am trying to select an element by id that was created dynamically:
<dom-module id="filter-box">
<style>
:host {
display: block;
}
</style>
<template>
<h4 id="title">{{title}}</h4>
<paper-checkbox id="test">test</paper-checkbox><br>
<template is="dom-repeat" items="{{filters}}">
<paper-checkbox id="{{item}}">{{item}}</paper-checkbox><br>
</template>
</template>
</dom-module>
<script>
(function () {
Polymer({
is: 'filter-box',
properties: {
filters: {
type: Array,
notify: true,
},
title: {
type: String
}
},
ready: function() {
this.filters = [
'Commercial',
'Enterprise'
];
},
isSelected: function(filter) {
return this.$$[filter].checked;
}
});
})();
</script>
When isSelected("something") is called I get:
"Uncaught TypeError: Cannot read property 'checked' of undefined"
I can select the title via this.$.title, however I can not select the dynamic elements this way or using this.$$ as suggested here.
According the reference you provided you are supposed to call this.$$(selector) with parentheses as in a function call (and not with brackets). So replace your code with this:
return this.$$('#'+filter).checked;
Note that you may also need to prepend the id selector with #.
I have these two custom Polymer Elements (Polymer 1.0.3):
Displays text to be translated.
Displays button to trigger loading of translation.
I also have a Behavior that holds the translations (json object) and contains all the functions that make translation possible.
This is what I expect to happen:
Click the button in Element 2
Translations load into the Behavior
Language selection is set in the Behavior
Text in Element 1 is updated with the translated equivalent
Steps 1 - 3 happen, but 4 doesn't. The text is never updated. I can get it to work if Elements 1 & 2 are combined as the same element, but not if they're separate (any they need to be separate).
If you're wondering about the "kick" property, it's something I learned from Polymer 0.5. It got things working when the two elements were combined, so I'm thinking it'll be be necessary when the elements are separate.
Any idea how I can make this happen? I'm open to alternative paradigms.
Code
This is roughly how my code is laid out. I also made a plunker with a single-page test case.
index.html
<!doctype html>
<html>
<head>
<script src="http://www.polymer-project.org/1.0/samples/components/webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="http://www.polymer-project.org/1.0/samples/components/polymer/polymer.html">
<link rel="import" href="behavior.html">
<link rel="import" href="element1.html">
<link rel="import" href="element2.html">
</head>
<body>
<my-element></my-element>
<another-element></another-element>
</body>
</html>
Element 1
<dom-module id="my-element">
<template>
<p>{{localize(label, kick)}}</p>
</template>
</dom-module>
<script>
Polymer({
is: 'my-element',
behaviors: [
behavior
],
properties: {
label: {
type: String,
value: 'original'
}
}
});
</script>
Element 2
<dom-module id="another-element">
<template>
<button on-click="buttonClicked">load</button>
</template>
</dom-module>
<script>
Polymer({
is: 'another-element',
behaviors: [
behavior
],
buttonClicked: function() {
this.registerTranslation('en', {
original: 'changed'
})
this.selectLanguage('en');
}
});
</script>
Behavior
<script>
var behavior = {
properties: {
kick: {
type: Number,
value: 0
},
language: {
type: String,
value: 'fun'
},
translations: {
type: Object,
value: function() {
return {};
}
}
},
localize: function(key, i) {
if (this.translations[this.language] && this.translations[this.language][key]) {
return this.translations[this.language][key];
}
return key;
},
registerTranslation: function(translationKey, translationSet) {
this.translations[translationKey] = translationSet;
},
selectLanguage: function(newLanguage) {
this.language = newLanguage;
this.set('kick', this.kick + 1);
}
};
</script>
First, although the notion is to have behavior be a conduit for shared data between instances, as written, each instance will have it's own copy of the translations object and the kick property.
Second, even if that data was privatized so it could be shared, the kick binding made via localize(label, kick) is in a different scope from the expression that modifies kick (i.e. this.set('kick', this.kick + 1); [{sic} this could simply be this.kick++;]).
To notify N instances of a change in shared data, one must keep track of those instances. A good way to do this is by attaching event listeners. Another way is simply keeping a list.
Here is an example implementation of your design:
<script>
(function() {
var instances = [];
var translationDb = {};
var language = '';
window.behavior = {
properties: {
l10n: {
value: 0
}
},
attached: function() {
instances.push(this);
},
detached: function() {
this.arrayDelete(instances, this);
},
_broadcast: function() {
instances.forEach(function(i) {
i.l10n++;
});
},
localize: function(key, i) {
if (translationDb[language] && translationDb[language][key]) {
return translationDb[language][key];
}
return key;
},
registerTranslation: function(translationKey, translationSet) {
translationDb[translationKey] = translationSet;
},
selectLanguage: function(newLanguage) {
language = newLanguage;
this._broadcast();
}
};
})();
</script>
<dom-module id="my-element">
<template>
<p>{{localize(label, l10n)}}</p>
</template>
<script>
Polymer({
behaviors: [
behavior
],
properties: {
label: {
type: String,
value: 'original'
}
}
});
</script>
</dom-module>
<dom-module id="another-element">
<template>
<button on-tap="buttonClicked">load</button>
</template>
<script>
Polymer({
behaviors: [
behavior
],
buttonClicked: function() {
this.registerTranslation('en', {
original: 'changed'
});
this.selectLanguage('en');
}
});
</script>
</dom-module>