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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ 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
Copy link
Member

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

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.

* [Breaking] The `createRenderRoot` method has moved from `UpdatingElement` to `LitElement`. Therefore, `UpdatingElement` no longer creates a `shadowRoot` by default ([#391](https://github.com/Polymer/lit-element/issues/391)).
* [Breaking] Changes property options to add `converter`. This option works the same as the previous `type` option except that the `converter` methods now also get `type` as the second argument. This effectively changes `type` to be a hint for the `converter`. A default `converter` is used if none is provided and it now supports `Boolean`, `String`, `Number`, `Object`, and `Array` ([#264](https://github.com/Polymer/lit-element/issues/264)).
* [Breaking] Numbers and strings now become null if their reflected attribute is removed (https://github.com/Polymer/lit-element/issues/264)).
* [Breaking] Previously, when an attribute changed as a result of a reflecting property changing, the property was prevented from mutating again as can happen when a custom
Expand Down
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
1,093 changes: 394 additions & 699 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@
"devDependencies": {
"@types/chai": "^4.0.1",
"@types/mocha": "^5.2.4",
"@webcomponents/shadycss": "^1.5.2",
"@webcomponents/webcomponentsjs": "^2.2.1",
"@webcomponents/shadycss": "^1.8.0",
"@webcomponents/webcomponentsjs": "^2.2.2",
"chai": "^4.0.2",
"mocha": "^5.0.5",
"rollup": "^0.64.1",
"rollup-plugin-filesize": "^4.0.1",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-terser": "^1.0.1",
"tslint": "^5.7.0",
"tslint": "^5.12.0",
"typedoc": "^0.8.0",
"typescript": "^3.0.3",
"typescript": "^3.2.2",
"uglify-es": "^3.3.9",
"wct-mocha": "^1.0.0",
"web-component-tester": "^6.9.0"
"web-component-tester": "^6.9.2"
},
"typings": "lit-element.d.ts",
"dependencies": {
Expand Down
17 changes: 17 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
interface ShadyCSS {
styleElement(host: Element, overrideProps?: {[key: string]: string}): void;
getComputedStyleValue(element: Element, property: string): string;
ScopingShim: {
prepareAdoptedCssText(cssText: string[], name: string): void;
}
nativeShadow: boolean;
}

interface ShadyDOM {
Expand All @@ -10,4 +14,17 @@ interface ShadyDOM {
interface Window {
ShadyCSS?: ShadyCSS;
ShadyDOM?: ShadyDOM;
ShadowRoot: typeof ShadowRoot;
}

// Augment existing types with styling API
interface ShadowRoot {
adoptedStyleSheets: CSSStyleSheet[];
}

declare var ShadowRoot: {prototype: ShadowRoot; new () : ShadowRoot;}

interface CSSStyleSheet {
replaceSync(cssText: string): void;
replace(cssText: string): Promise<unknown>;
}
56 changes: 56 additions & 0 deletions src/lib/css-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
@license
Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
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 supportsAdoptingStyleSheets =
('adoptedStyleSheets' in Document.prototype);
Copy link
Member

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.

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.


export class CSSResult {

_styleSheet?: CSSStyleSheet|null;

readonly cssText: string;

constructor(cssText: string) { this.cssText = cssText; }

// Note, this is a getter so that it's lazy. In practice, this means
// stylesheets are not created until the first element instance is made.
get styleSheet(): CSSStyleSheet|null {
if (this._styleSheet === undefined) {
// Note, if `adoptedStyleSheets` is supported then we assume CSSStyleSheet
// is constructable.
if (supportsAdoptingStyleSheets) {
this._styleSheet = new CSSStyleSheet();
this._styleSheet.replaceSync(this.cssText);
} else {
this._styleSheet = null;
}
}
return this._styleSheet;
}
}

const textFromCSSResult = (value: CSSResult) => {
if (value instanceof CSSResult) {
return value.cssText;
} else {
throw new Error(
`Value passed to 'css' function must be a 'css' function result: ${
value}.`);
}
};

export const css =
(strings: TemplateStringsArray, ...values: CSSResult[]): CSSResult => {
const cssText = values.reduce(
(acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1],
strings[0]);
return new CSSResult(cssText);
};
40 changes: 6 additions & 34 deletions src/lib/updating-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,26 +397,16 @@ export abstract class UpdatingElement extends HTMLElement {
private _reflectingProperties: Map<PropertyKey, PropertyDeclaration>|
undefined = undefined;

/**
* Node or ShadowRoot into which element DOM should be rendered. Defaults
* to an open shadowRoot.
*/
protected renderRoot?: Element|DocumentFragment;

constructor() {
super();
this.initialize();
}

/**
* Performs element initialization. By default this calls `createRenderRoot`
* to create the element `renderRoot` node and captures any pre-set values for
* Performs element initialization. By default captures any pre-set values for
* registered properties.
*/
protected initialize() {
this.renderRoot = this.createRenderRoot();
this._saveInstanceProperties();
}
protected initialize() { this._saveInstanceProperties(); }

/**
* Fixes any properties set on the instance before upgrade time.
Expand Down Expand Up @@ -454,20 +444,6 @@ export abstract class UpdatingElement extends HTMLElement {
this._instanceProperties = undefined;
}

/**
* Returns the node into which the element should render and by default
* creates and returns an open shadowRoot. Implement to customize where the
* element's DOM is rendered. For example, to render into the element's
* childNodes, return `this`.
* @returns {Element|DocumentFragment} Returns a node into which to render.
*/
protected createRenderRoot(): Element|ShadowRoot {
return this.attachShadow({mode : 'open'});
}

/**
* Uses ShadyCSS to keep element DOM updated.
*/
connectedCallback() {
this._updateState = this._updateState | STATE_HAS_CONNECTED;
// Ensure connection triggers an update. Updates cannot complete before
Expand All @@ -480,12 +456,6 @@ export abstract class UpdatingElement extends HTMLElement {
} else {
this.requestUpdate();
}
// Note, first update/render handles styleElement so we only call this if
// connected after first update.
if ((this._updateState & STATE_HAS_UPDATED) &&
window.ShadyCSS !== undefined) {
window.ShadyCSS.styleElement(this);
}
}

/**
Expand Down Expand Up @@ -632,6 +602,8 @@ export abstract class UpdatingElement extends HTMLElement {
return (this._updateState & STATE_UPDATE_REQUESTED);
}

protected get hasUpdated() { return (this._updateState & STATE_HAS_UPDATED); }

/**
* Performs an element update.
*
Expand Down Expand Up @@ -696,8 +668,8 @@ export abstract class UpdatingElement extends HTMLElement {

/**
* Updates the element. This method reflects property values to attributes.
* It can be overridden to render and keep updated DOM in the element's
* `renderRoot`. Setting properties inside this method will *not* trigger
* It can be overridden to render and keep updated element DOM.
* Setting properties inside this method will *not* trigger
* another update.
*
* * @param _changedProperties Map of changed properties with old values
Expand Down
111 changes: 111 additions & 0 deletions src/lit-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {PropertyValues, UpdatingElement} from './lib/updating-element.js';
export * from './lib/updating-element.js';
export * from './lib/decorators.js';
export {html, svg} from 'lit-html/lit-html';
import {supportsAdoptingStyleSheets, CSSResult} from './lib/css-tag.js';
export * from './lib/css-tag.js';

export class LitElement extends UpdatingElement {

Expand All @@ -31,6 +33,104 @@ export class LitElement extends UpdatingElement {
*/
static render = render;

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

private static _styles: CSSResult[]|undefined;

private static get _uniqueStyles(): CSSResult[] {
if (this._styles === undefined) {
const styles = this.styles;
// As a performance optimization to avoid duplicated styling that can
// occur especially when composing via subclassing, de-duplicate styles
// 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) => set.add(s), new Set());
this._styles = Array.from(styleSet).reverse();
}
return this._styles;
}

private _needsShimAdoptedStyleSheets?: boolean;

/**
* Node or ShadowRoot into which element DOM should be rendered. Defaults
* to an open shadowRoot.
*/
protected renderRoot?: Element|DocumentFragment;

/**
* Performs element initialization. By default this calls `createRenderRoot`
* to create the element `renderRoot` node and captures any pre-set values for
* registered properties.
*/
protected initialize() {
super.initialize();
this.renderRoot = this.createRenderRoot();
// 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 (window.ShadowRoot && this.renderRoot instanceof window.ShadowRoot) {
this.adoptStyles();
}
}

/**
* Returns the node into which the element should render and by default
* creates and returns an open shadowRoot. Implement to customize where the
* element's DOM is rendered. For example, to render into the element's
* childNodes, return `this`.
* @returns {Element|DocumentFragment} Returns a node into which to render.
*/
protected createRenderRoot(): Element|ShadowRoot {
return this.attachShadow({mode : 'open'});
}

/**
* Applies styling to the element shadowRoot using the `static get styles`
* property. Styling will apply using `shadowRoot.adoptedStyleSheets` where
* available and will fallback otherwise. When Shadow DOM is polyfilled,
* ShadyCSS scopes styles and adds them to the document. When Shadow DOM
* is available but `adoptedStyleSheets` is not, styles are appended to the
* end of the `shadowRoot` to [mimic spec
* behavior](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets).
*/
protected adoptStyles() {
const styles = (this.constructor as typeof LitElement)._uniqueStyles;
if (styles.length === 0) {
return;
}
// 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: append styles after
// rendering
if (window.ShadyCSS !== undefined && !window.ShadyCSS.nativeShadow) {
Copy link
Member

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?

Copy link
Member Author

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.

window.ShadyCSS.ScopingShim.prepareAdoptedCssText(styles.map((s) => s.cssText),
this.localName);
} else if (supportsAdoptingStyleSheets) {
(this.renderRoot as ShadowRoot).adoptedStyleSheets =
styles.map((s) => s.styleSheet!);
} else {
// This must be done after rendering so the actual style insertion is done
// in `update`.
this._needsShimAdoptedStyleSheets = true;
}
}

connectedCallback() {
super.connectedCallback();
// Note, first update/render handles styleElement so we only call this if
// connected after first update.
if (this.hasUpdated && window.ShadyCSS !== undefined) {
window.ShadyCSS.styleElement(this);
}
}

/**
* Updates the element. This method reflects property values to attributes
* and calls `render` to render DOM via lit-html. Setting properties inside
Expand All @@ -45,6 +145,17 @@ export class LitElement extends UpdatingElement {
.render(templateResult, this.renderRoot!,
{scopeName : this.localName!, eventContext : this});
}
// When native Shadow DOM is used but adoptedStyles are not supported,
// insert styling after rendering to ensure adoptedStyles have highest
// priority.
if (this._needsShimAdoptedStyleSheets) {
this._needsShimAdoptedStyleSheets = false;
(this.constructor as typeof LitElement)._uniqueStyles.forEach((s) => {
const style = document.createElement('style');
style.textContent = s.cssText;
this.renderRoot!.appendChild(style);
});
}
}

/**
Expand Down
Loading