Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aurelia dynamic binding

Tags:

aurelia

I've created a custom element that generates tabular data. For good reasons, this generates the actual HTML and inserts into the DOM without using a template.

I need to attach click observers to specific elements to I can run a function in the custom element in response to a click. If using a template, I'd use click.delegate, but I can't use that with generated HTML.

How do you attach an event handler with Aurelia other than by using jQuery?

like image 768
SenseDeep Avatar asked Mar 31 '16 02:03

SenseDeep


People also ask

How do I bind HTML in Aurelia templates?

Binding using the innerhtml attribute simply sets the element's innerHTML property. The markup does not pass through Aurelia's templating system. Binding expressions and require elements will not be evaluated. The binding system makes several properties available for binding in your templates, depending on the context.

What is dynamic composition in Aurelia?

In many respects, dynamic composition closely resembles that of how Aurelia's routing works. The big exception, of course, is dynamic composition allows you to dynamically render views and view-models after the page has loaded.

Does the markup pass through Aurelia's templating system?

The markup does not pass through Aurelia's templating system. Binding expressions and require elements will not be evaluated. The binding system makes several properties available for binding in your templates, depending on the context. $this - The binding context (the view-model).

How do I use the <compose> element in Aurelia?

When using Aurelia's <compose> element, inside of the view-model being used, you have access to all of Aurelia's standard view lifecycle events, such as attached, as well as the other callback hooks. Using the <compose> element, we are going to be dynamically composing a view.


1 Answers

I know this answer is late, but in case this hasn't been (properly) solved yet and/or someone else finds this in the future:

In order to make any aurelia behavior work in dynamically generated HTML, you need to compile that HTML.

I have worked on a custom element (based on how aurelia's enhance and compose work) that allows you to pass in a string of HTML and it will then be compiled, so that any behaviors like bindables, custom elements / attributes will just work. It will also re-compile when the html changes.

Here's an example: https://gist.run?id=1960218b52ba628f73774822aef55ad7

src/app.html

<template> 
  <dynamic-html html.bind="dynamicHtml"></dynamic-html>
</template>

src/app.ts

export class App {
  public dynamicHtml: string = `
<button click.delegate="handleClick()">Click me</button>
`;

  public handleClick(): void {
    alert("Hello!")
  }
}

src/dynamic-html.ts

import {
    customElement,
    TaskQueue,
    bindable,
    ViewCompiler,
    ViewSlot,
    View,
    ViewResources,
    Container,
    ViewFactory,
    inlineView,
    inject,
    DOM
} from "aurelia-framework";

@customElement("dynamic-html")
@inlineView("<template><div></div></template>")
@inject(DOM.Element, TaskQueue, Container, ViewCompiler)
export class DynamicHtml {
    @bindable()
    public html: string;

    public element: HTMLElement;

    private tq: TaskQueue;
    private container: Container;
    private viewCompiler: ViewCompile;

    private runtimeView: View;
    private runtimeViewSlot: ViewSlot;
    private runtimeViewFactory: ViewFactory;
    private runtimeViewAnchor: HTMLDivElement;

    constructor(element, tq, container, viewCompiler) {
        this.element = <HTMLElement>element;
        this.tq = tq;
        this.container = container;
        this.viewCompiler = viewCompiler;
    }

    public bindingContext: any;
    public overrideContext: any;
    public bind(bindingContext: any, overrideContext: any): void {
        this.bindingContext = bindingContext;
        this.overrideContext = overrideContext;

        if (this.html) {
            this.htmlChanged(this.html, undefined);
        }
    }

    public unbind(): void {
        this.disposeView();

        this.bindingContext = null;
        this.overrideContext = null;
    }

    public needsApply: boolean = false;
    public isAttached: boolean = false;
    public attached(): void {
        this.runtimeViewAnchor = this.element.firstElementChild;

        this.isAttached = true;
        if (this.needsApply) {
            this.needsApply = false;
            this.apply();
        }
    }

    public detached(): void {
        this.isAttached = false;

        this.runtimeViewAnchor = null;
    }

    private htmlChanged(newValue: string, oldValue: void): void {
        if (newValue) {
            if (this.isAttached) {
                this.tq.queueMicroTask(() => {
                    this.apply();
                });
            } else {
                this.needsApply = true;
            }
        } else {
            if (this.isApplied) {
                this.disposeView();
            }
        }
    }

    private isApplied: boolean = false;
    private apply(): void {
        if (this.isApplied) {
            this.disposeView();
        }

        this.compileView();
    }

    private disposeView(): void {
        if (this.runtimeViewSlot) {
            this.runtimeViewSlot.unbind();
            this.runtimeViewSlot.detached();
            this.runtimeViewSlot.removeAll();
            this.runtimeViewSlot = null;
        }

        if (this.runtimeViewFactory) {
            this.runtimeViewFactory = null;
        }

        if (this.runtimeView) {
            this.runtimeView = null;
        }

        this.isApplied = false;
    }

    private compileView(): void {
        this.runtimeViewFactory = createViewFactory(this.viewCompiler, this.container, this.html);

        this.runtimeView = createView(this.runtimeViewFactory, this.container);

        this.runtimeViewSlot = createViewSlot(this.runtimeViewAnchor);
        this.runtimeViewSlot.add(this.runtimeView);
        this.runtimeViewSlot.bind(this.bindingContext, this.overrideContext);
        this.runtimeViewSlot.attached();

        this.isApplied = true;
    }

}

function createViewFactory(viewCompiler: ViewCompiler, container: Container, html: string): ViewFactory {
    if (!html.startsWith("<template>")) {
        html = `<template>${html}</template>`;
    }
    let viewResources: ViewResources = container.get(ViewResources);
    let viewFactory = viewCompiler.compile(html, viewResources);
    return viewFactory;
}

function createView(viewFactory: ViewFactory, container: Container): View {
    let childContainer = container.createChild();
    let view = viewFactory.create(childContainer);
    return view;
}

function createViewSlot(containerElement: Element): ViewSlot {
    let viewSlot = new ViewSlot(containerElement, true);
    return viewSlot;
}
like image 92
Fred Kleuver Avatar answered Sep 24 '22 08:09

Fred Kleuver