Skip to main content
Version: v4.39

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.0 of @component/library which defines <my-button>
  • /admin loads version v2.0.0 of @component/library which 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:

  1. setTagTransformer allows application developers (your component library consumers) to assign a tag transformer function.
  2. tagTransform is mainly used within your component libraries - transforming any static string tag names using the tag transformer function assigned via setTagTransformer (Alternatively, you can auto apply tagTransform to all tag names via the extras.additionalTagTransformers config option.)

Using setTagTransformer

Stencil exports setTagTransformer which you can optionally make available to application developers:

  1. Make setTagTransformer available 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';
  1. 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 {}