stencil icon indicating copy to clipboard operation
stencil copied to clipboard

feat: transformTagName at runtime

Open Sanderand opened this issue 3 years ago • 10 comments

Prerequisites

Describe the Feature Request

Stencil provides the transformTagName extra which can be enabled in the stencil.config.ts. This allows us to register our web components with "transformed" tag names.

Example:

import { defineCustomElements } from '@stencil/core';

defineCustomElements(window, {
  transformTagName: (tagName) => `custom-${tagName}`,
});

// we can now use <custom-wb-button> (the original component was <wb-button>)

Many of our components render other custom elements internally.

Example:

render() {
  return (
    <Host>
      <slot />
      <wb-icon />
    </Host>
  );
}

When applying a transformTagName the above render function will now generate broken/useless html. It will render a <wb-icon> component, which does not exit. There only exists a <custom-wb-icon> component.

To fix this, we need the ability to access the passed in transformTagName function in our components during runtime.

Example:

render() {
  const WbIcon = transformTagName('wb-icon');

  return (
    <Host>
      <slot />
      <WbIcon />
    </Host>
  );
}

Describe the Use Case

Our DS is used in a very complex microfrontend environment. Feature teams need to be as independent as possible. They want to decide which version of the DS they use for the micro FE they provide. Because of the complexity it is not feasible to dictate one fixed version of the DS for all micro FEs.

To address this complexity, products should be able to apply a transformTagName to register their own instances of the DS Web Components, which won't conflict with any other version that might be running at the same time.

Describe Preferred Solution

import { transformTagName } from '@stencil/core';

@Component({ /* ... */ }) 
export class MyComponent {
  render() {
    const WbIcon = transformTagName('wb-icon');
  
    return (
      <Host>
        <slot />
        <WbIcon /> // renders <custom-wb-icon> or whatever tagName transform is applied
      </Host>
    );
  }
}

Describe Alternatives

We are currently using this as a workaround. It works, but it's really not ideal:

/**
 * transforms a tagName during runtime according to the Stencil transformTagName extra. required for multi version support
 * @param tagToBeTransformed i.e. 'wb-stepper'
 * @param knownUntransformedTag  i.e 'wb-step'
 * @param knownUntransformedTagElementReference
 * @returns i.e. 'PREFIX-wb7-stepper-SUFFIX'
 */
export const transformTagName = (tagToBeTransformed: string, knownUntransformedTag: string, knownUntransformedTagElementReference: HTMLElement): string => {
  const actualCurrentTag = knownUntransformedTagElementReference.tagName.toLowerCase(); // 'PREFIX-wb7-step-SUFFIX'
  const knownTagWithoutDesignSystemPrefix = knownUntransformedTag.split('wb-')[1]; // 'step'
  const targetTagWithoutDesignSystemPrefix = tagToBeTransformed.split('wb-')[1]; // 'stepper'
  const [prefix, suffix] = actualCurrentTag.split(knownTagWithoutDesignSystemPrefix); // ['PREFIX-wb7', '-SUFFIX']
  return prefix + targetTagWithoutDesignSystemPrefix + suffix; // 'PREFIX-wb7-stepper-SUFFIX'
};

We then call this function from our components. Example:

import { transformTagName } from '../helper';

@Component({
  tag: 'wb-button'
}) 
export class MyComponent {
  @Element()
  public el: HTMLElement;
  
  private WbIcon: string;
  
  connectedCallback() {
    this.WbIcon = transformTagName('wb-icon', 'wb-button', this.el);
  }
  
  render() {
    return (
      <Host>
        <slot />
        <WbIcon /> // renders <custom-wb-icon> or whatever tagName transform is applied
      </Host>
    );
  }
}

Related Code

No response

Additional Information

While our workaround works for the many of the tagName transforms that we consider relevant, it's very verbose, feels hacky and is quite error prone. A typo in the function call will cause the whole thing to break. There's no way to enforce type safety. Also each component needs to use the @Element decorator which further bloats up the codebase.

Sanderand avatar Mar 08 '22 13:03 Sanderand

Hi, I do not know if this suits your requirements, but Gil Fink has written on how to use dynamic Stencil component (tag) names. This technique enables 'on the fly' component loading at runtime. From the post:

