2

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?

Bart Hofland
  • 3,700
  • 1
  • 13
  • 22

1 Answers1

4

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

Arne Deruwe
  • 1,100
  • 2
  • 11
  • 25
  • Thank you for your answer. :) This indeed seems to be the most elegant solution. I did have to make some minor adjustments to your solution code due to my stricter TypeScript configuration, but it works perfectly. I hoped that the `bind` method could be avoided even here, but in production code I would probably need an `unbind` method as well for cleaning up the styling. I must say that after posting my question, I was in doubt if my scenario was somewhat too exotic. When a custom attribute's logic becomes so complex, I would also start thinking about migrating it to a custom element instead. – Bart Hofland Jul 07 '20 at 12:29