-
-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for optional custom element registration to support Scoped CustomElementRegistry #705
Comments
I appreciate the intent, as this is a real problem with some application architectures. Earlier this year at Microsoft, I finished working on a FAST-based solution to this very same problem (the scoped registry polyfill wasn't an option for a number of reasons) so I'm well aware of what it entails. To be upfront, I won't introduce that kind of custom logic in Shoelace. One, because it relies heavily on key FAST features; two, because it's a runtime solution that involves more overhead than I'd like; and three, Shoelace aims to follow standards and best practices more closely, so the "correct" solution is to wait for scoped custom element registries to ship. ObservationsThat said, one problem I see with this proposal is that Shoelace isn't setup for dynamic tags names, which you've touched on above. There are a number of tags hard-coded in stylesheets, templates, and logic. For example, menus require menu items and tab groups require tabs/tab panels. (This is definitely not an exhaustive list.) Some of this can me mitigated through the clever use of Another concern is documentation. The docs are already plenty of work, and dynamic tag names add to that burden. However, see my comment below under "A Possible Alternative" for a potential win-win solution.
If I'm understanding correctly, you're suggesting that most tags could be named arbitrarily but those with canonical names should also, by recommendation, be registered with the correct name. If that's true, it will lead to a lot of confusion. Users want a cohesive system where they change a single prefix and use that prefix everywhere. They don't want to worry about individual tag names, so this feels very hacky to me. (Or maybe I'm simply misunderstanding you.) All that aside, I recall hearing a suggestion where the class exists in one file and the registration in another. This would simplify the build logic, possibly eliminating it altogether. The caveat is that, for CDN users, they'll be loading additional chunks of boilerplate. (That may not be a big problem, though.) While I appreciate the use case for multiple registrations with the same underlying components, I personally feel like it usually stems from poor architectural decisions. It would be better to hoist shared components up and resolve dependencies in such a way that all pieces of the app use the same version. This means you'd always get the correct styles and behaviors — never will the "same" component be disjointed aesthetically nor behaviorally. At the same time, I realize in an enterprise scale, this isn't always easy or possible. 😔 But if Shoelace is going to enable such a pattern, I'd prefer that it take advantage of whatever optimizations come from the official standard once it becomes available. A Possible AlternativeI haven't looked at the code in your branch (I'm about out of time for today), but from what you've described, it sounds like a lot of magic to get this working. That doesn't give me a good feeling. Have you considered a post-build tool that analyzes and replaces tag names? This could exist as a standalone tool — possibly an official one — that parses and updates tag names in templates, stylesheets, and code. This means we don't have a runtime solution (i.e. it won't work via CDN) but it keeps the stopgap solution outside of the core library, making it easier for me to support scoped custom element registry support once it lands. You would just need to build the library with the desired prefix. The same tool could be used to parse and update tags in the docs, custom event names (which use the Remember that the scoped custom element registry proposal hasn't been finalized nor accepted, requires an imperfect polyfill, and it could change drastically before browsers implement it. I'd feel much better about doing this with external tooling than trying to wedge it in the existing code base. |
By the way, I really appreciate the thoroughness of your proposal and your desire to not make sweeping changes unannounced. As a maintainer, thank you! |
Hey, thanks for the thorough response! Some thoughts.
Feels like the only way you can do this kind of thing initially, so not a problem!
This is essentially what the build step does, split the class from the registration, but at build time. For now the decision to do this at build time was to avoid changing every component file, but also my thought was for tools like VSCode extensions and the Custom Element Manifest generator that would (possibly) rely on the Just manually splitting the files would mean none of the build steps are required, but obviously testing that the rest of the system, extensions and tools continue to work.
I understand that the current code does not support dynamic tag names, and using the ScopedElementRegistry also will not support this. Manual registration with the Scoped registration will have to be done with the "correct" names for the system to make it work. I'm not in a position to solve that problem with this approach, and since it's not a regression I assume it wouldn't be a thing that would be implemented for this. I've done this for similar reasons with other WebComponents in the past, but the idea I talk about above is to expose the tag name as a static, allowing people to manually register with the correct tag name. import { SlButton, SlButtonTag } from '@shoelace-style/shoelace/dist/components/button/button;
@customElement('scoped-component')
export class ScopedRegComponent extends ScopedRegistryHost(LitElement) {
static elementDefinitions: ElementDefinitionsMap = {
SlButtonTag: SlButton
};
static override styles = styles;
public override render() {
return html`<sl-button>Im a button</sl-button>`;
}
} |
if it helps, I implemented an approach to allow for multiple versions of the same component to exist on the same page at the same time via a custom webpack loader that literally changed the tag name in the source. I ran up against the same problem @claviska described where parent components style children by their tag name. I ended up styling via One other solution might just be that shoelace not directly use the If some application has a need to have two of the same component from shoelace, with access to just the shoelace component class with no sideffects, the app could just have their own registration file and do something like:
it would require maintenance steps in the application to make sure that the new registered names didnt clash somehow, but that is easy enough to solve with namespace prefixes and such. The tagname references in shoelace's global style would still be a problem, but maybe theres some sort of helper that could be written there that could import the css with specified tag names replaced with a custom one? If the new component class is being registered, i think technically there would be an opportunity to add some stylesheet to the existing static styles list?
|
+1 for being able to import the class independently from the customElements.define File structure could be Where alert.class.ts is equivalent to today's alert.ts minus the And new alert.ts looks something like
or if the decorator isn't necessary for VSCode extensions
At first glance it looks like this approach wouldn't break anything, since |
@austinhallock I like that approach. There's still a need to work around dynamic tag names, though. One other concern is how tooling will handle dynamic tag names since they'll be interpolated. I do this at work since it's a requirement (although it's with FAST Element) and it absolutely wrecks syntax highlighting in templates. |
my two cents: imo there are two issues here. One is the need for having an importable, side-effect-free class file as an option from shoelace. The other is how to deal with dynamic tag names internally to shoelace. Personally, I see them as separate features, because if shoelace only provides the class export as an option, nothing has to change about shoelace itself to just provide that one new export. All the existing styles that come with shoelace would still work. If a developer wants to take a shoelace class, import it, extend it, re-register it with a different name, then the technical details of how to handle that name change would be up to that developer, per se. One caveat would be that how separate these two features are depends on how often shoelace components refer to other shoelace components. Imo, if most of the shoelace components are largely standalone and dont refer to others, there's a lot of benefit in merely providing side-effect-free class file exports in the bundle for whatever a dev wants/needs to do with them. On the other hand, if most of the shoelace components are interconnected and depend on other components, then these features become more connected and might truly need to be solved for together. @claviska knows better than I do, and I'm assuming that there's a fair bit of interconnectedness that prompted him to bring it up in the first place :) I see a few possible solutions for custom tag names, each with pros and cons. source code approaches Loaders for bundlers Static generator tool |
@michaelwarren1106 The "static generator" approach sounds pretty good to me. If someone's needs are so specific they need custom tag names, they're long past the "just host it on a CDN" stage. Outside looking in, I feel like Shoelace should generally remain as-is as much as possible, and support for custom names would mostly require an extra layer of tooling to facilitate. |
Got it. Digging through the rest of this, it sounds like you don't like What would you say are some of the most complex instances of another component being referenced from a parent component? It looks like a lot of the CSS is styling slot elements if they're of a certain type of element. What are your thoughts on using the scoped nature of CSS variables instead? Eg. looking at sl-input, it has
Could that instead be
And have the sl-icon component use Disclaimer: I'm still very new to web components :) |
I think if done right, it doesn't need to require an extra layer of tooling. I do think they're two separate (but related) issues like @michaelwarren1106 mentioned:
Our use-case might be specific, but I imagine there are other instances where developers will want to extend off of an existing Shoelace component to add/change some functionality, without needing to fork the entire repo |
Right, that seems like an easy win. That way if I need to subclass But regarding the hardcoded tag names, I'd hate to see the component source files become much more complicated or hard to read. I don't think tag name references in parts, etc. can be wholly avoided either. Seems like it should be a reasonably straightforward proposition though simply to parse compiled output templates/CSS to replace |
I think I didn't communicate this well enough when I opened the ticket. I 100% agree that these feel like mutually exclusive features, with my intention of the change was to support the Scoped registry. The dynamic tag issue feels like a much larger thing. |
I don't consider them separate at all. The primary use case for optional registration is to change the prefix/tag names. If optional registration is officially supported, users will expect it to work no matter what they name each element. When it doesn't, that's a poor experience. And in many cases, it won't be immediately obvious that things are broken. Such an experience cheapens users' perception of the library. How many folks will file a bug about this? I suspect it will be a lot and "that's by design" isn't an answer they're going to want to hear. Not considering dynamic tag names as a prerequisite for optional registration is a recipe for failure on both sides.
Me too. I enjoy working on this library because it's fun for me. Mucking up the source to support a feature that [unless the tag name problem is sorted] will only serve to allow users to break things isn't a win for anyone.
A tag name is static. It can't change. A data attribute, a role, a class, etc. are all subject to DOM manipulation. For performance reasons, these would be set on init rather than on each render. If the attribute gets removed or changed at some point, the component(s) that reference them will break in unexpected ways. We can argue that users shouldn't change said attributes, but they will either accidentally (e.g. binding a class/attribute that overrides what the component sets) or through some third-party lib and I'll have to answer the support calls. Post-build and Premium FeaturesI still like the idea of a post-build option. If you're customizing Shoelace at that level, you're probably picking various components for use in a design system or a similar library. It's essentially a white-label option, so an additional step to make it your own seems like a fair tradeoff, IMO. I'd also like to float the idea of making custom prefix/tag names a premium option, as many (most?) "white-labelers" will be using this feature to bring Shoelace into their org's design system. This might seem like a big assumption, but I haven't seen any other use case for needing to change tag names. If you're using Shoelace that way, I think it's more than fair to ask for a reasonable license fee since even one component can save your company a significant amount of money in design/engineering expenses. I appreciate that this might be disappointing to some folks, but I'd like to spend more time on Shoelace and, in order to do that, I need to be able to justify it as more than just a free side project. I have a handful of amazing sponsors, but it's not nearly enough to drop my other obligations. A premium option like this could solve that. By the way, this month (July) marks the two year anniversary of Shoelace 2.0's release! |
Had not considered this, totally fair assessment. |
Not me. This is your baby, make your money, dude. The post-build or white-label option is something that you and I have talked about of this thread a little bit, like a generator that generates your own custom version of shoelace specifically for design systems and component libraries that want to use shoelace as a start, but need their own tag names and are willing and able to support/bug fix on their own after the fact. I am onboard for that approach for sure. I picture a one-time generation thing (as in you generate a point in time version of shoelace and dont get updates when shoelace updates) and after that generation is done, the consumer has their very own customized version of shoelace that they now own/maintain. I think sticking to one-time-generation is way simpler and easier, but if a consumer wants to get an updated version of shoelace, then they would need to regenerate and manually integrate whatever source code changes they've done on their side. On the shoelace side, this could be made a little easier by offering a generator tool that can generate both the whole lib and also a single component at a time. |
If the generator approach happens, I want to emphasize that users should be able to upgrade if they don't want to be responsible for bug fixes and new features. So you'd have the option to keep things pure and regenerate easily using the latest version, or you can generate once and "fork" components to make them your own. |
Just had an idea... If the white-labeling option is approached as a paid option, and you setup like subscriptions and accounts for consumers, then that would enable you to generate each special version of shoelace for every account according to their settings whenever shoelace changed and still be able to host those custom versions on a CDN if you wanted to. If there was a subscription plan/membership thing, then i can imagine that each subscriber gets their own custom CDN url for shoelace that would serve up their specifically generated version in case they wanted to use shoelace from CDN. something like Maybe the combination of both CDN hosted custom shoelace versions AND the shoelace generator would be a good offering to justify the subscription cost? |
for sure! But I think that is purely a function of what folks do after they get their hands on the source code. And i'm not sure we're ever going to be able to determine that? If shoelace generates a custom source, and a consumer copies that source into a new github repo then goes to town on customizations, they've effectively forked. If the only changes they make are in completely separate files/directories that just import/extend default shoelace assets, then it might be possible for them to seamlessly upgrade, but i'm not sure how shoelace would know what kind of situation folks have? |
Great ideas! We're getting a bit sidetracked from the original issue, though, so I've opened up a discussion to continue the talk about premium features. |
Why don't you temporarily replace the decorators with the following snippet:
This would solve the issue without any side effects (that I am aware of) and could be a quick solution before applying more complex solutions. We're currently looking at using this library, but this issue is kind of a deal-breaker |
What are you trying to accomplish with this? Are you looking to use the Scoped Custom Element Registry polyfill? |
We're trying to avoid having 2 or more components trying to register any <sl-*> component, resulting in an error and an empty web page |
Thanks for clarifying. Are you not concerned that the first loaded module will win, regardless of which version it is? For example, if Module A loads Shoelace 2.0.0-beta.48 and Module B loads Shoelace 2.0.0-beta.70, how can you be sure components that are designed to work together will in fact work together? There are also breaking changes in various beta versions, meaning the APIs can be different. Something as simple as a button variant can break, as the <sl-button type="primary">Button</sl-button> <!-- only works in beta.62 and below -->
<sl-button variant="primary">Button</sl-button> <!-- only works in beta.63 and above --> If you don't have control over which versions are being imported, you will run into issues like this. I would suggest that, even in a microfrontend architecture,component dependencies should be resolved and loaded higher up in the stack to avoid problems like this. The suggested conditional suppresses the error, but none of the problems that will occur as a result of re-registration will be solved. It's a foot gun. And if it's not a foot gun for you due to constraints not mentioned herein, it will be for other users. If you really want to do this, you can overload |
Thanks for your explanation. I did not consider that. |
The vision behind the Scoped Custom Element Registry does indeed aim to solve this problem. My personal opinion of front-end architectures aside, this is a real world problem and something I have to face myself at MS. The solution we came up with at work was a mechanism for allowing custom prefixes, so you can register different versions of the same components under different tag names. This eliminates most of the problems except for major versions with breaking changes, which would require a new tag name to prevent collisions. But that solution isn't as elegant as I'd like and it makes using the library a bit harder. I consider this a gap in the platform and the cleanest way to mitigate it in 2022 is to move component registration up in the stack. I realize this defeats the purpose of the micro front-end paradigm, but I'll leave my opinions on inefficiencies and disparate aesthetics/behaviors out of this thread. 😅 FWIW the concept of a generator that can produce unique Shoelace components, e.g. It's not a proper solution, but it would work reasonably well until the platform catches up. |
@MauriceAcerta ive been where you are I think :) Previously I was working on a design system that was being used in a large micro-front-end architecture application, where a single page was constructed from various MFEs written by completely different teams that werent at all aware of the activities of other teams, much less their code bases. As such, each MFE could be using a different version of the same component on the same page at the same time as @claviska described. In order to make this work at all, there are two solutions. IMO you can either A. have each MFE application maintain a list of re-registrations so that OR B. If your MFEs are applications with a bundler like webpack/rollup, write a common loader plugin designed to literally string replace all references to all shoelace component tag names to some different tag name of your choosing during the bundle step where you have access to the application source code. What we ended up doing is going with the loader approach, because that enables teams to use the regular tag names in their MFE apps (ie, devs would always write The re-register solution requires that shoelace never references a component by its tag name and uses some other method. The loader solution requires access to shoelace source code AND app source code so you can do the string replace (and also makes builds take a smidge longer). So, basically as @claviska says, if you're just trying to prevent errors upon re-registration, then you risk the first registration being the one that wins. If you really want to make each MFE's version of shoelace truly separate, then they must get registered as truly different component tag names which right now today will only work with the loader approach, since thats the only approach that can account for the fact that shoelace today refers to other shoelace components by tag name. The loader approach is afaik the only way that you'd be able to change ALL instances of a shoelace component tag name to some other tag name. EDIT: @claviska thought of a third option which is a pre-generation option where you take shoelace's source code and string replace it ahead of time instead of via a loader which would do it at build time |
Thanks a ton for the extensive reply @michaelwarren1106! Would you mind elaborating on how you can enable string replacement of tags in the source code of Shoelace during build time? I'm not familiar enough with Webpack/rollup to see how that's done right now |
Webpack has some docs on how to write a loader: https://webpack.js.org/contribute/writing-a-loader/ Rollup has guides about how to write a plugin: https://rollupjs.org/guide/en/#plugins-overview After checking out those docs, its just a matter of writing your plugin however you see fit according to your needs |
I more specifically mean how to actually implement that. |
you dont dig into the node_modules and replace it there, the loaders/plugins get run at build time and basically pass you the source code of files that are being run through the build and you can change stuff about them during the build process via your loader... |
@michaelwarren1106 Do you have any basic sample of how you have achieved this multiple versioning of webcomponents using loader ? |
unfortunately no, that was all internal proprietary code, sorry :( |
Oh wow, just stumbled on this thread. I went with |
@MauriceAcerta This has been fixed in #1450 Closed with #1450 https://shoelace.style/getting-started/installation/#avoiding-auto-registering-imports Note: Custom tagnames are not fully supported yet. |
What issue are you having?
My usage of Shoelace for a pseudo-microfrontend has finally hit the inevitable issue with WebComponents where two seperate bundles of JS are trying to register the Shoelace components they are using, and the global
CustomElementRegistry
is obviously failing.The most prominent solution to this issue appears to be using Scoped CustomElementRegistries, which relies on elements not auto registering with
customElements.define()
, and instead exposing just the raw classes to be manually registered later. As with all Lit based WebComponent libraries, this registration is handled in Shoelace by the@customElement
decorator which automatically executes when the JS is loaded. There is currently no way to disable this in Lit.Describe the solution you'd like
TL;DR I spent the day making a fork where I looked at a build time solution to offering all Shoelace components as both their raw classes and their current automatically registering forms. It's very early, and is for sure not production quality at the minute, but it does appear to be working on my Lit based test app via the
@lit-labs/scoped-registry-mixin
https://github.com/xenobytezero/shoelace/tree/optional-auto-registration
Intentions
Approach
My plan was to have a build time step that will offer each component as both the current auto registering form, and as a raw class allowing it to be manually registered with Scoped Element Registries.
This takes the form of two steps
1/ Generate "auto registration" shims for each of the components, similar in a way to how the React shims are generated
This is handled in the
scripts/make-reg.js
Node script which is called from the build script, and creates a<tag-name>.auto.js
shim in thedest/components
folder for each component. This shim imports the raw class, as well as the Lit@customElement
decorator, then registers the element. The shim looks like this (for example for<sl-button>
)All of the information needed for this is pulled from the
custom-element.json
metadata in the same way as the current React shims are created.2/ Strip out the usages of the
@customElement
decorator from the components at build time.This is done with a small ESBuild plugin which looks specifically for the component files and manually strips the decorator out before passing the source to ESbuild for standard building.
Issues
@customElement()
decorator) be output as<tag-name>.class.js
, and the generated shims to be output as<tag-name>.js
as the current files are. This means that there would be zero code changes required for current users of the library to continue to use the auto registering versions of the components, and developers that want to consume the raw classes would instead doUnfortunately, I couldn't get ESBuild to output a different filename than what it was originally, at least not for only a select set of files (only the ones in
src/components
). This could be solved by renaming the source files to<tag-name>.class.ts
, but I didnt want to go about making sweeping changes this early on.Elements that rely on specific tag names could fail when registered with a different name
None of this works with the "import everything" bundle.
No docs or tests
I understand that an issue is required first before creating a PR, so this serves that purpose. If there is interest I can do the official thing. This would also be my first proper PR, so apologies if I'm messing up the process.
Thoughts? Feelings? Opinions?
The text was updated successfully, but these errors were encountered: