Nested & dynamic custom element composition

I’ve followed v2 since the beginning, and after alpha release have tried porting an application from v1, but haven’t gone very far because haven’t been able to make some very basic stuff working with custom elements.

  1. Using custom elements inside another custom element

Dumber gist: Dumber Gist

If there are two custom elements, both can be used separately, but it seems using custom element inside another custom element does not work. In the example, “custom element A” is rendered when used in top level, but not if used inside “custom element B”.

  1. Using au-compose

Dumber gist: Dumber Gist

In the example, there are 3 items which can be selected by clicking a button, and it is supposed to render associated custom element whenever an item is selected. Two things that don’t seem to work

  • I cannot pass/bind attributes which would be visible inside the custom element. In the example, au-compose has “model.bind” but this is not visible in the component, rendering “with model: undefined”
  • Whenever actual component class changes (items A/B → C or vice versa), the au-compose DOM element is not updated but instead a new one is added.

I’ve gone through issues in Github, but haven’t found ones that would cover these. Is there something I have missed in the way these should be implemented?

1 Like

The 2nd example worked in our first few versions of <au-compose/>, though after a few refactoring, it lagged behind a bit since we need to come up with a way to address the old usage of v1 <compose/>. Dynamic composition got some hiccups for now, will be fixed.
For the first example, the convention plugin is not working as intended, as deleting the decorator code in custom-element-b works https://gist.dumber.app/?gist=b0803983cdb504a8d96e2fa9364d5ab6

Thanks for the 2nd example, it reminds that we still need to finish the port of v1 compose

1 Like

Do I understand you correctly: if the customElement decorator is used, the import tag inside the template is ignored?

1 Like

It shouldn’t be ignored, should be more of “not recognized correctly”.
cc @huochunpeng

1 Like

There is nothing wrong with the conventions. Please do not write manual decorator unless necessary. The misunderstanding of the behaviour comes from an assumption that the Aurelia 2 html module behaves same as Aurelia 1 html module, which is not the case.

When you write manually, the template you imported is a string.

import template from "./custom-element-b.html";
@customElement({ name: "custom-element-b", template })

Which is

<import from="./custom-element-a"></import>
<!-- the line above is translated as another esm module named export,
NOT in the template string -->

<!-- template string only contains content below -->
<div style="border: 1px solid blue">
  <div style="color: blue">This is custom element B</div>

  <span>And here should be custom element A:</span>
  <custom-element-a></custom-element-a>
  <span>(but it is not rendered)</span>
</div>

The fix is very simple, never write decorator manually, unless you fully understand its behaviour.
Just write the custom-element-b.js plainly like following (do the same for other custom elements too).

export class CustomElementB {
}

More details
When you write custom element plainly, the Aurelia 2 conventions will fill up the boilerplate decorator as following:

import * as __au2ViewDef from './custom-element-b.html';
import { customElement } from '@aurelia/runtime-html';
@customElement(__au2ViewDef)
export class CustomElementB {
}

Note it imports the full namespace (import * as __au2ViewDef) from the html module, NOT just the default export (which is only a string containing the html part, not the meta data parts).

For reference, the custom-element-b.html is transpiled at compile time (this is very different from Aurelia 1 which does parsing template at app running time) to following code:
Note the export default template which was what you imported manually.

import { CustomElement } from '@aurelia/runtime-html';
import * as d0 from "./custom-element-a";
export const name = "custom-element-b";
export const template = "\n\n<div style=\"border: 1px solid blue\">\n  <div style=\"color: blue\">This is custom element B</div>\n\n  <span>And here should be custom element A:</span>\n  <custom-element-a></custom-element-a>\n  <span>(but it is not rendered)</span>\n</div>\n";
export default template;
export const dependencies = [ d0 ];
let _e;
export function register(container) {
  if (!_e) {
    _e = CustomElement.define({ name, template, dependencies });
  }
  container.register(_e);
}

In contrast, Aurelia 1 html module is the plain string of the original html file content. All the magic is done by Aurelia 1 at runtime, which includes finding dependencies, and also figures out the element name itself. Aurelia 1 parses the full html string at runtime for those magic. This runtime behaviour is exactly why we need such a big webpack plugin to teach webpack about Aurelia 1’s runtime behaviour (so that webpack knows how to find dependencies at compile time).

Aurelia 2 runtime engine is much simpler, we do all the magic in compile time. That’s why the compiled html file module is much more than such that plain string.

3 Likes

Great explication - thanks!

1 Like

Thanks for explanation! I wouldn’t have used the decorator at all, but in the early stages this seemed to be the only way get it working at all, got it somewhere from this forum and never tried it again without.

1 Like

Regarding au-compose, I’m still missing how should I pass data into component with au-compose?

Using the same gist (Dumber Gist), it now works in terms of switching the proper component, but I would like to bind some data to the component.

It would be intuitive to use
<au-compose view-model.bind="selected.component" name.bind="selected.name" />
in order to bind data into property called “name” inside the component, but this does not work.

I am not quite sure if this kind of direct binding is working yet. By now you could catch the model just like in aurelia 1:

In app.html

<au-compose view-model.bind="selected.component" model.bind="{ name: selected.name}" />

And in the custom components

activate(model) {
    this.name = model?.name;
  }

Regards.

Yes, that works, thanks!

But it would be nice to know if this is how it is intended, or will there be something like direct binding. I don’t know how that would work with activate lifecycle event, but for me this combination of “view-model.bind” and “model.bind” does seem a bit counter-intuitive.