Issues with automatically resetting an element's data-bound observed value when its changes have been processed

Sorry for the complex title of this post. I will try to clarify my issue. :slight_smile:

Lately, I built a lookup component, which is essentially an input element and a dropdown list element. When typing in the input element, the dropdown is shown and items that contain the typed text are shown in the top of the list (with some highlighting on the matches). When an item is selected from the list, the lookup’s item property is set. This is a bindable property, so that client code can react to a change of the lookup’s selected item.

Currently, I am building a multiselect component, which is essentially an inline (horizontal) list of selected items (shown as tags/labels/badges), followed by my previously created lookup component for selecting new items. Selected items can be removed by clicking its tag/label/badge.

Selected items should be unique (for now), so when I select a new item from the lookup dropdown, it should be shown as a tag/label/badge in front of the lookup control. the lookup control’s value should be cleared, and the item should be removed from the lookup’s dropdown list.

It all works!
Well… almost…
For some reason, clearing the lookup’s value after a selection does not seem to work properly.

Below is a TypeScript example that reproduces the issue.

my-control.html:

<template>
  <input value.bind="inputValue">
</template>

my-control.ts:

import { bindable, bindingMode, observable } from 'aurelia-framework';

export interface Item {
  value: string;
}

export class MyControl {
  @bindable({ defaultBindingMode: bindingMode.twoWay }) item: Item | null = null;
  @observable inputValue: string = '';

  itemChanged(item: Item | null) {
    this.inputValue = (item) ? item.value : '';
  }

  inputValueChanged(value: string) {
    this.item = (value) ? { value } : null;
  }
}

app.html:

<template>
  <require from="./my-control"></require>
  <my-control item.bind="myControlItem"></my-control>
</template>

app.ts:

import { observable } from 'aurelia-framework';
import { Item } from './my-control';

export class App {
  @observable myControlItem!: Item | null;

  myControlItemChanged(item: Item) {
    if (item) {
      console.log(item.value);
      this.myControlItem = null;
    }
  }
}

When running this code, I press the A, S, D, and F keys in the <input> element.
After pressing the A key, the console shows a and the <input> is cleared. (Nice.)
After pressing the S key, the console shows s, but the <input> is not cleared. (Huh?)
After pressing the D key, the console shows sd and the <input> is still not cleared. (Hey!)
After pressing the F key, the console shows sdf and the <input> is still not cleared. (Aargh!)

Is this a known issue? How can I get this to work so that the <input> element will be cleared after every value change, and not only after the first value change?

Thank you very much in advance.

1 Like

In this case, I think it’s because of the combination of bindalbes are queued & processed asynchronously and the way values are changed during change handler. I remember there’s a similar issue reported, but can’t recall the exact link. Have you been able to work this out, or work around it?

I think the problem is when you set this.myControlItem = null; the binding is broken.

1 Like

Hi @bigopon,

Thanks for the answer. I also thought it might have something to do with some weird back-and-forth-observable callback execution.

Yes, luckily I have been able to work around it. I chose the easiest way: a complete redesign and reimplentation. I thought everything over and started from scratch. It is working now, but I am not so sure what I did wrong the first time. Let’s just say my first design was cr@p. :wink:

1 Like

Hi @khuongduybui,

Hmmm. I was wondering if that could actually be the case. So I tried it out with a little test app.

It turns out that the bindings of a bindable property are not broken if the property is set to null. It just works as expected. :relieved:

Below is my testing code, so you can try it for yourself:

my-control.html:

<template>
  <span if.bind="item">Item value is '${item.value}'</span>
  <span else>Item is ${item === null ? 'null' : 'undefined'}.</span>
</template>

my-control.ts:

import { bindable, bindingMode } from 'aurelia-framework';

export interface Item {
  value: string;
}

export class MyControl {
  @bindable({ defaultBindingMode: bindingMode.twoWay }) item?: Item | null;
}

app.html:

<template>
  <require from="./my-control"></require>

  <div>
    <input value.bind="itemValue">
    <button click.delegate="setItem()" disabled.bind="!itemValue">Set item</button>
    <button click.delegate="clearItem(null)" disabled.bind="myControlItem === null">Clear item to null</button>
    <button click.delegate="clearItem(undefined)" disabled.bind="myControlItem === undefined">Clear item to undefined</button>
  </div>

  <div>
    <my-control item.bind="myControlItem"></my-control>
  </div>
</template>

app.ts:

import { Item } from './my-control';

export class App {
  myControlItem?: Item | null;
  itemValue!: string;

  setItem() {
    this.myControlItem = { value: this.itemValue };
  }

  clearItem(emptyValue: null | undefined) {
    this.myControlItem = emptyValue;
  }
}

But thanks anyway. :slight_smile:

1 Like

Ah I see, you are right, because observation was not put on the “item” object itself, but on the “item” property of the class.

2 Likes