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.