Custom attribute fails when using updateTrigger behavior

I’m using a custom attribute that handles user input as number, e.g.

<input type="number" min="1" number-value.bind="fooNum">

The code for the custom behavior is

import {autoinject, bindingMode, customAttribute} from 'aurelia-framework';

@customAttribute('number-value', bindingMode.twoWay)
@autoinject
export class NumberValueCustomAttribute {
  private inputElement: HTMLInputElement;
  private value: number = NaN; // Bound value set by Aurelia

  constructor(inputElement: Element) {
    this.inputElement = inputElement as HTMLInputElement;
  }

  // noinspection JSUnusedGlobalSymbols
  /**
   * Hook into Aurelia component lifecycle
   * Data binding is activated on the view and view-model
   * @param _bindingContext the primary binding context that this view is data-bound to
   * @param _overrideContext the override context which contains properties capable of overriding those found on the binding context
   */
  bind(_bindingContext: object, _overrideContext: object): void {
    this.valueChanged(); // Since we implement both the bind and valueChanged callbacks, only bind will be called when the value is initially bound
    this.inputElement.addEventListener('blur', this.inputValueChanged.bind(this));
  }

  // noinspection JSUnusedGlobalSymbols
  /**
   * Items have changed
   * @param _newValue the new value
   * @param _oldValue the old value
   */
  valueChanged(_newValue?: string, _oldValue?: string): void {
    // Synchronize the input element with the bound value
    if (this.value == undefined || Number.isNaN(this.value)) {
      this.inputElement.value = '';
    }
    else {
      this.inputElement.value = this.value.toString(10);
    }
  }

  // noinspection JSUnusedGlobalSymbols
  /**
   * Hook into Aurelia component lifecycle
   * Component is unbound
   */
  unbind(): void {
    this.inputElement.removeEventListener('blur', this.inputValueChanged.bind(this));
  }

  /**
   * Handle the input element's value changing
   * @param _event the focus event
   * @private
   */
  private inputValueChanged(_event: FocusEvent): void {
    this.value = Number.parseFloat(this.inputElement.value);
  }
}

But when I try to use it with the binding behavior updateTrigger, e.g.

<input type="number" min="1" number-value.bind="fooNum & updateTrigger:'input'">

I get the error

The updateTrigger binding behavior can only be applied to two-way/ from-view bindings on input/select elements.

And it’s actually coming from this line of code:

// ensure the binding's target observer has been set.
let targetObserver = binding.observerLocator.getObserver(binding.target, binding.targetProperty);
if (!targetObserver.handler) {
  throw new Error(notApplicableMessage);
}

Any ideas on how to get my custom attribute to work with the updateTrigger binding behavior?

1 Like

You will need to make the property “native” to the element and register an observer adapter.

See how it’s done for value property of a mdc-text-field custom element here

and

1 Like

You can see from the line that throws error here https://github.com/aurelia/templating-resources/blob/c80c782b314628ec20e9a8aae88868da4f168d44/src/update-trigger-binding-behavior.ts#L21

It’s throwing because it cannot find a property handler on the observer of the property value of your number-input custom attribute. The reason is it’s supposed to work in bindings for native input elements only.

I’m not sure how to make it work together yet, but since you want to have events configurable, maybe do

  bind(_bindingContext: object, _overrideContext: object): void {
    this.valueChanged(); // Since we implement both the bind and valueChanged callbacks, only bind will be called when the value is initially bound
-   this.inputElement.addEventListener('blur', this.inputValueChanged.bind(this));
+   this.inputElement.addEventListener(this.event, this.inputValueChanged.bind(this));
  }

and have event as a bindable property on the number input attribute? then in your template, it could be like this:

<input type=number number-value="value.bind: fooNum; event: 'input'">

you really don’t need all of that at all.
html input already have a native way of extracting the value as a number, and aurelia can bind directly to it.

search for valueAsNumber in: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement

so just use <input type="number" value-as-number.two-way="myVal" />,
no extra custom attribute required.

the reason we need two-way instead of regular bind. is because aurelia’s default binding mode for value is two-way be default. but for value-as-number is not.

2 Likes

Thanks @bigopon, this is exactly what I ended up doing and it worked perfectly. Actually, no quotes on the event, i.e. number-value="value.bind: fooNum; event: input".

I was just wondering if there was an easy way to get my input / custom attribute to behave more like a native element.

Thanks again!

1 Like

That’s awesome, thanks for the heads up! However, it didn’t work when I tried

<input type="number" min="1" value-as-number.two-way="fooNum & updateTrigger:'input'">

Throws the same error on the same line (targetObserver.handler is undefined).

1 Like

Thanks @MaximBalaganskiy, I ended up going with a different solution, but was wondering how to make my element more native.

1 Like

this is very weird, because valueAsNumber is a native property just like value.
It seems logical to me that whatever work aurelia has done for value, can be applied similarly on valueAsNumber.

I assume there is a if somewhere in the framework, that disallow wrong uses of bindings, and that if didn’t take valueAsNumber and valueAsDate into account.

the same way we can see that the default bindingMode for those properties are not two-way, but its very logical that they should be.

they should behave exactly like value does.

if we find and fix that behavior in the framework - this code (and many other bits of tiny things) should work auto-magiclly.

2 Likes

The respective part where that happens is here templating-binding/syntax-interpreter.js at master · aurelia/templating-binding · GitHub

1 Like

well… valueAsNumber and valueAsDate should be added to that list.
but I’m not entirely sure that will fix the current issue - their must also be other places where this kind of logic take place. right?

1 Like

Beside that part, Aurelia also needs to be taught what events to use for observing the input element as well. In v2, this’ll be easier, I’ll follow up with an example how to do it.

3 Likes

The syntax is the same with v1, you can see an example here Dumber Gist

valueAsDate is quirkier than valueAsNumber as the way the date value & input type works. I’m not sure whether we want to support it in the core.

1 Like