3

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 ;)

suamikim
  • 5,350
  • 9
  • 40
  • 75
  • `processContent` will not help in this case since it won't have access to the view-model's properties (like `isLinkBtn`). – Jeff G Mar 02 '17 at 16:35

2 Answers2

3

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;
}
Jeff G
  • 1,996
  • 1
  • 13
  • 22
  • 1
    One thing to keep in mind is that using `compose` to bring in small pieces of HTML is going to be a lot lower performance than just not being DRY and inlining the code. In the specific example of this question, I would probably lean towards repeating the HTML instead of using `compose`. If we're bringing in larger blocks of HTML w/`compose`, then that makes more sense. The answer you have given is correct, just with the caveat I just mentioned. – Ashley Grant Mar 02 '17 at 18:32
  • 1
    Agreed Ashley. It is impossible to tell whether performance will be an issue without knowing the context(s) in which the code will run. I always start out using DRY unless I know the runtime environment will require maximum efficiency. Then, if testing shows unacceptable performance, I will purposely violate DRY where necessary to improve speed. – Jeff G Mar 02 '17 at 22:36
  • I was hoping for a less "verbose" way to accomplish this which doesn't require splitting up the elements shell (`button` or `a`) & the content since the content only consists of 2 elements which "can't live" independently and therefore don't really need to be a separated component. – suamikim Mar 03 '17 at 08:06
1

Sorry, I was doing 2 things at once while looking at gitter, which I am not good at apparently :-)

For the thing you wanted to accomplish in the end, could this also work?

I am not an a11y expert or have a lot of knowledge on that area, but from what I understand, this will accomplish what you want. The browser will look at the role attribute and handle it as a link or button and ignores the actual element type itself / won't care if it is button or anchor it will act as if it is of the type defined in role.

Then you can style it like a button or link tag with css.

<a role.bind="type"><span>x</span><span>y</span></a>

where type is either link or button, see this: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_link_role

Erik Lieben
  • 1,249
  • 6
  • 12
  • I guess this would work and even though I can't actually argue why, I'd still prefer to explicitly use the semantically correct tag whenever possible. I'd use the `role` in situations where its technically not possible to use the correct tag (e.g. custom `comboboxes` composed of `divs` or anything the like etc.). Thanks either way for the suggestion! – suamikim Mar 03 '17 at 08:13