Search code examples
google-chromepolymerobject.observe

How to detect change in a property of an object in an array with Polymer?


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


Solution

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