powerbuoy
powerbuoy

Reputation: 12838

How to dynamically enhance HTML with Aurelia

We're storing all our page content and blog posts etc inside WordPress, using its API to render the data in our Aurelia app. This is working well;

<div class="excerpt" innerhtml.bind="post.excerpt.rendered"></div>

Authors would now like to be able to link to popups or use route-href or other custom Aurelia attributes from within their blog posts, however code added to the page using innerhtml.bind doesn't get parsed by Aurelia.

I love that a normal <a href="..."> "just works" in Aurelia - but we have plenty of custom attributes (like <button popup="name-of-popup">...</button> which can't be used by authors.

How can we overcome this?

Edit: With the comments from @bigopon I've started on something, but still can't quite get it to work. Either I suck at searching or the documentation is a little lacking on the TemplatingEngine.enhance() method, but I tried creating a custom attribute like this:

import {Aurelia, inject, TemplatingEngine} from 'aurelia-framework';

@inject(Element, Aurelia, TemplatingEngine)
export class AureliaEnhanceCustomAttribute {
    constructor (el, aurelia, templatingEngine) {
        this.el = el;
        this.aurelia = aurelia; // NOTE: I've never done this before - is it even correct?
        this.templatingEngine = templatingEngine;
    }

    attached () {
        this.el.innerHTML = this.value;

        // NOTE: Without checking for this we get an endless loop of attached() calls
        if (!this.el.classList.contains('au-target')) {
            this.templatingEngine.enhance({
                element: this.el,
                container: Aurelia.container, // NOTE: I'm guessing here
                resources: Aurelia.resources, // NOTE: Same here, but I need all my global resources to be available inside the enhanced element too
                bindingContext: {} // NOTE: Not sure what to pass in here at all.. :/
            });
        }
    }
}

And I'm using it like so:

<div aurelia-enhance.bind="exampleContent"></div>

Where exampleContent is a string fetched from an API call that could look something like this: '<my-custom-element></my-custom-element><button my-custom-attribute="some-value">Log in</button>'.

Upvotes: 2

Views: 1605

Answers (1)

bigopon
bigopon

Reputation: 1964

You are on right track. There are few things to consider

  • bindingContext / overrideContext: these two you can get by hooking into bind lifecycle of the custom attribute. So you will be able to pass them to the enhance instruction (requires only bindingContext, but passing both is better, helps traversing the scope). About bindingContext, 99% it will be the view model you are in, but you can always use a different object. In this case, this (custom attribute view model), is the right one.

  • resources: depends on how you want the resource scope of the view returned by TemplatingEngine.prototype.enhance will be. Passing global Aurelia instance's resources will not yield the local resources scope of the custom element it resides in. In order to have the same resources with the element the attribute annotates on, hook in to created lifecycle of the attribute, store the first parameter as owningView. This is the view of the custom element containing the attribute. Then you can access its resources by owningView.resources

  • cleaning: TemplatingEngine.prototype.enhance returns a View, you need to store this reference to detached and unbind in your attribute life cycle too

  • Sanitizing

An example: https://gist.run/?id=9dd32bc8a772526ae527f593e26b275b

The gist above is an example I of inheritance / enhance I made to answer another question on Discourse. You can have a look at the select-field to see more example there. It's similar to what you did there.

P/S: If you are happy with the solution, maybe consider writing a blog post / article or open a topic on Aurelia Discourse forum to help others, who may also struggle there.


Updated example:

import {Aurelia, inject, TemplatingEngine, DOM} from 'aurelia-framework';

@inject(Element, TemplatingEngine)
export class AureliaEnhanceCustomAttribute {
    constructor (el, aurelia, templatingEngine) {
        this.el = el;
        this.templatingEngine = templatingEngine;
    }

    // Custom attribute doesn't have its own view
    // Only the view of the custom element that owns it
    created(owningElementView, thisView) {
        this.owningElementView = owningElementView;
    }

    bind(bindingContext, overrideContext) {
        this.bindingContext = bindingContext;
        this.overrideContext = overrideContext;
    }

    attached () {
        this.el.innerHTML = this.value;

        // NOTE: Without checking for this we get an endless loop of attached() calls

        if (!this.el.classList.contains('au-target')) {
            this.dynamicView = this. this.templatingEngine.enhance({
                element: this.el,
                // we have two choices here, either the container of owning element, or this attribute
                // Lets go with the element, since it propbably has everything we need
                container: this.owningElementView.container,
                // the resources has information about its parent,
                // So we just need the resources of the element containing this attribute
                resources: this.owningElementView.resources,
                // Or this.bindingContext (same)
                bindingContext: this,
                // this helps travel up
                overrideContext: this.overrideContext
            });
        }
    }

    detached() {
        this.dynamicView.detached();
    }

    unbind() {
        if (this.dynamicView) {
            this.dynamicView.unbind();
        }
    }
}

Upvotes: 3

Related Questions