Reputation: 81617
Let's say I want to create a todo list with Polymer (v0.5.5). In my element, I define a property tasks
, which is an array containing a list of objects like { name: 'foo', done: false }
. I want to display the number of remaining tasks, so I need to detect when the property done
of an object included in the array is changed.
Here is an extract of the code:
<polymer-element name="todo-list">
<template>
<span>You have {{ tasks.length }} tasks, {{ remaining }} remaining</span>
...
</template>
<script>
Polymer({
tasks: [
{name: "foo", done: false},
{name: "bar", done: true}
],
get remaining() {
return this.tasks.filter(function(t) { return !t.done }).length;
}
changeState: function(e) {
var _task = this.tasks[e.target.getAttribute('data-task-id')];
_task.done = !_task.done;
}
});
</script>
</polymer-element>
With Firefox, it is working but not with Chrome (41.x). Indeed, Chrome only detect the change of the array itself (for example, if I add a new task, the remaining count is updated correctly).
How do I do that?
Thanks
Edit, regarding Andy answer
When I do that kind of thing:
var tasks = tasks: [
{name: "foo", done: false},
{name: "bar", done: true},
{name: "baz", done: false}
];
Object.observe(tasks, function() { alert('Modification'); }
and if I do a modification in the array itself (like in tasks.push({...})
), then a popup is displayed. But if I change a property of an object contained in the array (e.g. tasks[0].done = true
), then nothing happen. That's the source of my problem...
Upvotes: 0
Views: 1888
Reputation: 26
Just my 2 cents
In fact the problem is independent of the presence of the button in a template. You just have to use a repeat template on a list to reproduce the problem. The code below demonstrates it.
<h1>You have {{ tasks.length }} tasks, {{ remaining }} remaining</h1>
<button
style="border: solid 1px red; padding: 20px; border-radius: 20px; display: inline-block;"
on-click="{{ doTask }}"
>
Click to mark as done
</button>
<ul>
<template repeat="{{ task in othertasks }}">
<li>{{task}}</li>
</template>
</ul>
<div>
{{ tasks[0].done }}
</div>
</template>
<script>
Polymer({
tasks: [
{name: "foo", done: false},
{name: "bar", done: true}
],
othertasks: ["foo","bar"],
get remaining() {
return this.tasks.filter(function(t) { return !t.done }).length;
},
doTask: function () {
this.tasks[0].done = true;
}
});
</script>
</polymer-element>
I actually find it rather natural. When we look at the specification Object.observe (). Indeed on an Array, it is triggered only if we:
So it will not fire if you change an internal property of an element in the array. If we add a listener, in a ready method , we will see that it is not triggered by our doTask method. And this why that the Horacio 's hack works . He replace the object. Another solution is to manually notify the change using
Object.getNotifier(this.tasks).notify
Below is a full version
<polymer-element name="todo-list">
<template>
<h1>You have {{ tasks.length }} tasks, {{ remaining }} remaining</h1>
<button
style="border: solid 1px red; padding: 20px; border-radius: 20px; display: inline-block;"
on-click="{{ doTask }}"
>
Click to mark as done
</button>
<ul>
<template repeat="{{ task in othertasks }}">
<li>{{task}}</li>
</template>
</ul>
<div>
{{ tasks[0].done }}
</div>
</template>
<script>
Polymer({
tasks: [
{name: "foo", done: false},
{name: "bar", done: true}
],
othertasks: ["foo","bar"],
ready: function () {
Object.observe(this.tasks, function(change) {
console.log(change); // What change
});
},
get remaining() {
return this.tasks.filter(function(t) { return !t.done }).length;
},
doTask: function () {
this.tasks[0].done = true;
Object.getNotifier(this.tasks).notify({
type: 'update',
name: '0'
});
}
});
</script>
</polymer-element>
I have more trouble understanding why it works without the template. If we keep our listener well we see that it is not call but remaining is updating ...
Upvotes: 1
Reputation: 1162
I'm afraid I don't understand the problem, Romain.
I've tested with the following code:
<polymer-element name="my-component" attributes="status count">
<template>
<style>
</style>
<div >
<h1>You have {{ tasks.length }} tasks, {{ remaining }} remaining</h1>
<div
style="border: solid 1px red; padding: 20px; border-radius: 20px; display: inline-block;"
on-click="{{ doTask }}"
>
Click to mark as done
</div>
<div>
{{ tasks[0].done }}
</div>
</div>
</template>
<script>
Polymer("my-component", {
tasks: [
{name: "foo", done: false},
{name: "bar", done: true}
],
get remaining() {
return this.tasks.filter(function(t) { return !t.done }).length;
},
doTask: function() {
this.tasks[0].done = true;
}
});
</script>
</polymer-element>
When I click on the button, the value of the label changes, i.e. the remining
getter detects the change. I've tested in Chromium 41 and Firefox on Linux.
You can test my code on http://embed.plnkr.co/HXaKsQHjchqwe0P3bjy5/preview
Could you please give me more info of what do you want to do and how does it differ from my code?
After speaking with @romaintaz via Twitter, it seems that the problem only happens when the buttons are inside if templates, like this:
<div >
<h1>You have {{ tasks.length }} tasks, {{ remaining }} remaining</h1>
<template repeat="{{ task, taskIndex in tasks }}">
<template if="{{task.done}}">
<button
style="border: solid 1px red; padding: 20px; border-radius: 20px; display: inline-block;"
on-click="{{ doTask }}"
>Click 1</button>
</template>
<template if="{{!task.done}}">
<button
style="border: solid 1px red; padding: 20px; border-radius: 20px; display: inline-block;"
on-click="{{ doTask }}"
>Click 2</button>
</template>
</template>
<div>
{{ tasks[0].done }}
</div>
In this case, the removing
getter doesn't detect the changes of one of the list's item properties anymore.
For the moment I only have a quick'n'dirty solution: instead of changing only one property of a list item, change the whole list item, then the getter see it.
Example:
<polymer-element name="my-component" attributes="status count">
<template>
<style>
</style>
<div >
<h1>You have {{ tasks.length }} tasks, {{ remaining }} remaining</h1>
<template repeat="{{ task, taskIndex in tasks }}">
<template if="{{task.done}}">
<button
style="border: solid 1px red; padding: 20px; border-radius: 20px; display: inline-block;"
on-click="{{ doTask }}"
>Click 1</button>
</template>
<template if="{{!task.done}}">
<button
style="border: solid 1px red; padding: 20px; border-radius: 20px; display: inline-block;"
on-click="{{ doTask }}"
>Click 2</button>
</template>
</template>
<div>
{{ tasks[0].done }}
</div>
</div>
</template>
<script>
Polymer("my-component", {
tasks: [
{name: "foo", done: false},
{name: "bar", done: true}
],
get remaining() {
return this.tasks.filter(function(t) { return !t.done }).length;
},
doTask: function() {
tmp = this.tasks[0];
tmp.done = true
this.tasks[0] = {};
this.tasks[0] = tmp;
},
observe: {
tasks: 'validate'
},
validate: function(oldValue, newValue) {
}
});
</script>
</polymer-element>
Plunkr here: http://embed.plnkr.co/YgqtKgYRaRTZmoKEFwBy/preview
Upvotes: 1
Reputation: 1939
You are not updating the observed "task" array in your changeState
method. Change it to this:
changeState: function(e) {
this.tasks[e.target.getAttribute('data-task-id')].done = !this.tasks[e.target.getAttribute('data-task-id')].done;
}
Basically you are creating a local variable by reassigning the value of the array - there is no back reference until you explicit set it. Now the above code is pretty long, so you could also do this:
changeState: function(e) {
var _task = this.tasks[e.target.getAttribute('data-task-id')];
_task.done = !_task.done;
// re assign
this.tasks[e.target.getAttribute('data-task-id')] = _task;e
}
Upvotes: 0