Skip to content
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

Improve style sheet adoption ergonomics. #166

Merged
merged 1 commit into from
Oct 18, 2024
Merged

Conversation

theengineear
Copy link
Collaborator

@theengineear theengineear commented Feb 2, 2024

One common pattern for element authors (now that import attributes enable folks to import css files directly) is to adopt imported style sheets into a shadow root at custom element initialization time.

This adds one static getters — styles. The goal is to make shadow root initialization as declarative as possible. Note that we still expose createRenderRoot so we don’t get in the way if folks need to do something more advanced (it’s also still the only way to forgo the creation of a shadow root at all).

Closes #52.

x-element.js Outdated
* stylesheet file via import attributes. This has no effect if you are using
* the “host” as your render root (versus attaching a shadow root).
*/
static get styleSheets() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the crux of the change. We can fuss over the name here. I personally like cssStyleSheets, styleSheets, or styles as the options. But, I’m willing to be convinced otherwise 😸

Copy link
Collaborator

@klebba klebba Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import MyStylesAreTheBest from './path-to.css' with { type: 'css' };
import YourStyles from 'https://your.style/sheet.css' with { type: 'css' };

static get styles() {
  
  const rules = `:host { display: block }`;
  const inlineStyles = new CSSStyleSheet();
  inlineStyles.replaceSync(rules);

  return [
    MyStylesAreTheBest,
    YourStyles,
    inlineStyles,
  ];
}


class TestElement2 extends XElement {
static get styleSheets() {
// TODO: Replace with direct import of css file when better-supported in
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’ll have to use your imagination a bit until import attributes are better-supported. This is the pattern we’d be using though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need to use our imagination :^)

We might be able to update these to import the CSS in directly. Assuming all our testing clients are using Chrome, that should probably work!.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… except for ESLint doesn’t dig this yet. So close!

@theengineear
Copy link
Collaborator Author

@klebba — This is more-or-less what I figured we’d do here. It’s pretty straightforward, just a way to declare things in isolation as an ergonomic benefit.

x-element.js Outdated
}

/**
* Declare an array of CSSSTyleSheet objects to adopt on the shadow root.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo STyle

x-element.js Outdated
@@ -68,7 +87,9 @@ export default class XElement extends HTMLElement {
* E.g., setup focus delegation or return host instead of host.shadowRoot.
*/
static createRenderRoot(host) {
return host.attachShadow({ mode: 'open' });
const shadowRoot = host.attachShadow(this.shadowRootInit);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend against the shadowRootInit concept — developers should simply override createRenderRoot to customize the render root init

x-element.js Outdated
@@ -68,7 +87,9 @@ export default class XElement extends HTMLElement {
* E.g., setup focus delegation or return host instead of host.shadowRoot.
*/
static createRenderRoot(host) {
return host.attachShadow({ mode: 'open' });
const shadowRoot = host.attachShadow(this.shadowRootInit);
shadowRoot.adoptedStyleSheets = this.styleSheets;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend moving this out of createRenderRoot and instead moving it to the constructor — it would throw an error if there is no shadowRoot on the host

@theengineear theengineear force-pushed the style-sheet-ergonomics branch 3 times, most recently from fc224a2 to 784cd5f Compare October 8, 2024 19:55
} else {
throw new Error('Unexpected "styles" declared without a shadow root.');
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of a lot of lines dedicated to error-handling… but so it goes 🤷


### Fixed

- The `map` function now works with properties / attributes bound across template contexts (#179).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just adding this in the [Unreleased] section — forgot to request this earlier.

One common pattern for element authors (now that import attributes
enable folks to import css files directly) is to adopt imported style
sheets into a shadow root at custom element initialization time.

This adds one static getter — `styles`. It’s still possible to do
something more custom in `createRenderRoot`, but this adds a simple,
declarative interface for the task of adding styles to a shadow root.

Closes #52.
el.remove();
});

it('should only get styles _once_ per constructor', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importantly, this function is called once since it’s a static member, we follow our typical pattern — assume it can be set up once during static analysis and re-used for all instances.

@@ -21,6 +22,15 @@ export default class XElement extends HTMLElement {
return XElement.defaultTemplateEngine;
}

/**
* Declare an array of CSSStyleSheet objects to adopt on the shadow root.
* Note that a CSSStyleSheet object is the type returned when importing a
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: an example syntax might be more useful here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll add some additional commentary into the changes to use JSDoc 👌

@@ -534,7 +544,18 @@ export default class XElement extends HTMLElement {
const computeMap = new Map();
const observeMap = new Map();
const defaultMap = new Map();
const { propertyMap } = XElement.#constructors.get(host.constructor);
const { styles, propertyMap } = XElement.#constructors.get(host.constructor);
if (styles.length > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if (styles.length)

@klebba
Copy link
Collaborator

klebba commented Oct 18, 2024

Looks awesome, nice work!

@theengineear theengineear merged commit 827031d into main Oct 18, 2024
1 check passed
@theengineear theengineear deleted the style-sheet-ergonomics branch October 18, 2024 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Leverage the pattern of "Constructable Stylesheet Objects" for shared CSS
2 participants