1

I have an array of strings bound to input elements:

<div repeat.for="i of messages.length">
    <input type="text" value.bind="$parent.messages[i]">
</div>

I need to delete an element when the input content is deleted, without using dirty-checking.

This sounds easy - just delete the element which has empty value from the input.delegate handler, unfortunately this does not work due to an Aurelia bug #527. Here's a gist that tries this approach: https://gist.run/?id=e49828b236d979450ce54f0006d0fa0a

I tried to work around the bug by using queueTask to postpone deleting the array element, to no avail. And since the devs closed the bug because according to them it is a duplicate of a completely unrelated issue I guess it is not getting fixed anytime soon.

I am out of ideas how to implement this, so any suggestions are welcome.

Eliran Malka
  • 15,821
  • 6
  • 77
  • 100
Angel Popov
  • 47
  • 1
  • 6
  • what do you mean by "without using dirty-checking"? how are you sure that dirty-checking is taking place here? is this working at the moment, and you're just looking to optimize your code, or is it not working at all? – Eliran Malka Oct 27 '16 at 10:05
  • I tried to do this without dirty checking (see the linked gist) and It is not working, due to an Aurelia bug. Dirty checking will search the array every 100ms and delete any empty elements, it will work, but I want to avoid it. – Angel Popov Oct 27 '16 at 10:16
  • see [this blog post](http://ilikekillnerds.com/2015/10/observing-objects-and-arrays-in-aurelia/), or [this stackoverflow post](http://stackoverflow.com/a/32019971/547020). – Eliran Malka Oct 27 '16 at 12:46
  • also, check out the new [`collectionObserver` API](http://stackoverflow.com/a/30286225/547020). – Eliran Malka Oct 27 '16 at 12:47
  • Unfortunately The collectionObserver API only watches when elements are added or removed, not when they are changed, see this gist: https://gist.run/?id=c9dcdcc9aaa0da3734ce32ffe8e4a86f – Angel Popov Oct 27 '16 at 13:52

2 Answers2

1

Absolutely no need for any kind of dirty checking here! :)

Here's a working demo for your scenario: https://gist.run/?id=20d92afa1dd360614147fd381931cb17

  • $parent isn't needed anymore. It was related to pre-1.0 Aurelia versions.
  • If you use a variable instead of array indexes, you can leverage two-way data-binding provided by the input.
<template>
  <div repeat.for="msg of messages">
    <input type="text" value.bind="msg" input.delegate="onMessageChanged(msg, $index)">
  </div>
</template>

So, your onChange event could be simplified like this:

  • msg holds the actual value of your current input.
  • i index will be used for deletion.
export class App {
  messages = ['Alpha','Bravo','Charlie','Delta','Echo'];

  onMessageChanged(msg, i){
    if (!msg || msg.length === 0) {
      this.messages.splice(i, 1); 
    }
  }
}

There was a related question about a similar problem. This answer might give you more details about the main idea.

Community
  • 1
  • 1
Marton Sagi
  • 1,247
  • 1
  • 8
  • 10
  • sounds great (and working, too!). how can you be sure no dirty checking is going on? – Eliran Malka Oct 27 '16 at 12:51
  • Aurelia uses observation techniques to avoid conventional dirty-checking as much as possible. There are some cases when it falls back to dirty-checking, but that doesn't apply to your case (simple two-way databinding). Most of the time you can rely on Aurelia's built-in observers without having to implement custom dirty checks. More info: [Docs](http://aurelia.io/hub.html#/doc/article/aurelia/binding/latest/binding-how-it-works/1), [Jeremy Danyow's blog post(might be outdated but useful reading)](https://www.danyow.net/aurelia-property-observation/) – Marton Sagi Oct 27 '16 at 13:17
  • thanx, i'm aware of that, just asking practically - can it be tested in the browser? e.g. if i create a getter *without* a `computedFrom` decoration, and put a console log inside, i can see the dirty-check mechanism calls. is there any other way of telling (proving) if dirty-checking is on for a certain property? – Eliran Malka Oct 27 '16 at 13:31
  • But this disables the two-way binding, see [issue #444](https://github.com/aurelia/binding/issues/444). That $parent.messages[i] is actually required to work around this, though messages[$index] will work just as well. Put a console.log( this.messages ) at the end of onMessageChanged in your gist and notice that the elements are not actually changed: https://gist.run/?id=c7f23bc33bec12405e82bf8836104c8f But this gave me an idea - to work around the bug, don't use 2-way binding, but change the value from onMessageChanged and this works: https://gist.run/?id=2323c09ec9da989eed21534f177bf5a8 – Angel Popov Oct 27 '16 at 13:32
  • 1
    @EliranMalka: Well, you could profile your application and see the callstack to make sure of it. In addition to that, here's a [video](https://youtu.be/fi33aDFKvxE?t=3775), where Rob Eisenberg talks about data-binding. Update on `@computedFrom` dirty-checking: it can be avoided too by using `@observables` decorator: [blog post](http://ilikekillnerds.com/2016/06/working-aurelia-observable/) – Marton Sagi Oct 28 '16 at 07:20
-2

Ok, so the solution to this is not to use the buggy (in this case) aurelia 2-way binding, but to use 1-way binding and set the value from the input.delegate handler:

https://gist.run/?id=2323c09ec9da989eed21534f177bf5a8

The @marton answer seems to work at first sight, but it actually disables 2-way binding, so any changes to the inputs are not copied to the array. But it gave me an important hint how to solve the issue. The equivalent of this html code:

<div repeat.for="msg of messages">
    <input type="text" value.bind="msg">
</div>

is this:

for (let msg of messages) {
  msg = 'something else'; // note: this does not change the contents of the array
}

See issue #444 for more details

Hence, this forces one-way binding. To fix this in the @marton solution, we only have to change the value from the input.delegate handler:

onMessageChanged(msg, i){
  if (!msg || msg.length === 0) {
    this.messages.splice(i, 1);//delete the element 
  }
  else {
    this.messages[i] = msg;//change the value
  }
}
Angel Popov
  • 47
  • 1
  • 6
  • 1
    why do you think aurelia-2-way binding is buggy if it does what is expected to be done? – Fabio Oct 27 '16 at 15:30
  • It is buggy in the case explained in this question. It seems that a single input value change is applied to the view-model twice - before and after the input.delegate handler is called. Which is a problem when the handler deletes an element from the array. Because the array has changed - the second time Aurelia overwrites an unrelated element. See this gist for a simple example: https://gist.run/?id=f3e7002bf3b60472313b8f9c5963c7c4 This is explained in the bug linked from this question. – Angel Popov Oct 27 '16 at 15:44
  • 1
    @AngelPopov: I showed you a work-around for handling deletion, I think that was the scope of above question. :) Although, I assumed you'd like to do more, that's why I pointed you out to the other related answer. On one hand, you lose the advantage of implicit array updates by using `msg` reference. But on the other hand, Aurelia provides you with sufficient contextual information (e.g. $index) to allow an easy way for updating `messages` manually when input value changes. – Marton Sagi Oct 28 '16 at 07:52
  • 1
    Using $index vs looping for deletion gives the same result, $index is more efficient. The 2-way array binding is confusing and you failed in this trap, you say the solution leverages 2-way binding which is not the case and this will confuse people - they will use it w/o realizing that the array is not updated, which is the whole point. Thank you for pointing out about the contextual information! I have credited you in the solution I posted and am not marking as accepted answer neither solution because mine builds on top of yours and just adds the missing last step to update the array manually. – Angel Popov Oct 28 '16 at 09:08