0

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...

Romain Linsolas
  • 73,921
  • 45
  • 197
  • 265

3 Answers3

1

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?

Edit

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

LostInBrittany
  • 1,102
  • 9
  • 19
  • Thanks Horacio. I've changed the code for the `changeState`method to something like that: `changeState: function(e) { var _id = e.target.getAttribute('data-task-id'); var tmp = { name: this.tasks[_id].name, done: !this.tasks[_id].done}; this.tasks[_id] = tmp; }` and it works... – Romain Linsolas Mar 23 '15 at 22:27
1

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:

  • Delete an element
  • Add an element
  • Modify an item (here we speak of reference change)

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 ...

0

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
}
flyandi
  • 1,859
  • 14
  • 23