Better know a framework #22: bind and attached

Base doc:
https://aurelia.io/docs/templating/html-behaviors#html-behavior-lifecycle

  • bind() typically happens after view and view model bindings have been created and bound. At this point, view has also been created and added to the host element, but the host element has not been connected to the document yet.
  • attached() typically happens after the host element of a custom element/attribute has been connected to the document.

During bind(), all bindings have been bound:

  • view model has access to the view elements via ref bindings. This is a great time to do heavy DOM modification setup, as it doesn’t trigger browser to re-layout or re-render.
  • parent data has been passed to child via bound binding, it’s good, and early enough to initiate data related setup. For examples: create third party utilities instances, setup observer on elements, create dynamic bindings etc…
  • view has not been connected to the document, so anything that requires real element sizing will not work. Typical apis: window.getComputedStyle, element.getBoundingClientRect(), element.offsetHeight, element.clientHeight etc…

During attached(), all bindings have been bound and view has been connected to the document:

  • animation can be run, when there is need for visual feedback.
  • element sizing can be used. This is great for setup that needs some specific calculation, such as canvas rendering, text/code editor resizing.
  • document.querySelector(...), jQuery(...) can be used, it can be used for some third party library that relies on selctor

What do you use?

5 Likes

Some examples (not nearly exhaustive, but these would be fairly common):

Bind()

  • Initialize models in a component based on values passed to the bindings (vCurrent)
@customElement('product-details')
export class ProductDetails {
  public static inject = [ProductService, CompositionTransaction];

  @bindable public productId: number;

  public product: ProductModel;
  public service: ProductService;
  public notifier: CompositionTransactionNotifier;

  constructor(productService, compositionTransaction) {
    this.service = productService;
    this.notifier = compositionTransaction.enlist();
  }

  public bind(): void {
    this.service.getProduct({ id: this.productId })
      .then(product => {
        this.product = product;
        this.notifier.done();
      });
  }
}
  • Initialize models in a component based on values passed to the bindings (vNext)
@customElement('product-details')
export class ProductDetails {
  public static inject = [ProductService];

  @bindable public productId: number;

  public product: ProductModel;
  public service: ProductService;

  constructor(productService) {
    this.service = productService;
  }

  public async binding(): Promise<void> {
    // Promises returned from binding and unbinding are awaited
    // before the lifecycle continues in vNext
    this.product = await this.service.getProduct({ id: this.productId });
  }
}
  • Add/remove event listeners (also fine during attached()), vCurrent (vNext is identical here except it’s binding() instead of bind())
@customElement('draggable-widget')
export class DraggableWidget {
  public static inject = [Element];

  constructor(
    public element: Element
  ) {}

  public bind(): void {
    this.element.addEventListener('mousedown', this.handleMouseDown);
  }

  public unbind(): void {
    this.element.removeEventListener('mousedown', this.handleMouseDown);
  }

  private readonly handleMouseDown = (ev: MouseEvent) => {
    window.addEventListener('mousemove', this.handleMouseMove);
    window.addEventListener('mouseup', this.handleMouseUp);
  };

  private readonly handleMouseUp = (ev: MouseEvent) => {
    window.removeEventListener('mousemove', this.handleMouseMove);
    window.removeEventListener('mouseup', this.handleMouseUp);
  };

  private readonly handleMouseMove = (ev: MouseEvent) => {
    // magic to move the element
  };
}

Attached()

  • Initialize 3rd party UI plugins that require the DOM to be attached and ready
@customElement('tabs-container')
export class TabsContainer {
  public static inject = [Element];

  constructor(
    public element: Element
  ) {}

  public attached(): void {
    $(this.element).init(...); // some jQuery/bootstrap stuff
  }

  public detached(): void {
    $(this.element).destroy(...); // some jQuery/bootstrap stuff
  }
}
2 Likes

I can remember @EisenbergEffect writing , that waiting for lifecycle events would be a performance disaster and wouldn´t map to web components.
How is the vNext different from what OP proposed / how did you solve the said performance issues?

Best

1 Like

There are a lot of things that would be a problem in the current design of Aurelia 1, but we’ve found ways to open up new possibilities for vNext, including clever ways to mitigate the costs of these types of convenience abstractions. That’s the short answer at least.

3 Likes

Examples are simplified / not actual framework code, but to illustrate the point.

In v1, to support async hooks, we’d have to make everything async and do the equivalent of this in the controller:

await this.viewModel.bind();

this.isBound = true;

this.doNextThing();

Whole method has to be async (or everything needs to be wrapped in Promise.resolve - either way, we get the full overhead even in sync situations)

Here’s what the process boils down to in v2:

const result = this.viewModel.binding();
if (result instanceof Promise) {
  return result.then(this.doNextThing);
} else {
  return this.doNextThing();
}

We’ve split lifecycle logic up into methods/functions in such a manner that we can use conditional sync/async continuation.

Everything stays synchronous while being able to deal with promises whenever they show up. Even then it stays synchronous before and after awaiting the promise.

We can’t do this in v1 without breaking stuff and running into issues with the task queue. In v2 it’s all rewritten from the ground up to accommodate for this and other things.

2 Likes