Tag Transformation
Background
Web components use a global registry to define and look-up custom elements (accessible via window.customElements).
If an application tries to define 2 custom elements with the same tag name (e.g. customElements.define('my-component', MyComponent) x2)
an error will be thrown and the application will fail.
It's quite common for multiple versions of a component library to be loaded into an application. For example:
- Application root loads version
v1.0.0of@component/librarywhich defines<my-button> /adminloads versionv2.0.0of@component/librarywhich also defines<my-button>with breaking changes
In this scenario the second version of the library will fail to load and throw an error, crashing the application.
Scoped custom element registries are a proposed web standard that would allow multiple versions of the same component library to be loaded into an application without conflicts. However at the time of writing vendor adoption is limited.
Until scoped custom element registries are more widely supported, Stencil provides runtime tag transformation utilities for use when authoring and consuming web components.
setTagTransformer and tagTransform
As of Stencil v4.39, Stencil makes available two utilities to help manage dynamic tag names:
setTagTransformerallows application developers (your component library consumers) to assign a tag transformer function.tagTransformis mainly used within your component libraries - transforming any static string tag names using the tag transformer function assigned viasetTagTransformer(Alternatively, you can auto applytagTransformto all tag names via theextras.additionalTagTransformersconfig option.)
Using setTagTransformer
Stencil exports setTagTransformer which you can optionally make available to application developers:
- Make
setTagTransformeravailable by exporting it from your component library's main entry point (e.g.src/index.ts):
// Within your lib's `src/index.ts` expose `setTagTransformer`
export { setTagTransformer } from '@stencil/core';
- Application developers can then import and use
setTagTransformer(before component definition)
Usage via the dist output
<!-- Setup a tag transformer -->
<script type="module">
import { setTagTransformer } from 'https://cdn.jsdelivr.net/npm/@component/library/dist/index.esm.js';
setTagTransformer((tag) => {
if (tag.startsWith('my-')) {
return tag.replace('my-', 'your-');
}
return tag;
});
</script>
<!-- Use the lazy loader as normal -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@component/library/dist/my-app.esm.js"></script>
<body>
<your-button></your-button>
<!-- ^^^ note the transformed tag name -->
</body>
Usage via a the dist-custom-elements output
import { setTagTransformer } from '@component/library';
import { defineCustomElement } from '@component/library/my-component.js';
// setup a tag transformer
setTagTransformer((tag) => {
if (tag.startsWith('my-')) {
return tag.replace('my-', 'your-');
}
return tag;
})
// define components as normal
defineCustomElement();
Usage via dist-hydrate-script output (on a Node.js server)
import { renderToString } from '@component/library/v1/hydrate/index.mjs';
import { setTagTransformer, renderToString as renderToStringV2 } from '@component/library/v2/hydrate/index.mjs';
// setup a tag transformer
setTagTransformer((tag) => {
if (tag.startsWith('my-')) {
return tag.replace('my-', 'your-');
}
return tag;
})
/**
* // handle incoming / outgoing html e.g.
* const incomingHtmlString = `
* <html>
* </body>
* <my-button></my-button> <!-- from v1.0.0 -->
* <your-button></your-button> <!-- from v2.0.0 -->
* </body>
* </html>
* `;
*/
return await renderToStringV2(
(await renderToString(incomingHtmlString)).html
).html;
Using tagTransform
If you make setTagTransformer available,
Stencil also exports tagTransform which you can use within your component code;
transforming static tag names via any assigned tag transformer function.
import { h, Component, Element, tagTransform } from '@stencil/core';
@Component({
tag: 'my-component',
shadow: true,
})
export class MyComponent {
@Element() host: HTMLElement;
connectedCallback() {
const ele = this.host.querySelector(tagTransform('my-other-element'));
const anotherEle = document.createElement(tagTransform('my-another-element'));
...
}
render() {
return (
<div>
<my-button>Click here</my-button>
{/* ^^ jsx tags are automatically transformed */}
</div>
);
}
}
Notes on CSS
Unless using extras.additionalTagTransformers,
you need to be thoughtful when writing CSS selectors within a component library that exposes setTagTransformer.
For example, if my-button is transformed to your-button this won't work:
:host {
display: block;
my-button {
color: red;
}
}
Instead, structure your components to use other selectors:
import { h, Component, Host } from '@stencil/core';
@Component({
tag: 'my-button',
shadow: true,
})
export class MyComponent {
@Element() host: HTMLElement;
render() {
return (
<Host class="my-button">...</Host>
);
}
}
:host {
display: block;
.my-button {
color: red;
}
}
extras.additionalTagTransformers
Setting the experimental extras.additionalTagTransformers configuration option to true (or prod to only apply to production builds)
will auto-wrap tagTransform(...) to most static tag names within your component library (including CSS selectors!).
Examples of auto-transformations include:
document.createElement('my-element');
// becomes
document.createElement(tagTransform('my-element'));
document.querySelectorAll('my-element');
// becomes
document.querySelectorAll(tagTransform('my-element'));
document.createElement('my-element');
// becomes
document.createElement(tagTransform('my-element'));
Incoming CSS like:
:host my-element {}
my-element::before {}
my-element.active:hover {}
Is transformed for use at runtime to:
`:host ${tagTransform('my-element')} {}
${tagTransform('my-element')}::before {}
${tagTransform('my-element')}.active:hover {}