-
Notifications
You must be signed in to change notification settings - Fork 319
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
Adds static get styles()
#401
Conversation
This should be implemented to return an array of styles defined using the `css` tag function. These styles will be applied with `adoptedStyleSheets` where available and shimmed when not. WIP: needs tests.
Questions: why is it an array? and not just return css`
h2 { color: orange; }
`; using only a css tagged template literal you can do more advanced stuff like duplicate css. e.g. // somewhere
const headerColor = css`h2 { color: orange; }`;
// myBase.js
static get styles() {
return css`
${headerColor}
.footer { border-top: 1px solid green; }
`;
}
// myEl.js
static get styles() {
return css`
${super.styles}
${headerColor}
.some { color: yellow; }
`;
} Output of myEl css h2 { color: orange; }
.footer { border-top: 1px solid green; }
h2 { color: orange; }
.some { color: yellow; } Output of myEl css WITH dedupe h2 { color: orange; }
.footer { border-top: 1px solid green; }
.some { color: yellow; } I am telling this as we have been using a BaseClass that supports style deduping for a while now and this is a really powerful feature and it makes the life of the css parser/shadycss just simpler by not needing to process as much css. In this simple example it's not much but really if you have huge css blocks then deduping css improves performance a lot. So I am really interested what the reasoning for using an array is. PS: lit-css already supports this see https://github.com/lit-styles/lit-styles/blob/master/packages/lit-css/test/lit-css.spec.js#L29 |
I'm happy to see that you folks take a step forward to support this. Would be nice if you take lit-css as an inspiration for your implementation. I personally always thought this should be part of the LitElement core. I do understand that tagged templates are not designed for such a use case or at least this use case with deduping is uncommon, but so far there were some users happy with it, for example, @web-padawan who prototypes new vaadin components. |
Also some ideas behind such a design I presented in this talk a few weeks ago https://youtu.be/wNKyEtLRTe4?t=2373 (will start at the most interesting part of the talk). Between the lines it explains the motivation for lit-css and why the getter for styles in LitElement is static (we agree here anyway which is a good sign). |
src/lib/updating-element.ts
Outdated
* Array of styles to apply to the element. The styles should be defined | ||
* using the `css` tag function. | ||
*/ | ||
static get styles() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add type of Array<CSSStyleSheet|LiteralString>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added specific type for this and used it here.
src/lib/css-tag.ts
Outdated
*/ | ||
export const supportsAdoptedStyleSheets = ('adoptedStyleSheets' in Document.prototype); | ||
|
||
class LiteralString { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would name this CSSLiteral
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/lib/css-tag.ts
Outdated
} | ||
}; | ||
|
||
export const css = (strings: string[], ...values: any[]) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should consider having this return an object with an optional .styleSheet
and .cssText
so that there's a consistent return type for the css
tag and so the static style property has a single item type.
This would change below to just:
const cssText = values.reduce((acc, v, idx) =>
acc + literalValue(v) + strings[idx + 1], strings[0]);
return new CSSResult(cssText);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/lib/css-tag.ts
Outdated
return value.value; | ||
} else { | ||
throw new Error( | ||
`non-literal value passed to 'css' function: ${value}` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice to allow user-provided values in safe contexts with context-aware auto-escaping. Not sure how far we can go purely client-side. We may be able to allow directive-like functions that return a sentinel value that parses correctly for a particular context, then replace it, lit-html style. Soy does interesting things here at compile time. Maybe @mikesamuel has ideas.
src/lib/updating-element.ts
Outdated
styles.forEach((s) => { | ||
const style = document.createElement('style'); | ||
style.textContent = s; | ||
shadowRoot.appendChild(style); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to match Constructible StyleSheet ordering these have to come first in the ShadowRoot. Is that happening because you're directly waiting _updatePromise
? Comment if so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, comment added. Note, they come last in the shadowRoot
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't they have to come first in case there are <style>
tags in the rest of the shadow content?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see: https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets Let's include a link to the spec
src/lib/updating-element.ts
Outdated
@@ -462,7 +472,25 @@ export abstract class UpdatingElement extends HTMLElement { | |||
* @returns {Element|DocumentFragment} Returns a node into which to render. | |||
*/ | |||
protected createRenderRoot(): Element|ShadowRoot { | |||
return this.attachShadow({mode : 'open'}); | |||
const shadowRoot = this.attachShadow({mode : 'open'}); | |||
const styles = (this.constructor as typeof UpdatingElement).styles; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I (weakly) feel like all this should be in LitElement, since it does the actual rendering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the shadowRoot
rendering (creation + styles) should go together, but I think it's reasonable that this go into LitElement
and not UpdatingElement
. Let's discuss separately.
src/lib/updating-element.ts
Outdated
return this.attachShadow({mode : 'open'}); | ||
const shadowRoot = this.attachShadow({mode : 'open'}); | ||
const styles = (this.constructor as typeof UpdatingElement).styles; | ||
if (window.ShadyCSS !== undefined && !(window.ShadyCSS as any).nativeShadow) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment the three cases a little
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@daKmoR styles is an array because elements may be sharing styles, and in a Constrctible StyleSheet supporting browser the A modified version of your example: const sharedStyles = css`
h2 { color: orange; }
`;
class Base extends LitElement {
static get styles() {
return [
sharedStyles,
css`
.footer { border-top: 1px solid green; }
`
];
}
}
class El extends LitElement {
static get styles() {
return [
...super.styles,
// you probably know this is included in the parent, so this is dubious
// we can still dedupe the arrays before setting `adoptedStyleSheets` but that
// can change the cascade
sharedStyles,
css`
.some { color: yellow; }
`
];
}
} |
Is Having a custom implementation for |
unfortunately, you can not know that always... for example if you are a mixin. So sometimes you have shared stylings for two mixins but each needs it to work on its own. However, you can also apply both mixins and then you will get duplicate css. const srOnlyClass = css`.sr-only { ... }`;
const DescriptionMixin = ... // applies srOnlyClass so
class foo extends DescriptionMixin(BaseElement) // works
const DetailsMixin = ... // applies srOnlyClass so
class foo extends DetailsMixin(BaseElement) // works
// now this will apply srOnlyClass 2 times
class bar extends DescriptionMixin(DetailsMixin(BaseElement)) {} we have this actually quite often in our codebase that there is shared stylings.
true so for us we always import the (parent) stylings at the top... which worked really nicely for us. So overall arrays should work as well... I do find it a little bit more cumbersome but if it's more future proof then it's worth it. But deduping should still be done... and for a css tagged template it was nicely part of the PS: I am off work till next year so the above example I made up... if need be I could see if I can look up some of our real examples |
I really like this direction! I think it's important to consider how to compose style modules: const typographyStyles = css`...`;
const buttonStyles = css`...`;
// a standalone style module
const styleModuleA = css`...`;
// a style module that composes with another
const styleModuleB = [typographyStyles, css`...`];
class MyElement extends LitElement {
static get styles() {
return [
typographyStyles,
buttonStyles,
styleModuleA,
...styleModuleB,
];
}
} I think this creates two challenges:
|
src/lib/css-tag.ts
Outdated
result.styleSheet = new CSSStyleSheet() as ConstructableStyleSheet; | ||
result.styleSheet.replaceSync(cssText); | ||
} else { | ||
result.cssText = new CSSLiteral(cssText); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can always assign this field
Removes duplicate styles in the `static get styles` array. This is useful when styles are cmposed via mixins/subclasses. To try to preserve cascade ordering, the last duplicate style in the list is preserved.
Not only that, but just disabling shadowDom by returning |
That's just going to be true of any styles. If you have styles that assume ShadowDOM scoping, they're going to do the wrong thing if you don't render to a ShadowRoot. In this case at least nothing will happen with the styles unless you do something yourself, which is probably better than them being applied globally by default. |
src/lib/css-tag.ts
Outdated
|
||
class CSSLiteral { | ||
|
||
value: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
readonly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/lib/css-tag.ts
Outdated
value: string; | ||
|
||
constructor(value: string) { | ||
this.value = value.toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the .toString()
? If value
can be something other than a string, update the type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
src/lib/updating-element.ts
Outdated
* property. Styling will apply using `shadowRoot.adoptedStyleSheets` where | ||
* available and will fallback otherwise. | ||
*/ | ||
protected createRenderRootStyles(shadowRoot: ShadowRoot) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is too generic, since this doesn't work with any render root, but only ShadowRoots. How about adoptStyles
or applyStyles
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to adoptStyles
src/lib/updating-element.ts
Outdated
*/ | ||
protected createRenderRootStyles(shadowRoot: ShadowRoot) { | ||
let styles = (this.constructor as typeof UpdatingElement).styles; | ||
// de-duplicate styles preserving the last item in the list. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add why this is important
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added note, for performance.
src/lib/updating-element.ts
Outdated
protected createRenderRootStyles(shadowRoot: ShadowRoot) { | ||
let styles = (this.constructor as typeof UpdatingElement).styles; | ||
// de-duplicate styles preserving the last item in the list. | ||
const stylesSet = new Set(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this is better, but it gets rid of the deletes (which involve hashes) in exchange for a reverse:
const stylesSet = new Set();
for (const i = styles.length - 1; i >= 0; i==) {
s.add(styles[i]);
}
styles = [...styleSet].reverse();
or:
styles = [...new Set([...styles].reverse())].reverse();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used reduceRight
+ reverse
src/lib/updating-element.ts
Outdated
styles.forEach((s) => { | ||
const style = document.createElement('style'); | ||
style.textContent = s; | ||
shadowRoot.appendChild(style); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see: https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets Let's include a link to the spec
src/lib/updating-element.ts
Outdated
/** | ||
* Applies styling to the element shadowRoot using the `static get styles` | ||
* property. Styling will apply using `shadowRoot.adoptedStyleSheets` where | ||
* available and will fallback otherwise. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's describe the fallback. Specifically that it appends <style>
elements to the ShadowRoot after the first render. This will be important for other rendering libraries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
src/lib/updating-element.ts
Outdated
export * from './css-tag.js'; | ||
|
||
// Augment existing types with bleeding edge API. | ||
declare global { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we may want these here: https://github.com/Polymer/lit-element/blob/master/src/env.d.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added types to env.d.ts
src/lib/css-tag.ts
Outdated
*/ | ||
export const supportsAdoptedStyleSheets = ('adoptedStyleSheets' in Document.prototype); | ||
|
||
interface ConstructableStyleSheet extends CSSStyleSheet { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to w3c/csswg-drafts#3433 these additions are going to be merged into CSSStyleSheet
, so add them globally as with adoptedStyleSheets
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/lib/css-tag.ts
Outdated
} | ||
}; | ||
|
||
export type CSSStyleSheetOrCssText = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think Or
is the best naming anymore. It's really specific to this operation, so something like CSSResult
seems fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed.
src/lib/updating-element.ts
Outdated
* available and will fallback otherwise. | ||
*/ | ||
protected createRenderRootStyles(shadowRoot: ShadowRoot) { | ||
let styles = (this.constructor as typeof UpdatingElement).styles; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check if styles is undefined and return to avoid extra works on elements that don't declare styles?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done.
src/lib/css-tag.ts
Outdated
@@ -0,0 +1,52 @@ | |||
/** | |||
@license | |||
Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be 2019 I assume
* simplified `css` function and removed `cssLiteral`; values must be `css` results * updated types and put into `env.d.ts` * memoized unique-ifying `styles` * `adoptStyles` now does the style adoption and is called separately from `createRenderRoot` and only if the root is a ShadowRoot. * updated changelog.
src/lib/updating-element.ts
Outdated
@@ -415,6 +444,12 @@ export abstract class UpdatingElement extends HTMLElement { | |||
*/ | |||
protected initialize() { | |||
this.renderRoot = this.createRenderRoot(); | |||
// Note, tf renderRoot is not a shadowRoot, styles would/could apply to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"tf" should be "if"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! (when the ShadyCSS change is in)
src/lib/css-tag.ts
Outdated
|
||
_styleSheet?: CSSStyleSheet|null; | ||
|
||
constructor(public readonly cssText: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer not to use parameter properties, because it's non-standard JS outside of the type system. I collected guidance in the lit-html repo: https://github.com/Polymer/lit-html/blob/master/CONTRIBUTING.md#typescript
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/lib/css-tag.ts
Outdated
if (this._styleSheet === undefined) { | ||
// Note, assume that if adoptedStyleSheets is supported, | ||
// constructable stylesheets are as well. | ||
if (supportsAdoptedStyleSheets) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename to supportsCSSShadowParts
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to supportsAdoptingStyleSheets
and added explicit check for construct-able stylesheet support.
src/lib/updating-element.ts
Outdated
*/ | ||
protected adoptStyles() { | ||
const styles = (this.constructor as typeof UpdatingElement)._uniqueStyles; | ||
if (!styles.length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer if (style.length === 0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/lib/updating-element.ts
Outdated
// Note, if renderRoot is not a shadowRoot, styles would/could apply to the | ||
// element's getRootNode(). While this could be done, we're choosing not to | ||
// support this now since it would require different logic around de-duping. | ||
if (this.renderRoot instanceof window.ShadowRoot) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will fail in a env that does not supports shadow dom and not is pollyfied. Such env would be supported at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, fixed.
* moves `renderRoot` and all styling features from UpdatingElement to LitElement * remove unncessary checking for constractable stylesheet support
})(); | ||
|
||
export const supportsAdoptingStyleSheets = supportsConstructableStyleSheets && | ||
('adoptedStyleSheets' in Document.prototype); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adoptedStylesheets
is sufficient because it's unlikely a browser would ever ship that without constructable stylesheets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
src/lib/updating-element.ts
Outdated
// This matches the spec'd behavior of `adoptedStyleSheets`. | ||
// Ensure an updated is requested and then render styling directly after the update. | ||
this.requestUpdate(); | ||
this._updatePromise.then(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing lit-html
to have a render option that allows the user to specify a "before reference" to create the part before would allow us to remove this timing code here and instead put the <style>
s in synchronously and just say render(result, this.renderRoot, {beforeRef: this.renderRoot.firstChild})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since part
is cached off the container, avoiding this. However, moved this case to update
to avoid the fancy waiting here.
src/lib/updating-element.ts
Outdated
// preserving the last item in the list. The last item is kept to | ||
// try to preserve cascade order with the assumption that it's most | ||
// important that last added styles override previous styles. | ||
const styleSet = styles.reduceRight((set, s) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can just be styles.reduceRight((set, s) => set.add(s), new Set())
(set.add returns the set)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). | |||
## Unreleased | |||
|
|||
### Added | |||
* Added `static get styles()` to allow defining element styling separate from `render` method. | |||
This takes advantage of [`adoptedStyleSheets`](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets) when possible ([#391](https://github.com/Polymer/lit-element/issues/391)). | |||
* Added the `performUpdate` method to allow control of update timing ([#290](https://github.com/Polymer/lit-element/issues/290)). | |||
* Updates deferred until first connection ([#258](https://github.com/Polymer/lit-element/issues/258)). | |||
### Changed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add [breaking]
change notice about moving createRenderRoot
and related functionality from UpdatingElement
to LitElement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// (1) shadowRoot polyfilled: use ShadyCSS | ||
// (2) shadowRoot.adoptedStyleSheets available: use it. | ||
// (3) shadowRoot.adoptedStyleSheets polyfilled: append styles after rendering. | ||
if (window.ShadyCSS !== undefined && !window.ShadyCSS.nativeShadow) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth moving more of the prep steps to the static one-time spot (_uniqueStyles
)? Basically, move the map
calls out of instance-time work, and possibly also create a template of styles to clone for the final case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem worth it.
Cleans up the Shadow DOM + shim adoptedStyles case: * avoids making the `updatePromise` protected * handles stamping styles into shadowRoot after render.
|
Fixes #391.
This should be implemented to return an array of styles defined using the
css
tag function. These styles will be applied withadoptedStyleSheets
where available and shimmed when not.