user1372778
user1372778

Reputation:

Aurelia repeat.for does not refresh when model changes

I want to build a simple custom component with Aurelia that allows user to input one or more strings. When there are more than one items, list should show remove button for each item on the list.

My problem is that the first item of the list does not show remove button when there are multiple items in the list. This is how it looks

Here is the code and html I have for the custom list component:

View

<template>
  <div repeat.for="item of items">
    <input type="text" value.bind="items[$index]">
    <button click.delegate="remove($index)" 
            if.bind="hasMoreThanOne()">Remove</button>
  </div>
  <button click.delegate="add()">Add</button>
</template>

ViewModel

export class List {
  items: string[];

  constructor() {
    this.items = [];
    this.add();
  }

  add() {
    this.items.push("");
  }

  hasMoreThanOne() {
    return this.items.length > 1;
  }

  remove(index) {
    this.items.splice(index,1);
  }
}

My question is two-fold:

Upvotes: 5

Views: 4737

Answers (3)

Christian Jarenfors
Christian Jarenfors

Reputation: 11

If you would pass items.length into hasMoreThanOne() then Aurelia would recalculate the method each time the length changes. Like this: hasMoreThanOne(items.length).

In Html Aurelia doesn't reevalute functions unless the parameters have changed.

I just solved an issue i had with a function using I18n, a function in a table. The function wouldn't reevalute the funtion when the language changed. By passing a boolean to the function which i changed on an event I solved it. Even though the actual data to the function didn't change the changed boolean triggered Aurelia to revalute the whole function.

Like this:

<td>
${functionToTrigger(item.value, triggerBool}
</td>

then

this.eventAggregator('notactualli18nEvent:changed',()=> {this.triggerBool = !triggerBool});

Upvotes: 1

Ashley Grant
Ashley Grant

Reputation: 10887

Aurelia treats any functions that are part of a bind command as pure functions. This means that it will not call the function again until the parameters being passed to the function have changed. Since hasMoreThanOne() has a return value that changes based on something that isn't a parameter to the function (naturally, since the function doesn't have any parameters), Aurelia isn't going to call the function again.

The reason Aurelia doesn't re-evaluate the function when the array changes is that the repeater is optimized and sees that the first item in the array has not changed, so it just keeps using the existing DOM it has for it. With a properly created view, this helps greatly increase performance, but in your case, it's causing unwanted issues.

You found one, non-optimal way to deal with this, by using a getter. The reason this is non-optimal is that Aurelia, by default, uses dirty checking every 200ms to check for changes to getters. This fixes the problem you had, but isn't ideal for performance.

The simplest option, given how simple the hasMoreThanOne() function is, would be to simply inline the function in your binding, like this:

<template>
  <div repeat.for="item of items">
    <input type="text" value.bind="items[$index]">
    <button click.delegate="remove($index)" 
            if.bind="items.length > 1">Remove</button>
  </div>
  <button click.delegate="add()">Add</button>
</template>

This is honestly how I would probably handle this.

You could also use the getter as you are doing, but attach the computedFrom decorator to it to preclude dirty checking:

import {computedFrom} from 'aurelia-framework';

export class List {
  items: string[];

  constructor() {
    this.items = [];
    this.add();
  }

  add() {
    this.items.push("");
  }

  @computedFrom('items.length')
  get hasMoreThanOne() {
    return this.items.length > 1;
  }

  remove(index) {
    this.items.splice(index,1);
  }
}

This will give you the exact same performance as the inlined binding I used above, but there is a bit more code to write.

Upvotes: 15

user1372778
user1372778

Reputation:

I was able to solve this with pure luck. Turning the hasMoreThanOne() function into property fixed the issue and now the remove button is visible also for the first item in the list.

Here are the changes I made:

hasMoreThanOne() {
    return this.items.length > 1;
}

is now

get hasMoreThanOne() {
    return this.items.length > 1;
}

and similar change to view:

<button click.delegate="remove($index)" 
            if.bind="hasMoreThanOne()">Remove</button>

is now

<button click.delegate="remove($index)" 
            if.bind="hasMoreThanOne">Remove</button>

I'm still confused why this changed anything. So if anyone is able to explain me this behaviour, I'm more than happy to hear about it.

Upvotes: 0

Related Questions