render() {
  const Tag = this.isInline ? 'span' : 'div';
  return (
    <Tag>
      <slot />
    </Tag>
  );
}

stevexm avatar Mar 09 '22 00:03 stevexm

Hey Steve. It's good to see your reply here, hopefully giving this issue some additional traction.

The technique you're mentioning here is pretty much what we do with out custom transformTagName function. That function returns a string that we then assign a variable and render as a JSXElement.

We now want to get rid of our error-prone and non-ideal custom transformTagName function and ideally get it replaced with the function that we pass to Stencil when calling defineCustomElements. That way we can always be use that we apply the exact same tagName transformation that we've used to register our CustomElements in the first place.

Sanderand avatar Mar 09 '22 06:03 Sanderand

Would something like this help?

oscargm avatar Jan 09 '23 11:01 oscargm

I'm updating our library, KoliBri, to the latest Stencil version while ensuring we continue to support micro-frontends that use different versions of our library.

Previously, we achieved this by passing a transformTagName function as an option to defineCustomElements. For the frameworks adapters we made slight adjustments to support the "tag name transformer" as well.

However, I discovered that the React Adapter now builds on the dist-custom-elements output. This means it registers the custom elements individually (instead of using defineCustomElements) and does not support a tag name transformer.

Given this change, I'm wondering what the best approach would be to enable micro-frontends to work with the latest Stencil version, and where I could contribute.

