Firing event from custom element only when value changes

What I am trying to achieve is to get the changed event to fire only when the new value has changed. With the control below, the changed event fires twice - once with the current value and then a second time with the new value.

<template>
  <div class="input-group">
    <div class="input-group-prepend">
      <span class="input-group-text">${label}</span>
    </div>
    <div class="d-inline">
      <select class="custom-select w-100" value.bind="value">
        <option if.bind="showOption">[${label}]</option>
        <option repeat.for="item of datasource" model.bind="item">${item.name}</option>
      </select>
    </div>
  </div>
</template>
import { autoinject, bindable, bindingMode } from "aurelia-framework";
import {TaskQueue} from "aurelia-framework";

@autoinject
export class FormSelectControl {
  @bindable public label = "Select";
  @bindable({ defaultBindingMode: bindingMode.twoWay }) public value;
  @bindable public datasource;
  @bindable public showOption = false;

  constructor(private readonly element: Element, private readonly taskQueue: TaskQueue) {
  }

  public valueChanged(newValue) {
    const evt = new CustomEvent("change", {
      bubbles: true,
      detail: newValue
    });

    this.taskQueue.queueMicroTask(() => {
      this.element.dispatchEvent(evt);
    });
  }
}

I’ve tried the obvious things such as

if(newValue !== oldValue) {
...
this.element.dispatchEvent(evt);
...
}

but the event never fires.

In the view

<form-select-control label="Select something" datasource.bind="myDataList"  value.bind="selectedItem" change.delegate="onSelectedItemChanged($event)"></form-select-control>

and in the model

public onSelectControlChanged(){
   this.loadDataFromServer(this.selectedItem.id);
// check selectedItem is the new data
}

It is here that I need to check that the selectedItem has changed, otherwise it hits the server for the data is currently in the viewModel and then again for the new value of the selectedItem.

How about using observable on selectedItem instead of triggering events?

GistRun example: https://gist.run/?id=ff8fc7f2844345c9e34186226041d4ca

Simplified ES6 code:

form-select-control.html

<template>
  <select value.bind="value">
    <option repeat.for="item of datasource" model.bind="item">${item.name}</option>
  </select>
</template>

form-select-control.js

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

export class FormSelectControl {
  @bindable({ defaultBindingMode: bindingMode.twoWay }) value;
  @bindable()datasource;
}

app.html

<template>
  <require from="./form-select-control"></require>
  <form-select-control datasource.bind="myDataList" value.bind="selectedItem"></form-select-control>
</template>

app.js

import { observable } from 'aurelia-framework';

export class App { 
  @observable()selectedItem;
  myDataList = [{name: "Jerry"}, {name: "George"}];
  
  selectedItemChanged(newValue, oldValue) {
    if (newValue.name !== oldValue.name) {
      alert(`new: ${JSON.stringify(newValue)} old: ${JSON.stringify(oldValue)}`);
    }
  }
}
2 Likes

@jeffgrann nice post :smile:

Thank you - I’d never realized how observable works :grin:

For those who are visiting this discussion topic looking for why the event is firing twice, it’s because the original event is bubbling and then you are firing off another. To handle changes on your custom element, all you would need to do is add a change.delegate on the custom element and it will catch the bubbling event.

Additionally, you may be binding the value of a model that is not observable. Handling the change event is helpful in these situations.

1 Like