Aurelia if.bind inside repeat.for is not updating
Asked Answered
C

1

6

I have a strange case in an Aurelia template of elements with if.bind inside a repeat.for not being shown/hidden when their underlying property is changed. With the following code, the edit fields should be shown and the edit button should be hidden as soon as the edit button is clicked. Subsequently, both the save and undo buttons should hide the edit fields and show the edit buttons again.

MyList.ts:

import { computedFrom } from "aurelia-binding";

export class MyList
{
  items: any[] = [{
      "firstName": "Joe",
      "lastName" : "Blow",
      "id": 1
    },
    {
      "firstName": "Jane",
      "lastName" : "Doe",
      "id": 2
    }
  ]

  editingItem: any = null
  isEditing: boolean;

  edit(item){
    this.editingItem = item;
    this.isEditing = true;
  }

  editFirst(item){
    this.editingItem = this.items[0];
    this.isEditing = true;
  }

  undo(){
    // undo logic here
    this.editingItem = null;
    this.isEditing = false;
  }

  save(){
    // Save logic here
    this.editingItem = null;
    this.isEditing = false;
  }
}

MyList.html:

<template>
  <table>
    <tbody>
      <tr repeat.for="item of items">
        <td if.bind="!isEditing">
          <button click.delegate="edit(item)">Edit</button>
        </td>
        <td>${item.firstName}</td>
        <td>${item.lastName}</td>
        <td if.bind="isEditing && editingItem.id == item.id">
          <button click.delegate="save()">Save</button>
          <button click.delegate="undo()">Undo</button>
          <input value.bind="editingItem.firstName" />
          <input value.bind="editingItem.lastName" />
        </td>
      </tr>
    </tbody>
  </table>
</template>

Clicking the edit button does nothing. Interestingly, if I add

${isEditing}

Anywhere in the template outside the repeat.for, the code works as expected. It's as if the rendering engine doesn't know to re-render elements inside the repeat loop.

Is this a bug? Or am I doing something silly?

Coprology answered 20/9, 2017 at 21:18 Comment(4)
if.bind and repeat.for are template controllers. I know that there are some problems mixing them up. Try using show.bind instead of if.bindArnica
@BrunoMarotta The only thing I know that can be problematic if both of them are present on the same element. Even more weirdly, such a scenario works in Chrome but not in Firefox and others, and according to a response to an issue about this, it is by design (that is, they should not work on the same element. The fact that it works in Chrome or whichever is not desired). There is nothing wrong with using them separately, they are meant to be used...Zillah
... And keep in mind that visible.bind only affects the visual aspect, screen readers, crawlers, etc. will still "see" the content, which, depending on the conditions, is not what you want.Zillah
@Balázs - That's what I meant. They shouldn't be mixed up in the same element. Sorry for being imprecise. And I also observed this effect that works in Chrome and not in Firefox.Arnica
M
6

This is weird. I created a gist from your code and as you said it wasn't working. No errors in the console either.

But when I initialized isEditing it started working

isEditing: boolean = false;

Updated gist

From @Balázs in the comments: By not initializing the isEditing, that property virtually does not exist -- it can be used for autocompletion/IntelliSense, but it is not actually present on the object. You can verify that by replacing the console.log call in the edit method with console.log(this.editing);, you will see that it is undefined. By it being undefined, Aurelia cannot subscribe to it (because it is as though it wasn't even there - no getter/setter exists), and therefore has no way of knowing when, if ever, it comes to life. However, that even explicitly setting it to undefined is different from this, because that does actually create the property, whose value happens to be set to undefined.


Also note:

Since you are assigning editingItem directly with item here:

edit(item){
    this.editingItem = item;
    this.isEditing = true;
  }

Whatever you change in editingItem will affect item as well. So even if you undo, the change will persist to item. So you should do a clone before assigning to editingItem.


Megaton answered 21/9, 2017 at 12:20 Comment(10)
The explaination is probably that by not initializing the isEditing, that property virtually does not exist -- it can be used for autocompletion/IntelliSense, but it is not actually present on the object. You can verify that by replacing the console.log call in the edit method with console.log(this.editing);, you will see that it is undefined. By it being undefined, Aurelia cannot subscribe to it (because it is as though it wasn't even there - no getter/setter exists), and therefore has no way of knowing when, if ever, it comes to life.Zillah
Note however, that even explicitly setting it to undefined is different from this, because that does actually create the property, whose value happens to be set to undefined.Zillah
@Balázs I'll update the answer with this explanation. Wwhen a property is not initialized in typescript, the js generated will not have the property. But I created an ES6 class in the gist. Is it getting compiled to a constructor function without the isEditing?Megaton
Yes. If you comment out the this.isEditing = true; line, and add a console.log(this); at the top of the method, you will see that the object does not contain an isEditing property. It is important that you comment out the this.isEditing = true; line, because otherwise, the browser will evaluate the object when you click "1 more..." and then, it will be present, because by the time you click the object to investigate, it will already be set, even though it didn't exist originally.Zillah
@Balázs thank you for the detailed explanation. I removed the editingItem initialization (null). But it was still working. Is it because when we do this.editingItem = item;,editingItem is actually pointing to item object in the memory and it is actually item's subscription that is at play here?Megaton
"I removed the editingItem initialization (null)." -- You mean that now editingItem is not initialized, but isEditing is? In that case, no, the answer is different. In that case it works, because of the order of evaluation of the if.bind expression attached to the last <td>. Like in most programming languages, if the evaluation of the value of an expression can be terminated early, then it will be terminated early. In other words, you have this: isEditing && editingItem.id == item.id. If either side of the && operator is false, the expression can never be true˙...Zillah
So, by initializing isEditing with false, and not initializing editingItem, the expression is evaluated to be false as soon as isEditing is evaluated, therefore the editingItem is never evaluated and subscribed to up until that point when isEditing becomes true. It is only the favor of the logic you have there that the first time Aurelia touches that variable, it is already initialized. If you swap the order of the if.bind expression, that is, to this: editingItem.id == item.id && isEditing, you will see that now this does not work either.Zillah
Thank you so much. Initializing the property solved it in my real case as well. @adiga: this was a test case distilled from a much more complicated real case. The real code takes care of cloning the object to facilitate the undo operation.Coprology
@Balázs oh, that is just lovely. Thank you so much.Megaton
@Coprology go through Balázs 's comments for a detailed explanation on binding.Megaton

© 2022 - 2024 — McMap. All rights reserved.