From the discussions, I see two possible approaches:

  • Supporting transformTagName (this issue)
  • Supporting Scoped Elements (with a polyfill), which would render transformTagName unnecessary (https://github.com/stenciljs/core/issues/2957)

Are my observations correct so far, and do you already have a preference for one of these approaches?

sdvg avatar Mar 19 '25 07:03 sdvg

Hey all (not @sdvg < this doesn't help your request unfortunately) - I recently had to tackle this myself - here's a demo of my full solution, hopefully it's helpful :)

https://github.com/johnjenkins/stencil-prefix-demo/

A mix of centrally registering a namespace, then using that register to augment Stencil's rendering h() function means all internal JSX is handled / transformed during render.

johnjenkins avatar Mar 19 '25 15:03 johnjenkins

@sdvg thanks for the feedback. Your observation seem to be correct. The transformTagName method has been poorly implemented/tested in Stencil so it is not surprising that it broke with the update of the React Output Target. As you already know, this is something we would love to get implemented/fixed and we don't have to wait for v5 for this to happen. I would suggest to comment in #2957 on how this feature could be implemented from the user POV, maybe by providing an example about your use case. If you are interested to support the project developing this, please join us on Discord and let's coordinate this effort.

christian-bromann avatar Mar 19 '25 17:03 christian-bromann

@christian-bromann I have worked on something similar on our project, creating a custom AST transformer that is just added at the end of the customBeforeTransformers, traversing the abstract syntax tree and replacing all tagname with a customSuffix function. Effectively this makes it possible to not need worrying about changing anything in our source code at all, except for the const componentNameCss variable, which I then have written a custom rollup plugin to patch (but not tested thoroughly yet)

So no need to wrap tagnames in a transformTagname function.

I have only looked at this for the dist-custom-elements output target, but could be that some of my work could be useful here?

At the moment I moved the work from my fork of stencil to run as a custom output target just in our project.

The main use case is consuming angular components in a microfrontend environment, which is a PIA with web components because of the global scope. My initial tests for the custom output target seems to work really well.

My idea was to have a function that is just appended to every tagname, resulting in this:

    components.forEach(tagName => {
        switch (tagName) {
            case "my-component":
                if (!customElements.get(tagName) {
                    customElements.define(tagName, MyComponent$1);

being transformed by AST to this (actual output from the transformer):

    components.forEach(tagName => {
        switch (tagName) {
            case "my-component":
                if (!customElements.get(tagName + getCustomSuffix())) {
                    customElements.define(tagName + getCustomSuffix(), MyComponent$1);

same with query selectors:

        this.el.querySelector(`my-component${getCustomSuffix()}`);
        this.el.querySelector("#my-component");
        this.el.querySelector(".my-component");
        this.el.querySelector(`parent > my-component${getCustomSuffix()} + my-component${getCustomSuffix()}`);
        this.el.querySelector(`previous + my-component${getCustomSuffix()}`);
        this.el.querySelector(`my-component${getCustomSuffix()}[attribute="value"]`);
        this.el.querySelector(`my-component${getCustomSuffix()}:pseudo-class`);
        this.el.querySelector(`stn-checkbox${getCustomSuffix()}`);
        this.el.querySelector(`stn-button${getCustomSuffix()}`);

and the returned jsx

 return (h("stn-button" + getCustomSuffix(), { key: "3db7e9a0b09b9a298e47211007819383ef683749" }, "Click me!"), h("stn-checkbox" + getCustomSuffix(), { key: "085ed1f662211ea5ec5f377309cfeb26a8cafcd0" }, "Check me!"))));

also for the css-transformation (not sure if this will actually work, but will test next week:

const myComponentCss = `stn-button${getCustomSuffix()}{background-color:#007bff}stn-checkbox${getCustomSuffix()}{border:1px solid #ccc}component{padding:10px}#component{display:block}.component{color:#333}`;

It would be much better for us to have this as a part of @stencil/core than just an internal custom output target

If this seems interesting and of use, I'd be happy to discuss it more.

Cliffback avatar Apr 30 '25 13:04 Cliffback

This looks like an interesting approach, I wonder what others think about it. @sdvg @johnjenkins @Sanderand @stevexm et al

christian-bromann avatar Apr 30 '25 18:04 christian-bromann

Looks good - similar to everything I’m doing here https://github.com/stenciljs/core/pull/6211

^ that’s very close to complete, the only issue I’m having is the SSR / hydrate output getting muddled. I just haven’t had the time to fix it :/

johnjenkins avatar Apr 30 '25 19:04 johnjenkins

Looks good - similar to everything I’m doing here #6211

Nice! Started working on this a while ago, so didn't realize that there were anyone else working on it, but that is really good to hear!

My approach was to make this an output-target / separated transformer, only touching the dist-custom-elements, so a more wholistic approach for every output is really great!

How do you set the tagName in consuming projects with your approach?

Also, do you have any thoughts on patching internal "h" references in the return function, querySelectors, or even the CSS? In our case, it would be preferrable to not need to wrap all internal tagname references in functions, as it would be quite a rewrite for us, so I solved it with transformers.

Would this be something you think could fit into your solution, or perhaps better off as a separate feature PR (or custom output target) since it revolves around patching the outputted files after transpilation?

Since your solution seem to mostly touch the define functions, am I correct? Just trying to figure out the overlap between what I've done, and if it is worth continuing with some of my stuff?

Cliffback avatar May 05 '25 06:05 Cliffback

Hi guys, I just want to add to the discussion that even if the extra tagNameTransform is not documented in the website, it's implemented in the defineCustomElements

import { defineCustomElements } from 'some-button/loader';

defineCustomElements(window, { 
  transformTagName: (tagName: string) => `my-${tagName}` 
} as never);

In runtime I have:

<some-button> <!-- not working anymore -->
<my-some-button> <!-- working as expected-->

Because the type is not well exposed, I'm casting to never to don't have typescript complaining. It works like a charm in runtime, however I'm not sure if I should use after this discussions around the topic and also because Stencil docs don't have this extra documented (it will be deprecated?)

My use case matches with this guy here: https://dev.to/sanderand/running-multiple-versions-of-a-stencil-design-system-without-conflicts-2f46 Micro Front-end applications using multiple versions of the same component.

alvarocjunq avatar Jun 19 '25 13:06 alvarocjunq

Thanks for sharing this. I wonder in what scenarios this works, e.g. you are using the loader capability but some folks have different ways to inject Stencil in their applications. It will require some more investigations AND testing to better understand this feature. Note: I am saying this because I even don't know all the features Stencil has.

I think if we could add some e2e tests with WDIO for this we can ensure this won't be accidentally removed. I will go ahead and merge your docs update as I think it is valuable information to share. Thanks for bringing this up.

@sdvg is this something that would work for Kolibri?

christian-bromann avatar Jun 19 '25 16:06 christian-bromann

The tagNameTransform don't work for us, as we use the dist-custom-elements output target, and don't use the loader, however we are actually now using our own custom output target that runs the AST transformer to modify all tagnames, and it seems to work quite well. The tagname is validated runtime in the consuming projects.

In our case this is for use in angular microfrontends.

I can push up a repo with the output target

Cliffback avatar Jun 19 '25 20:06 Cliffback

@sdvg is this something that would work for Kolibri?

@christian-bromann Thanks for the ping! Unfortunately we have the same problem as @Cliffback here: We use the adapters (mostly React) which internally use dist-custom-elements, not the loader.

The AST transformer is a workaround I will discuss with my team as well. I think for most projects this approach might be too complex, though.

sdvg avatar Jun 20 '25 08:06 sdvg

@sdvg and @christian-bromann I just pushed up our solution for dist-customelements` here: Cliffback/stencil-custom-suffix-output-target

I will add documentation to the README.md later on how we use it, but to summarize this patches all tagname definitions, all references to tagnames inside the render function, all query selectors and also the const containing the css with a custom tagname retrieved from a custom-suffix.json file inside the dist folder.

I have written tests to make sure it does what it's supposed to, but also serves as good reference for what it does, you can take a look here: custom-suffix-output-target.data.ts

I have a script to update the custom-suffix.json postinstall in the consuming project that I'll upload later.

In addition, we also patch our angular wrapper, I'll outline how we do this as well.

This can be installed in a Stencil project and be used as a custom-output-target that transforms the file after stencil compilation.

If this is something that is interesting to include in Stencil, we could move it over as a PR, but else I could publish it as a npm package so that others could could get some use out of it.


The use case for this is a a huge angular microfrontend project where we have had loads of trouble because of the issue with versioning web components in the same browser / registry. It has taken quite some time to get this to work properly, so really hope this is useful for more people!

Also, I'd be very happy to take feedback, contributions or suggestion or whatever (or move it to @stencil/core)

Cliffback avatar Jun 25 '25 07:06 Cliffback

Thanks @Cliffback for putting this out. It would be amazing if we could drive this into Stencil core in a way that is outlined by @johnjenkins in #3138 (#ref). What do you think?

christian-bromann avatar Jun 30 '25 23:06 christian-bromann

@christian-bromann Sorry for the late reply, in the middle of summer vacation here, but absolutely, that would be very nice, and would be up for helping with that. We would need to discuss the points where the solutions differ though, as with the query selectors and in my case patching the css as well, if this is viable to bring into Stencil.

Cliffback avatar Jul 08 '25 18:07 Cliffback

@Cliffback enjoy your summer vacation and ping us on Discord once you back 😉

christian-bromann avatar Jul 08 '25 18:07 christian-bromann

Hi guys, just a heads up in the issue I'm really interesting in the work that @Cliffback did, I'm kindly waiting for some documentation to understand better what you did on your side =)

Or do the Stencil team have any news about this topic?

alvarocjunq avatar Aug 20 '25 14:08 alvarocjunq

Hi guys, just a heads up in the issue I'm really interesting in the work that @Cliffback did, I'm kindly waiting for some documentation to understand better what you did on your side =)

Or do the Stencil team have any news about this topic?

Have been on vacation so haven’t had the time, but I’ll update the documentation in my repo so that it is easier to understand how it works.

Cliffback avatar Aug 20 '25 15:08 Cliffback

@Cliffback enjoy your summer vacation and ping us on Discord once you back 😉

Thank you! I’m back now, so I’ll ping you there tomorrow :)

Cliffback avatar Aug 20 '25 15:08 Cliffback

Hi guys, just a heads up in the issue I'm really interesting in the work that @Cliffback did, I'm kindly waiting for some documentation to understand better what you did on your side =)

Or do the Stencil team have any news about this topic?

Have updated the documentation now, as well as published it to npm so that it is easy to install. Will try to improve both documentation and add a few of the helper scripts in the coming days. Feel free to ask questions or come with suggestions over in my repo, so that we don't "spam" this issue"

https://www.npmjs.com/package/stencil-custom-suffix-output-target

Cliffback avatar Aug 21 '25 13:08 Cliffback