Search code examples
typescriptdata-bindingaureliaaurelia-templating

Aurelia: data binding issue with the primary property of a custom attribute


I am learning about Aurelia for a few weeks now and I seem to have a data binding issue with a custom attribute.

I have created the following square custom attribute (based on the examples from the "Templating:Custom Attributes" guide on the Aurelia website):

square.ts:

import { bindable, autoinject } from "aurelia-framework";

@autoinject
export class SquareCustomAttribute {
  @bindable sideLength = "100px";
  @bindable color = "red";

  constructor(private element: Element) {
    this.sideLengthChanged(this.sideLength);
    this.colorChanged(this.color);
  }

  sideLengthChanged(newValue: string) {
    if (this.element instanceof HTMLElement) {
      this.element.style.width = this.element.style.height = newValue;
    }
  }

  colorChanged(newValue: string) {
    if (this.element instanceof HTMLElement) {
      this.element.style.backgroundColor = newValue;
    }
  }
}

I would like this custom attribute to be usable without explicitly binding it, in which case it should use the default values, like in this consuming view:

app.html:

<template>
  <require from="./square"></require>
  <div square></div>
</template>

The code above works fine. It renders the div as a square with 100px sides and a red background.

Problems arise when I set the color property of SquareCustomAttribute as the primary property (using @bindable's configuration object parameter) like this:

Updated square.ts:

import { bindable, autoinject } from "aurelia-framework";

@autoinject
export class SquareCustomAttribute {
  @bindable sideLength = "100px";
  @bindable({ primaryProperty: true }) color = "red";

  constructor(private element: Element) {
    this.sideLengthChanged(this.sideLength);
    this.colorChanged(this.color);
  }

  sideLengthChanged(newValue: string) {
    if (this.element instanceof HTMLElement) {
      this.element.style.width = this.element.style.height = newValue;
    }
  }

  colorChanged(newValue: string) {
    if (this.element instanceof HTMLElement) {
      this.element.style.backgroundColor = newValue;
    }
  }
}

For some reason, setting color as the primary property of the custom attribute, the colorChanged callback gets invoked twice now: first by the constructor with the default value and then once more from somewhere in the lifecycle initialization with an empty value.

How can I avoid this second invocation of the colorChanged callback, so that the default value of the primary property of my custom attribute will not be cleared when I do not explicitly supply a binding/value of the square attribute in the consuming view's HTML markup?


Solution

  • You will have to tackle this another way:

    import { bindable, autoinject } from "aurelia-framework";
    
    @autoinject
    export class SquareCustomAttribute {
      @bindable sideLength;
      @bindable({ primaryProperty: true }) color;
    
      constructor(private element: Element) {
      }
    
      sideLengthChanged(newValue: string) {
        if (this.element instanceof HTMLElement) {
          this.element.style.width = this.element.style.height = newValue;
        }
      }
      
      bind(){
        this.sideLengthChanged(this.sideLength ? this.sideLength : "100px");
        this.colorChanged(this.color ? this.color : "red");
      }
    
      colorChanged(newValue: string) {
        if (this.element instanceof HTMLElement) {
          this.element.style.backgroundColor = newValue;
        }
      }
    }
    

    When you declare { primaryProperty: true }, you are basically telling the framework to create a binding on the value of your custom attribute, whether it is filled or not, the framework will map it to your color property. so when you declare <div square></div>, the color property in the bind() lifecycle will be an empty string. As bind() is only invoked once, it's the perfect spot to declare your default values, should they be empty in the beginning.

    Example here: https://gist.dumber.app/?gist=b0244ac4078e2a0664b7be0fbcc0b22b