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?

esmoore68
  • 1,276
  • 1
  • 8
  • 16
  • 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.bind – Bruno Marotta Sep 21 '17 at 08:55
  • @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... – Balázs Sep 21 '17 at 12:42
  • ... 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. – Balázs Sep 21 '17 at 12:43
  • @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. – Bruno Marotta Oct 25 '18 at 09:52

1 Answers1

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.


adiga
  • 34,372
  • 9
  • 61
  • 83
  • 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. – Balázs Sep 21 '17 at 12:34
  • 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. – Balázs Sep 21 '17 at 12:36
  • @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`? – adiga Sep 21 '17 at 12:41
  • 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. – Balázs Sep 21 '17 at 12:48
  • @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? – adiga Sep 21 '17 at 13:00
  • "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 ``. 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`˙... – Balázs Sep 21 '17 at 13:16
  • 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. – Balázs Sep 21 '17 at 13:19
  • 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. – esmoore68 Sep 21 '17 at 13:25
  • @Balázs oh, that is just lovely. Thank you so much. – adiga Sep 21 '17 at 13:45
  • @esmoore68 go through Balázs 's comments for a detailed explanation on binding. – adiga Sep 21 '17 at 13:47