aureliaaurelia-templating

Change element type at runtime


Is it possible to dynamically define the type of an element inside a custom components template at runtime?

I'd like to avoid duplication of the inner contents of the button and a element in the following example:

<template>
    <button if.bind="!isLinkBtn">
        <span class="btn-icon">${icon}</span>
        <span class="btn-text">${contentText}</span>
    </button>

    <a if.bind="isLinkBtn">
        <!--
        The content is a 1:1 duplicate of the button above which should be prevented
        somehow in order to keep the view DRY
        -->
        <span class="btn-icon">${icon}</span>
        <span class="btn-text">${contentText}</span>
    </a>
</template>

Is it possible to write something like this:

<template>
    <!--
    The type of element should be defined at runtime and can be a standard HTML "button"
    or an anchor "a"
    -->
    <element type.bind="${isLinkBtn ? 'a' : 'button'}">
        <span class="btn-icon">${icon}</span>
        <span class="btn-text">${contentText}</span>
    </element>
</template>

I'm aware of dynamic composition with <compose view="${widget.type}-view.html"></compose> but as far as I know, this won't allow me to create default HTML elements but only custom components, correct?

I've asked this question on the Aurelia Gitter where Erik Lieben suggested to use a @processContent(function) decorator, replace the content within the given function and return true to let Aurelia process it.

Unfortunately I don't know how to actually apply those instructions and am hoping for some alternative approaches here or some details about how to actually accomplish this.


Edit

I've created a corresponding feature request. Even though possible solutions have been provided, I'd love to see some simpler way to solve this ;)


Solution

  • When you want to reuse HTML snippets, use compose. Doing so does not create a new custom element. It simply includes the HTML at the location of each compose element. As such, the view-model for the included HTML is the same as for the element into which it is composed.

    Take a look at this GistRun: https://gist.run/?id=36cf2435d39910ff709de05e5e1bedaf

    custom-link.html

    <template>
        <button if.bind="!isLinkBtn">
          <compose view="./custom-link-icon-and-text.html"></compose>
        </button>
    
        <a if.bind="isLinkBtn" href="#">
          <compose view="./custom-link-icon-and-text.html"></compose>
        </a>
    </template>
    

    custom-link.js

    import {bindable} from 'aurelia-framework';
    
    export class CustomLink {
        @bindable() contentText;
        @bindable() icon;
        @bindable() isLinkBtn;
    }
    

    custom-link-icon-and-text.html

    <template>
        <span class="btn-icon">${icon}</span>
        <span class="btn-text">${contentText}</span>
    </template>
    

    consumer.html

    <template>
      <require from="./custom-link"></require>
      <custom-link content-text="Here is a button"></custom-link>
      <custom-link is-link-btn.bind="true" content-text="Here is a link"></custom-link>
    </template>
    

    You may want to split these into separate elements, like <custom-button> and <custom-link> instead of controlling their presentation using an is-link-btn attribute. You can use the same technique to reuse common HTML parts and composition with decorators to reuse the common code.

    See this GistRun: https://gist.run/?id=e9572ad27cb61f16c529fb9425107a10

    Response to your "less verbose" comment

    You can get it down to one file and avoid compose using the techniques in the above GistRun and the inlineView decorator:

    See this GistRun: https://gist.run/?id=4e325771c63d752ef1712c6d949313ce

    All you would need is this one file:

    custom-links.js

    import {bindable, inlineView} from 'aurelia-framework';
    
    function customLinkElement() {
        return function(target) {
            bindable('contentText')(target);
            bindable('icon')(target);
      }
    }
    
    
    const tagTypes = {button: 'button', link: 'a'};
    
    
    @inlineView(viewHtml(tagTypes.button))
    @customLinkElement()
    export class CustomButton {
    
    }
    
    
    @inlineView(viewHtml(tagTypes.link))
    @customLinkElement()
    export class CustomLink {
    
    }
    
    
    function viewHtml(tagType) {
      let result = `
        <template>
            <${tagType}${tagType === tagTypes.link ? ' href="#"' : ''}>
                <span class="btn-icon">\${icon}</span>
                <span class="btn-text">\${contentText}</span>
            </${tagType}>
        </template>
        `;
    
      return result;
    }