Search code examples
javascriptaureliacustom-attribute

Aurelia Custom attribute which changes the elements inner HTML


Use Case

I have a custom attribute which changes the content of the element it is attached, e.g. from non bold to bold.

Problem

With normal binding, the inner HTML does not get changed. I think, Aurelia is losing the binding as soon as the HTML is updated with "plain" Javascript.

You can see it in this GistRun: https://gist.run/?id=6f16ac1c8affb0277d7ad5c53d433482

Change the text in the area. There are five cases:

  • Case 1: no attribute. Text updated. This is fine.
  • Case 2: the text is not updated. Expected: update the text, but it should not be bold (missing value change call).
  • Case 3: text is updated but not bold (missing value change call). This is fine.
  • Case 4: text is not updated. Exptected: update text and make it bold
  • Case 5: text is updated and made bold. This is fine.

Question

Can someone explain me, what is happing internally with case 2 and 4?


CODE:

app.html

<template>
  <require from="./bold-custom-attribute"></require>

  <textarea value.bind="content"></textarea>
  <pre>${content}</pre>
  <pre bold>${content}</pre>
  <pre bold textcontent.bind="content"></pre>
  <pre bold.bind="content">${content}</pre>
  <pre bold.bind="content" textcontent.bind="content"></pre>
</template>

bold-custom-attribute.js

export class BoldCustomAttribute {
  static inject = [Element];

  constructor(element) {
    this.element = element;
  }

  bind() {
    //this.bolden(this.element);
  }

  attached() {
    this.bolden(this.element);
  }

  valueChanged(newValue, oldValue){
    this.bolden(this.element);
  }

  bolden(anElement) {
    anElement.innerHTML = '<b>' + anElement.innerText + '</b>';
  }
}

Solution

  • I was able to find out what is happening exactly by a combination of the post of Ashley, Fabio and the right place to debug.

    The ${content} string interpolations all get a ChildInterpolationBinding binding to the text node they form (which is within the <pre>, although not visible directly in e.g. Chrome Debugger), or more precisely to the textContent property. This means, as soon as the inner HTML of the <pre> is replaced in bolden(), the text node disappears from the DOM (however, the binding is still there and updating the text nodes textContent). The new nodes have no binding of course (explaining why case 4 is not working).

    Now, the differnce in case 5 (textcontent.bind) is that the Binding is directly attached to the <pre> it is specified on, or more precisely on the pre.textContent property. If the content within <pre> changes (e.g. via textContent or innerHTML), the binding is still on the correct node (<pre>).

    EDIT An important point here mentioned by Ashley is that the textcontent binding is called before the valueChanged(). This makes case 5 work at all. First, the textcontent is updated (with the binding which does not get lost), then, the value change is triggered reading in the just updated text content and applying the new HTML tags.

    Because of this point, case 4 is working for one input change. After VM construction, the binding is correct and changing the value work. The interpolation binding is updating the text node, then valueChanged() is called and the text node with the bindings disappears from the DOM.


    I was playing around a little bit, and managed to change the binding. For sure, this should not be used in production. When adding created(), everyhting works "fine".

    export class BoldCustomAttribute {
      static inject = [Element];
    
      constructor(element) {
        this.element = element;
      }
    
      created(owningView, myView) {
        if (this.element.hasChildNodes() && this.element.firstChild.auInterpolationTarget === true) {
          owningView.bindings.forEach(binding => {
            if (binding.target === this.element.firstChild) {;
              binding.target = this.element;
              binding.targetProperty = 'innerHTML';
            }
          });
        }
      }
    
      bind() {
        this.bolden(this.element);
      }
    
      valueChanged(newValue, oldValue){
        this.bolden(this.element);
      }
    
      bolden(anElement) {
        anElement.innerHTML = '<b>' + anElement.innerText + '</b>';
      }
    }