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

Adds static get styles() #401

Merged
merged 15 commits into from
Jan 10, 2019
20 changes: 9 additions & 11 deletions demo/lit-element.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
<ts-element message="Yo" more-info="person"></ts-element>

<script type="module">
import { LitElement, html } from '../lit-element.js';
import { LitElement, html, css } from '../lit-element.js';

class Inner extends LitElement {

static get styles() {
return [css`:host { color: green; }`];
}
render() {
return html`Hello world`;
}
Expand All @@ -34,23 +38,13 @@
static get properties() {
return {
nug: {},
zot: {},
foo: {},
bar: {},
whales: {type: Number},
fooBar: {converter: {fromAttribute: parseInt, toAttribute: (value) => value + '-attr'}, reflect: true}
}
}

// a custom getter/setter can be created to customize property processing
get zot() { return this.getAttribute('zot'); }

set zot(value) {
this.setAttribute('zot', value);
this.invalidate();
}


constructor() {
super();
this.foo = 'foo';
Expand All @@ -64,6 +58,10 @@
});
}

static get styles() {
return [css`h4 { color: orange;} `];
}

render() {
const {foo, bar, whales, fooBar, nug} = this;
return html`
Expand Down
61 changes: 61 additions & 0 deletions src/lib/css-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
Copy link
Contributor

@kenchris kenchris Dec 22, 2018

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

This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
export const supportsAdoptedStyleSheets = ('adoptedStyleSheets' in Document.prototype);

interface ConstructableStyleSheet extends CSSStyleSheet {
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

replaceSync(cssText: string): void;
replace(cssText: string): Promise<unknown>;
}

class CSSLiteral {

value: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

readonly

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.


constructor(value: string) {
this.value = value.toString();
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed.

}

toString() {
return this.value;
}
}

const cssLiteralValue = (value: CSSLiteral) => {
if (value instanceof CSSLiteral) {
return value.value;
} else {
throw new Error(
`Non-literal value passed to 'css' function: ${value}.`
);
}
};

export type CSSStyleSheetOrCssText = {
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed.

cssText?: CSSLiteral,
styleSheet?: ConstructableStyleSheet}
;

export const css = (strings: TemplateStringsArray, ...values: CSSLiteral[]): CSSStyleSheetOrCssText => {
const cssText = values.reduce((acc, v, idx) =>
acc + cssLiteralValue(v) + strings[idx + 1], strings[0]);
const result: CSSStyleSheetOrCssText = {};
if (supportsAdoptedStyleSheets) {
result.styleSheet = new CSSStyleSheet() as ConstructableStyleSheet;
result.styleSheet.replaceSync(cssText);
} else {
result.cssText = new CSSLiteral(cssText);
Copy link
Contributor

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

}
return result;
};

export const cssLiteral = (strings: TemplateStringsArray, ...values: any[]) => {
return new CSSLiteral(values.reduce((acc, v, idx) =>
acc + cssLiteralValue(v) + strings[idx + 1], strings[0]));
};
55 changes: 54 additions & 1 deletion src/lib/updating-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {supportsAdoptedStyleSheets, CSSStyleSheetOrCssText} from './css-tag.js';
export * from './css-tag.js';

// Augment existing types with bleeding edge API.
declare global {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

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

interface ShadyCSS {
prepareAdoptedCssText(cssText: string[], name: string): void;
}

interface ShadowRoot {
adoptedStyleSheets: CSSStyleSheet[];
}
}

/**
* Returns the property descriptor for a property on this prototype by walking
Expand Down Expand Up @@ -215,6 +228,14 @@ export abstract class UpdatingElement extends HTMLElement {

static properties: PropertyDeclarations = {};

/**
* Array of styles to apply to the element. The styles should be defined
* using the `css` tag function.
*/
static get styles(): CSSStyleSheetOrCssText[] {
return [];
}

/**
* Returns a list of attributes corresponding to the registered properties.
*/
Expand Down Expand Up @@ -462,7 +483,39 @@ 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'});
this.createRenderRootStyles(shadowRoot);
return shadowRoot;
}

/**
* Applies styling to the element shadowRoot using the `static get styles`
* property. Styling will apply using `shadowRoot.adoptedStyleSheets` where
* available and will fallback otherwise.
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added.

*/
protected createRenderRootStyles(shadowRoot: ShadowRoot) {
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to adoptStyles

const styles = (this.constructor as typeof UpdatingElement).styles;
Copy link
Contributor

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.

Copy link
Member Author

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.

// There are three separate cases here based on Shadow DOM support:
// (1) shadowRoot polyfilled: use ShadyCSS
// (2) shadowRoot.adoptedStyleSheets available: use it.
// (3) shadowRoot.adoptedStyleSheets polyfilled: add styles after rendering.
if (window.ShadyCSS !== undefined && !(window.ShadyCSS as any).nativeShadow) {
Copy link
Contributor

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

window.ShadyCSS.prepareAdoptedCssText(styles.map((s) => s.cssText!.toString()), this.localName);
} else if (supportsAdoptedStyleSheets) {
shadowRoot.adoptedStyleSheets = styles.map((s) => s.styleSheet!);
} else {
// Ensure styling comes after rendering so styles are *after* all other rendered content.
// 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(() => {
Copy link
Member

@kevinpschaaf kevinpschaaf Jan 9, 2019

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})

Copy link
Member Author

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.

styles.forEach((s) => {
const style = document.createElement('style');
style.textContent = s.cssText!.toString();
shadowRoot.appendChild(style);
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

});
});
}
}

/**
Expand Down
117 changes: 117 additions & 0 deletions src/test/lit-element_styling_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import '@webcomponents/shadycss/apply-shim.min.js';

import {
html as htmlWithStyles,
css, cssLiteral,
LitElement,
} from '../lit-element.js';

Expand Down Expand Up @@ -330,6 +331,122 @@ suite('Styling', () => {
});
});

suite('Static get styles', () => {
let container: HTMLElement;

setup(() => {
container = document.createElement('div');
document.body.appendChild(container);
});

teardown(() => {
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
});

test('content shadowRoot is styled via static get styles', async () => {
const name = generateElementName();
customElements.define(name, class extends LitElement {

static get styles() {
return [css`div {
border: 2px solid blue;
}`, css`span {
display: block;
border: 3px solid blue;
}`];
}

render() {
return htmlWithStyles`
<div>Testing1</div>
<span>Testing2</span>`;
}
});
const el = document.createElement(name);
container.appendChild(el);
await (el as LitElement).updateComplete;
const div = el.shadowRoot!.querySelector('div');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '2px');
const span = el.shadowRoot!.querySelector('span');
assert.equal(getComputedStyleValue(span!, 'border-top-width').trim(), '3px');
});

test('static get styles allows `cssLiteral` values', async () => {
const name = generateElementName();
customElements.define(name, class extends LitElement {

static get styles() {
return [css`div {
border: ${cssLiteral`2px solid blue`};
}`, css`span {
display: block;
border: ${cssLiteral`3px solid blue`};
}`];
}

render() {
return htmlWithStyles`
<div>Testing1</div>
<span>Testing2</span>`;
}
});
const el = document.createElement(name);
container.appendChild(el);
await (el as LitElement).updateComplete;
const div = el.shadowRoot!.querySelector('div');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '2px');
const span = el.shadowRoot!.querySelector('span');
assert.equal(getComputedStyleValue(span!, 'border-top-width').trim(), '3px');
});

test('`css` get styles throws when unsafe values are used', async () => {
assert.throws(() => {
css`div { border: ${`2px solid blue;` as any}}`;
});
});

test('styles in render compose with `static get styles`', async () => {
const name = generateElementName();
customElements.define(name, class extends LitElement {

static get styles() {
return [css`div {
border: 2px solid blue;
}`, css`span {
display: block;
border: 3px solid blue;
}`];
}

render() {
return htmlWithStyles`
<style>
div {
padding: 4px;
}
span {
display: block;
border: 4px solid blue;
}
</style>
<div>Testing1</div>
<span>Testing2</span>`;
}
});
const el = document.createElement(name);
container.appendChild(el);
await (el as LitElement).updateComplete;
const div = el.shadowRoot!.querySelector('div');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '2px');
assert.equal(getComputedStyleValue(div!, 'padding-top').trim(), '4px');
const span = el.shadowRoot!.querySelector('span');
assert.equal(getComputedStyleValue(span!, 'border-top-width').trim(), '3px');
});

});

suite('ShadyDOM', () => {
let container: HTMLElement;

Expand Down