Socket
Socket
Sign inDemoInstall

@polymer/lit-element

Package Overview
Dependencies
Maintainers
9
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@polymer/lit-element - npm Package Compare versions

Comparing version 0.5.2 to 0.6.0-dev.1

demo/ts-element.d.ts

175

lit-element.d.ts

@@ -1,170 +0,17 @@

/**
* @license
* Copyright (c) 2017 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
*/
import { PropertiesChangedConstructor } from '@polymer/polymer/lib/mixins/properties-changed.js';
import { PropertiesMixinConstructor } from '@polymer/polymer/lib/mixins/properties-mixin.js';
import { TemplateResult } from 'lit-html/lit-html.js';
export { PropertiesChangedConstructor } from '@polymer/polymer/lib/mixins/properties-changed.js';
export { PropertiesMixinConstructor } from '@polymer/polymer/lib/mixins/properties-mixin.js';
export { html, svg } from 'lit-html/lib/lit-extended.js';
export declare type __unused = PropertiesChangedConstructor & PropertiesMixinConstructor;
/**
* Renders attributes to the given element based on the `attrInfo` object where
* boolean values are added/removed as attributes.
* @param element Element on which to set attributes.
* @param attrInfo Object describing attributes.
*/
export declare function renderAttributes(element: HTMLElement, attrInfo: {
[name: string]: string | boolean | number;
}): void;
/**
* Returns a string of css class names formed by taking the properties
* in the `classInfo` object and appending the property name to the string of
* class names if the property value is truthy.
* @param classInfo
*/
export declare function classString(classInfo: {
[name: string]: string | boolean | number;
}): string;
/**
* Returns a css style string formed by taking the properties in the `styleInfo`
* object and appending the property name (dash-cased) colon the
* property value. Properties are separated by a semi-colon.
* @param styleInfo
*/
export declare function styleString(styleInfo: {
[name: string]: string | boolean | number;
}): string;
declare const LitElement_base: {
new (): HTMLElement;
prototype: HTMLElement;
} & PropertiesMixinConstructor & PropertiesChangedConstructor;
export declare class LitElement extends LitElement_base {
private __renderComplete;
private __resolveRenderComplete;
private __isInvalid;
private __isChanging;
private _root?;
import { TemplateResult } from 'lit-html';
import { UpdatingElement, PropertyValues } from './lib/updating-element.js';
export { property, identity, BooleanAttribute, PropertyDeclarations, PropertyDeclaration, PropertyValues } from './lib/updating-element.js';
export { html, svg } from 'lit-html/lib/lit-extended';
export declare abstract class LitElement extends UpdatingElement {
/**
* Override which sets up element rendering by calling* `_createRoot`
* and `_firstRendered`.
* Override which performs element rendering by calling the `render` method.
* Override to perform tasks before and/or after updating.
*/
ready(): void;
connectedCallback(): void;
protected update(_props: PropertyValues): void;
/**
* Called after the element DOM is rendered for the first time.
* Implement to perform tasks after first rendering like capturing a
* reference to a static node which must be directly manipulated.
* This should not be commonly needed. For tasks which should be performed
* before first render, use the element constructor.
*/
_firstRendered(): void;
/**
* Implement to customize where the element's template is rendered by
* returning an element into which to render. By default this creates
* a shadowRoot for the element. To render into the element's childNodes,
* return `this`.
* @returns {Element|DocumentFragment} Returns a node into which to render.
*/
protected _createRoot(): Element | DocumentFragment;
/**
* Override which returns the value of `_shouldRender` which users
* should implement to control rendering. If this method returns false,
* _propertiesChanged will not be called and no rendering will occur even
* if property values change or `requestRender` is called.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
* @returns {boolean} Default implementation always returns true.
*/
_shouldPropertiesChange(_props: object, _changedProps: object, _prevProps: object): boolean;
/**
* Implement to control if rendering should occur when property values
* change or `requestRender` is called. By default, this method always
* returns true, but this can be customized as an optimization to avoid
* rendering work when changes occur which should not be rendered.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
* @returns {boolean} Default implementation always returns true.
*/
protected _shouldRender(_props: object, _changedProps: object, _prevProps: object): boolean;
/**
* Override which performs element rendering by calling
* `_render`, `_applyRender`, and finally `_didRender`.
* @param props Current element properties
* @param changedProps Changing element properties
* @param prevProps Previous element properties
*/
_propertiesChanged(props: object, changedProps: object, prevProps: object): void;
_flushProperties(): void;
/**
* Override which warns when a user attempts to change a property during
* the rendering lifecycle. This is an anti-pattern and should be avoided.
* @param property {string}
* @param value {any}
* @param old {any}
*/
_shouldPropertyChange(property: string, value: any, old: any): boolean;
/**
* Implement to describe the DOM which should be rendered in the element.
* Ideally, the implementation is a pure function using only props to describe
* the element template. The implementation must return a `lit-html`
* TemplateResult. By default this template is rendered into the element's
* shadowRoot. This can be customized by implementing `_createRoot`. This
* method must be implemented.
* @param {*} _props Current element properties
Invoked on each update to perform rendering tasks. This method must return a
lit-html TemplateResult.
* @returns {TemplateResult} Must return a lit-html TemplateResult.
*/
protected _render(_props: object): TemplateResult;
/**
* Renders the given lit-html template `result` into the given `node`.
* Implement to customize the way rendering is applied. This is should not
* typically be needed and is provided for advanced use cases.
* @param result {TemplateResult} `lit-html` template result to render
* @param node {Element|DocumentFragment} node into which to render
*/
protected _applyRender(result: TemplateResult, node: Element | DocumentFragment): void;
/**
* Called after element DOM has been rendered. Implement to
* directly control rendered DOM. Typically this is not needed as `lit-html`
* can be used in the `_render` method to set properties, attributes, and
* event listeners. However, it is sometimes useful for calling methods on
* rendered elements, like calling `focus()` on an element to focus it.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
*/
protected _didRender(_props: object, _changedProps: object, _prevProps: object): void;
/**
* Call to request the element to asynchronously re-render regardless
* of whether or not any property changes are pending.
*/
requestRender(): void;
/**
* Override which provides tracking of invalidated state.
*/
_invalidateProperties(): void;
/**
* Returns a promise which resolves after the element next renders.
* The promise resolves to `true` if the element rendered and `false` if the
* element did not render.
* This is useful when users (e.g. tests) need to react to the rendered state
* of the element after a change is made.
* This can also be useful in event handlers if it is desireable to wait
* to send an event until after rendering. If possible implement the
* `_didRender` method to directly respond to rendering within the
* rendering lifecycle.
*/
readonly renderComplete: Promise<boolean>;
protected abstract render(): TemplateResult;
}

@@ -1,240 +0,33 @@

import { PropertiesMixin } from '@polymer/polymer/lib/mixins/properties-mixin.js';
import { camelToDashCase } from '@polymer/polymer/lib/utils/case-map.js';
import { render } from 'lit-html/lib/shady-render.js';
export { html, svg } from 'lit-html/lib/lit-extended.js';
/**
* Renders attributes to the given element based on the `attrInfo` object where
* boolean values are added/removed as attributes.
* @param element Element on which to set attributes.
* @param attrInfo Object describing attributes.
* @license
* Copyright (c) 2017 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 function renderAttributes(element, attrInfo) {
for (const a in attrInfo) {
const v = attrInfo[a] === true ? '' : attrInfo[a];
if (v || v === '' || v === 0) {
if (element.getAttribute(a) !== v) {
element.setAttribute(a, String(v));
}
}
else if (element.hasAttribute(a)) {
element.removeAttribute(a);
}
}
}
/**
* Returns a string of css class names formed by taking the properties
* in the `classInfo` object and appending the property name to the string of
* class names if the property value is truthy.
* @param classInfo
*/
export function classString(classInfo) {
const o = [];
for (const name in classInfo) {
const v = classInfo[name];
if (v) {
o.push(name);
}
}
return o.join(' ');
}
/**
* Returns a css style string formed by taking the properties in the `styleInfo`
* object and appending the property name (dash-cased) colon the
* property value. Properties are separated by a semi-colon.
* @param styleInfo
*/
export function styleString(styleInfo) {
const o = [];
for (const name in styleInfo) {
const v = styleInfo[name];
if (v || v === 0) {
o.push(`${camelToDashCase(name)}: ${v}`);
}
}
return o.join('; ');
}
export class LitElement extends PropertiesMixin(HTMLElement) {
constructor() {
super(...arguments);
this.__renderComplete = null;
this.__resolveRenderComplete = null;
this.__isInvalid = false;
this.__isChanging = false;
}
import { render } from 'lit-html/lib/shady-render';
import { UpdatingElement } from './lib/updating-element.js';
export { property, identity, BooleanAttribute } from './lib/updating-element.js';
export { html, svg } from 'lit-html/lib/lit-extended';
export class LitElement extends UpdatingElement {
/**
* Override which sets up element rendering by calling* `_createRoot`
* and `_firstRendered`.
* Override which performs element rendering by calling the `render` method.
* Override to perform tasks before and/or after updating.
*/
ready() {
this._root = this._createRoot();
super.ready();
this._firstRendered();
}
connectedCallback() {
if (window.ShadyCSS && this._root) {
window.ShadyCSS.styleElement(this);
update(_props) {
super.update(_props);
if (typeof this.render === 'function') {
render(this.render(), this.renderRoot, this.localName);
}
super.connectedCallback();
}
/**
* Called after the element DOM is rendered for the first time.
* Implement to perform tasks after first rendering like capturing a
* reference to a static node which must be directly manipulated.
* This should not be commonly needed. For tasks which should be performed
* before first render, use the element constructor.
*/
_firstRendered() { }
/**
* Implement to customize where the element's template is rendered by
* returning an element into which to render. By default this creates
* a shadowRoot for the element. To render into the element's childNodes,
* return `this`.
* @returns {Element|DocumentFragment} Returns a node into which to render.
*/
_createRoot() {
return this.attachShadow({ mode: 'open' });
}
/**
* Override which returns the value of `_shouldRender` which users
* should implement to control rendering. If this method returns false,
* _propertiesChanged will not be called and no rendering will occur even
* if property values change or `requestRender` is called.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
* @returns {boolean} Default implementation always returns true.
*/
_shouldPropertiesChange(_props, _changedProps, _prevProps) {
const shouldRender = this._shouldRender(_props, _changedProps, _prevProps);
if (!shouldRender && this.__resolveRenderComplete) {
this.__resolveRenderComplete(false);
else {
throw new Error('render() not implemented');
}
return shouldRender;
}
/**
* Implement to control if rendering should occur when property values
* change or `requestRender` is called. By default, this method always
* returns true, but this can be customized as an optimization to avoid
* rendering work when changes occur which should not be rendered.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
* @returns {boolean} Default implementation always returns true.
*/
_shouldRender(_props, _changedProps, _prevProps) {
return true;
}
/**
* Override which performs element rendering by calling
* `_render`, `_applyRender`, and finally `_didRender`.
* @param props Current element properties
* @param changedProps Changing element properties
* @param prevProps Previous element properties
*/
_propertiesChanged(props, changedProps, prevProps) {
super._propertiesChanged(props, changedProps, prevProps);
const result = this._render(props);
if (result && this._root !== undefined) {
this._applyRender(result, this._root);
}
this._didRender(props, changedProps, prevProps);
if (this.__resolveRenderComplete) {
this.__resolveRenderComplete(true);
}
}
_flushProperties() {
this.__isChanging = true;
this.__isInvalid = false;
super._flushProperties();
this.__isChanging = false;
}
/**
* Override which warns when a user attempts to change a property during
* the rendering lifecycle. This is an anti-pattern and should be avoided.
* @param property {string}
* @param value {any}
* @param old {any}
*/
// tslint:disable-next-line no-any
_shouldPropertyChange(property, value, old) {
const change = super._shouldPropertyChange(property, value, old);
if (change && this.__isChanging) {
console.trace(`Setting properties in response to other properties changing ` +
`considered harmful. Setting '${property}' from ` +
`'${this._getProperty(property)}' to '${value}'.`);
}
return change;
}
/**
* Implement to describe the DOM which should be rendered in the element.
* Ideally, the implementation is a pure function using only props to describe
* the element template. The implementation must return a `lit-html`
* TemplateResult. By default this template is rendered into the element's
* shadowRoot. This can be customized by implementing `_createRoot`. This
* method must be implemented.
* @param {*} _props Current element properties
* @returns {TemplateResult} Must return a lit-html TemplateResult.
*/
_render(_props) {
throw new Error('_render() not implemented');
}
/**
* Renders the given lit-html template `result` into the given `node`.
* Implement to customize the way rendering is applied. This is should not
* typically be needed and is provided for advanced use cases.
* @param result {TemplateResult} `lit-html` template result to render
* @param node {Element|DocumentFragment} node into which to render
*/
_applyRender(result, node) {
render(result, node, this.localName);
}
/**
* Called after element DOM has been rendered. Implement to
* directly control rendered DOM. Typically this is not needed as `lit-html`
* can be used in the `_render` method to set properties, attributes, and
* event listeners. However, it is sometimes useful for calling methods on
* rendered elements, like calling `focus()` on an element to focus it.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
*/
_didRender(_props, _changedProps, _prevProps) { }
/**
* Call to request the element to asynchronously re-render regardless
* of whether or not any property changes are pending.
*/
requestRender() { this._invalidateProperties(); }
/**
* Override which provides tracking of invalidated state.
*/
_invalidateProperties() {
this.__isInvalid = true;
super._invalidateProperties();
}
/**
* Returns a promise which resolves after the element next renders.
* The promise resolves to `true` if the element rendered and `false` if the
* element did not render.
* This is useful when users (e.g. tests) need to react to the rendered state
* of the element after a change is made.
* This can also be useful in event handlers if it is desireable to wait
* to send an event until after rendering. If possible implement the
* `_didRender` method to directly respond to rendering within the
* rendering lifecycle.
*/
get renderComplete() {
if (!this.__renderComplete) {
this.__renderComplete = new Promise((resolve) => {
this.__resolveRenderComplete = (value) => {
this.__resolveRenderComplete = this.__renderComplete = null;
resolve(value);
};
});
if (!this.__isInvalid && this.__resolveRenderComplete) {
Promise.resolve().then(() => this.__resolveRenderComplete(false));
}
}
return this.__renderComplete;
}
}
//# sourceMappingURL=lit-element.js.map
{
"name": "@polymer/lit-element",
"version": "0.5.2",
"version": "0.6.0-dev.1",
"description": "Polymer based lit-html custom element",

@@ -25,4 +25,4 @@ "license": "BSD-3-Clause",

"@types/chai": "^4.0.1",
"@types/mocha": "^5.0.0",
"@webcomponents/webcomponentsjs": "^2.0.1",
"@types/mocha": "^5.2.4",
"@webcomponents/webcomponentsjs": "^2.0.2",
"chai": "^4.0.2",

@@ -32,11 +32,10 @@ "mocha": "^5.0.5",

"typedoc": "^0.8.0",
"typescript": "^2.7.2",
"typescript": "^3.0.1",
"uglify-es": "^3.3.9",
"wct-browser-legacy": "^1.0.1",
"web-component-tester": "^6.6.0"
"web-component-tester": "^6.7.1"
},
"typings": "lit-element.d.ts",
"dependencies": {
"@polymer/polymer": "^3.0.2",
"lit-html": "^0.10.0"
"lit-html": "dev"
},

@@ -43,0 +42,0 @@ "publishConfig": {

@@ -13,7 +13,42 @@ > ## 🛠 Status: In Development

element's [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM)
and [Polymer's](https://github.com/Polymer/polymer)
[PropertiesMixin](https://github.com/Polymer/polymer/blob/master/lib/mixins/properties-mixin.js)
to help manage element properties and attributes. LitElement reacts to changes in properties
and adds API help manage element properties and attributes. LitElement reacts to changes in properties
and renders declaratively using `lit-html`.
* **Setup properties:** LitElement supports observable properties which may trigger an
update when set. These properties can be written in a few ways:
* As class fields with the `@property()` [decorator](https://github.com/tc39/proposal-decorators#decorators),
if you're using a compiler that supports them, like TypeScript or Babel.
* With a static `properties` getter.
* By manually writing getters and setters. You can call `setProperty` or `invalidate`
to trigger an update. If you use `setProperty`, you should use `getProperty` in the getter.
Properties can be given an options argument which is an object that describes how to
process the property. This can be done either in the `@property({...})` decorator or in the
object returned from the `properties` getter, e.g. `static get properties { return { foo: {...} }`.
Property options include:
* `attribute`: Describes how and if the property becomes an observed attribute.
If the value is false, the property is not added to `observedAttributes`.
If true or absent, the lowercased property name is observed (e.g. `fooBar` becomes `foobar`).
If a string, the string value is observed (e.g `attribute: 'foo-bar'`).
* `type`: Describes how to serialize and deserialize the attribute to/from a property.
If this value is a function, it is used to deserialize the attribute value
a the property value. If it's an object, it can have keys for `fromAttribute` and
`toAttribute` where `fromAttribute` is the deserialize function and `toAttribute`
is a serialize function used to set the property to an attribute. If no `toAttribute`
function is provided and `reflect` is set to true, the property value is set
directly to the attribute.
* `reflect`: Describes if the property should reflect to an attribute.
If true, when the property is set, the attribute is set using the
attribute name determined according to the rules for the `attribute`
propety option and the value of the property serialized using the rules from
the `type` property option.
* `shouldInvalidate`: Describes if setting a property should trigger
invalidation and updating. This function takes the `newValue` and `oldValue` and
returns true if invalidation should occur. If not present, a strict identity
check is used. This is useful if a property should be considered dirty only
if some condition is met, like if a key of an object value changes.
* **React to changes:** LitElement reacts to changes in properties and attributes by

@@ -72,6 +107,6 @@ asynchronously rendering, ensuring changes are batched. This reduces overhead

1. Create a class that extends `LitElement`.
1. Implement a static `properties` getter that returns the element's properties
(which automatically become observed attributes).
1. Then implement a `_render(props)` method and use the element's
current properties (props) to return a `lit-html` template result to render
1. Use a `@property` decorator to create a property (or implement a static `properties`
getter that returns the element's properties). (which automatically become observed attributes).
1. Then implement a `render()` method and use the element's
current properties to return a `lit-html` template result to render
into the element. This is the only method that must be implemented by subclasses.

@@ -86,7 +121,8 @@

static get properties() { return { mood: String }}
@property({type: String})
mood = 'happy';
_render({mood}) {
render() {
return html`<style> .mood { color: green; } </style>
Web Components are <span class="mood">${mood}</span>!`;
Web Components are <span class="mood">${this.mood}</span>!`;
}

@@ -105,10 +141,5 @@

See the [source](https://github.com/PolymerLabs/lit-element/blob/master/src/lit-element.ts#L90)
for detailed API info, here are some highlights. Note, the leading underscore
is used to indicate that these methods are
[protected](https://en.wikipedia.org/wiki/Class_(computer_programming)#Member_accessibility);
they are not private and can and should be implemented by subclasses.
These methods generally are called as part of the rendering lifecycle and should
not be called in user code unless otherwise indicated.
for detailed API info, here are some highlights.
* `_createRoot()`: Implement to customize where the
* `createRenderRoot()` (protected): Implement to customize where the
element's template is rendered by returning an element into which to

@@ -118,25 +149,56 @@ render. By default this creates a shadowRoot for the element.

* `_firstRendered()`: Called after the element DOM is rendered for the first time.
* `_shouldRender(props, changedProps, prevProps)`: Implement to control if rendering
should occur when property values change or `invalidate` is called.
* `shouldUpdate(changedProps)` (protected): Implement to control if updating and rendering
should occur when property values change or `invalidate` is called. The `changedProps`
argument is an object with keys for the changed properties pointing to their previous values.
By default, this method always returns true, but this can be customized as
an optimization to avoid rendering work when changes occur which should not be rendered.
an optimization to avoid updating work when changes occur, which should not be rendered.
* `_render(props)`: Implement to describe the element's DOM using `lit-html`. Ideally,
the `_render` implementation is a pure function using only `props` to describe
the element template. This is the only method that must be implemented by subclasses.
* `update()` (protected): This method calls `render()` and then uses `lit-html` to
render the template DOM. Override to customize how the element renders DOM. Note,
during `update()` setting properties does not trigger `invalidate()`, allowing
property values to be computed and validated.
* `_didRender(props, changedProps, prevProps)`: Called after element DOM has been rendered.
Implement to directly control rendered DOM. Typically this is not needed as `lit-html`
can be used in the `_render` method to set properties, attributes, and
event listeners. However, it is sometimes useful for calling methods on
rendered elements, for example focusing an input:
`this.shadowRoot.querySelector('input').focus()`.
* `render()` (protected): Implement to describe the element's DOM using `lit-html`. Ideally,
the `render` implementation is a pure function using only the element's current properties
to describe the element template. This is the only method that must be implemented by subclasses.
Note, since `render()` is called by `update()` setting properties does not trigger
`invalidate()`, allowing property values to be computed and validated.
* `renderComplete`: Returns a promise which resolves after the element next renders.
* `finishUpdate(changedProps): Promise?` (protected): Called after element DOM has been updated and
before the `updateComplete` promise is resolved. Implement to directly control rendered DOM.
Typically this is not needed as `lit-html` can be used in the `render` method
to set properties, attributes, and event listeners. However, it is sometimes useful
for calling methods on rendered elements, for example focusing an input:
`this.shadowRoot.querySelector('input').focus()`. The `changedProps` argument is an object
with keys for the changed properties pointing to their previous values. If this function
returns a `Promise`, it will be *awaited* before resolving the `updateComplete` promise.
Setting properties in `finishUpdate()` does trigger `invalidate()` and blocks
the `updateComplete` promise.
* `_requestRender`: Call to request the element to asynchronously re-render regardless
* `updateComplete`: Returns a promise which resolves after the element next renders.
* `invalidate`: Call to request the element to asynchronously update regardless
of whether or not any property changes are pending.
## Update Lifecycle
* When the element is first connected or a property is set (e.g. `element.foo = 5`)
and the property's `shouldInvalidate(value, oldValue)` returns true. Then
* `invalidate()` tries to update the element after waiting a [microtask](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) (at the end
of the event loop, before the next paint). Then
* `shouldUpdate(changedProps)` is called and if this returns true which it
does by default:
* `update(changedProps)` is called to update the element.
Note, setting properties inside `update()` will set their values but
will *not* trigger `invalidate()`. This calls
* `render()` which should return a `lit-html` TemplateResult
(e.g. <code>html\`Hello ${world}\`</code>)
* `finishUpdate(changedProps)` is then called to do post update/render tasks.
Note, setting properties here will trigger `invalidate()` and block
the `updateComplete` promise.
* `updateComplete` promise is resolved only if the element is
not in an invalid state.
* Any code awaiting the element's `updateComplete` promise runs and observes
the element in the updated state.
## Bigger Example

@@ -150,15 +212,13 @@

// Public property API that triggers re-render (synced with attributes)
static get properties() {
return {
foo: String,
whales: Number
}
}
@property()
foo = 'foo';
@property({type: Number})
whales = 5;
constructor() {
super();
this.foo = 'foo';
this.addEventListener('click', async (e) => {
this.whales++;
await this.renderComplete;
await this.updateComplete;
this.dispatchEvent(new CustomEvent('whales', {detail: {whales: this.whales}}))

@@ -169,3 +229,3 @@ });

// Render method should return a `TemplateResult` using the provided lit-html `html` tag function
_render({foo, whales}) {
render() {
return html`

@@ -180,4 +240,4 @@ <style>

</style>
<h4>Foo: ${foo}</h4>
<div>whales: ${'🐳'.repeat(whales)}</div>
<h4>Foo: ${this.foo}</h4>
<div>whales: ${'🐳'.repeat(this.whales)}</div>
<slot></slot>

@@ -201,3 +261,2 @@ `;

## Known Issues
* When the Shady DOM polyfill and ShadyCSS shim are used, styles may be [out of order](https://github.com/PolymerLabs/lit-element/issues/34).
* Rendering is not supported in IE11 due to a lit-html [issue](https://github.com/Polymer/lit-html/issues/210).

@@ -14,284 +14,32 @@ /**

*/
import {
PropertiesChangedConstructor
} from '@polymer/polymer/lib/mixins/properties-changed.js';
import {PropertiesMixin} from '@polymer/polymer/lib/mixins/properties-mixin.js';
import {
PropertiesMixinConstructor
} from '@polymer/polymer/lib/mixins/properties-mixin.js';
import {camelToDashCase} from '@polymer/polymer/lib/utils/case-map.js';
import {render} from 'lit-html/lib/shady-render.js';
import {TemplateResult} from 'lit-html/lit-html.js';
import {render} from 'lit-html/lib/shady-render';
import {TemplateResult} from 'lit-html';
import {UpdatingElement, PropertyValues} from './lib/updating-element.js';
export {
PropertiesChangedConstructor
} from '@polymer/polymer/lib/mixins/properties-changed.js';
export {
PropertiesMixinConstructor
} from '@polymer/polymer/lib/mixins/properties-mixin.js';
export {html, svg} from 'lit-html/lib/lit-extended.js';
export {property, identity, BooleanAttribute, PropertyDeclarations, PropertyDeclaration, PropertyValues} from './lib/updating-element.js';
export {html, svg} from 'lit-html/lib/lit-extended';
// This is a hack to get tsc to not complain about unused interfaces and
// still generate the type declarations properly
export type __unused = PropertiesChangedConstructor&PropertiesMixinConstructor;
/**
* Renders attributes to the given element based on the `attrInfo` object where
* boolean values are added/removed as attributes.
* @param element Element on which to set attributes.
* @param attrInfo Object describing attributes.
*/
export function renderAttributes(
element: HTMLElement, attrInfo: {[name: string]: string|boolean|number}) {
for (const a in attrInfo) {
const v = attrInfo[a] === true ? '' : attrInfo[a];
if (v || v === '' || v === 0) {
if (element.getAttribute(a) !== v) {
element.setAttribute(a, String(v));
}
} else if (element.hasAttribute(a)) {
element.removeAttribute(a);
}
}
}
export abstract class LitElement extends UpdatingElement {
/**
* Returns a string of css class names formed by taking the properties
* in the `classInfo` object and appending the property name to the string of
* class names if the property value is truthy.
* @param classInfo
*/
export function classString(
classInfo: {[name: string]: string|boolean|number}) {
const o = [];
for (const name in classInfo) {
const v = classInfo[name];
if (v) {
o.push(name);
}
}
return o.join(' ');
}
/**
* Returns a css style string formed by taking the properties in the `styleInfo`
* object and appending the property name (dash-cased) colon the
* property value. Properties are separated by a semi-colon.
* @param styleInfo
*/
export function styleString(
styleInfo: {[name: string]: string|boolean|number}) {
const o = [];
for (const name in styleInfo) {
const v = styleInfo[name];
if (v || v === 0) {
o.push(`${camelToDashCase(name)}: ${v}`);
}
}
return o.join('; ');
}
export class LitElement extends PropertiesMixin
(HTMLElement) {
private __renderComplete: Promise<boolean>|null = null;
private __resolveRenderComplete: Function|null = null;
private __isInvalid: Boolean = false;
private __isChanging: Boolean = false;
private _root?: Element|DocumentFragment;
/**
* Override which sets up element rendering by calling* `_createRoot`
* and `_firstRendered`.
* Override which performs element rendering by calling the `render` method.
* Override to perform tasks before and/or after updating.
*/
ready() {
this._root = this._createRoot();
super.ready();
this._firstRendered();
}
connectedCallback() {
if (window.ShadyCSS && this._root) {
window.ShadyCSS.styleElement(this);
protected update(_props: PropertyValues) {
super.update(_props);
if (typeof this.render === 'function') {
render(this.render(), this.renderRoot!, this.localName!);
} else {
throw new Error('render() not implemented');
}
super.connectedCallback();
}
/**
* Called after the element DOM is rendered for the first time.
* Implement to perform tasks after first rendering like capturing a
* reference to a static node which must be directly manipulated.
* This should not be commonly needed. For tasks which should be performed
* before first render, use the element constructor.
*/
_firstRendered() {}
/**
* Implement to customize where the element's template is rendered by
* returning an element into which to render. By default this creates
* a shadowRoot for the element. To render into the element's childNodes,
* return `this`.
* @returns {Element|DocumentFragment} Returns a node into which to render.
*/
protected _createRoot(): Element|DocumentFragment {
return this.attachShadow({mode : 'open'});
}
/**
* Override which returns the value of `_shouldRender` which users
* should implement to control rendering. If this method returns false,
* _propertiesChanged will not be called and no rendering will occur even
* if property values change or `requestRender` is called.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
* @returns {boolean} Default implementation always returns true.
*/
_shouldPropertiesChange(_props: object, _changedProps: object,
_prevProps: object): boolean {
const shouldRender = this._shouldRender(_props, _changedProps, _prevProps);
if (!shouldRender && this.__resolveRenderComplete) {
this.__resolveRenderComplete(false);
}
return shouldRender;
}
/**
* Implement to control if rendering should occur when property values
* change or `requestRender` is called. By default, this method always
* returns true, but this can be customized as an optimization to avoid
* rendering work when changes occur which should not be rendered.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
* @returns {boolean} Default implementation always returns true.
*/
protected _shouldRender(_props: object, _changedProps: object,
_prevProps: object): boolean {
return true;
}
/**
* Override which performs element rendering by calling
* `_render`, `_applyRender`, and finally `_didRender`.
* @param props Current element properties
* @param changedProps Changing element properties
* @param prevProps Previous element properties
*/
_propertiesChanged(props: object, changedProps: object, prevProps: object) {
super._propertiesChanged(props, changedProps, prevProps);
const result = this._render(props);
if (result && this._root !== undefined) {
this._applyRender(result, this._root!);
}
this._didRender(props, changedProps, prevProps);
if (this.__resolveRenderComplete) {
this.__resolveRenderComplete(true);
}
}
_flushProperties() {
this.__isChanging = true;
this.__isInvalid = false;
super._flushProperties();
this.__isChanging = false;
}
/**
* Override which warns when a user attempts to change a property during
* the rendering lifecycle. This is an anti-pattern and should be avoided.
* @param property {string}
* @param value {any}
* @param old {any}
*/
// tslint:disable-next-line no-any
_shouldPropertyChange(property: string, value: any, old: any) {
const change = super._shouldPropertyChange(property, value, old);
if (change && this.__isChanging) {
console.trace(
`Setting properties in response to other properties changing ` +
`considered harmful. Setting '${property}' from ` +
`'${this._getProperty(property)}' to '${value}'.`);
}
return change;
}
/**
* Implement to describe the DOM which should be rendered in the element.
* Ideally, the implementation is a pure function using only props to describe
* the element template. The implementation must return a `lit-html`
* TemplateResult. By default this template is rendered into the element's
* shadowRoot. This can be customized by implementing `_createRoot`. This
* method must be implemented.
* @param {*} _props Current element properties
Invoked on each update to perform rendering tasks. This method must return a
lit-html TemplateResult.
* @returns {TemplateResult} Must return a lit-html TemplateResult.
*/
protected _render(_props: object): TemplateResult {
throw new Error('_render() not implemented');
}
protected abstract render(): TemplateResult;
/**
* Renders the given lit-html template `result` into the given `node`.
* Implement to customize the way rendering is applied. This is should not
* typically be needed and is provided for advanced use cases.
* @param result {TemplateResult} `lit-html` template result to render
* @param node {Element|DocumentFragment} node into which to render
*/
protected _applyRender(result: TemplateResult,
node: Element|DocumentFragment) {
render(result, node, this.localName!);
}
/**
* Called after element DOM has been rendered. Implement to
* directly control rendered DOM. Typically this is not needed as `lit-html`
* can be used in the `_render` method to set properties, attributes, and
* event listeners. However, it is sometimes useful for calling methods on
* rendered elements, like calling `focus()` on an element to focus it.
* @param _props Current element properties
* @param _changedProps Changing element properties
* @param _prevProps Previous element properties
*/
protected _didRender(_props: object, _changedProps: object,
_prevProps: object) {}
/**
* Call to request the element to asynchronously re-render regardless
* of whether or not any property changes are pending.
*/
requestRender() { this._invalidateProperties(); }
/**
* Override which provides tracking of invalidated state.
*/
_invalidateProperties() {
this.__isInvalid = true;
super._invalidateProperties();
}
/**
* Returns a promise which resolves after the element next renders.
* The promise resolves to `true` if the element rendered and `false` if the
* element did not render.
* This is useful when users (e.g. tests) need to react to the rendered state
* of the element after a change is made.
* This can also be useful in event handlers if it is desireable to wait
* to send an event until after rendering. If possible implement the
* `_didRender` method to directly respond to rendering within the
* rendering lifecycle.
*/
get renderComplete() {
if (!this.__renderComplete) {
this.__renderComplete = new Promise((resolve) => {
this.__resolveRenderComplete = (value: boolean) => {
this.__resolveRenderComplete = this.__renderComplete = null;
resolve(value);
};
});
if (!this.__isInvalid && this.__resolveRenderComplete) {
Promise.resolve().then(() => this.__resolveRenderComplete!(false));
}
}
return this.__renderComplete;
}
}
}

@@ -21,2 +21,4 @@ /**

import {generateElementName, nextFrame, getComputedStyleValue} from './test-helpers.js';
declare global {

@@ -44,5 +46,6 @@ interface Window {

test('content shadowRoot is styled', () => {
customElements.define('s-1', class extends LitElement {
_render() { return html`
test('content shadowRoot is styled', async () => {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`
<style>

@@ -56,9 +59,10 @@ div {

});
const el = document.createElement('s-1');
const el = document.createElement(name);
container.appendChild(el);
await (el as LitElement).updateComplete;
const div = el.shadowRoot!.querySelector('div');
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '2px');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '2px');
});
test('shared styling rendered into shadowRoot is styled', () => {
test('shared styling rendered into shadowRoot is styled', async () => {
const style = html`<style>

@@ -69,4 +73,5 @@ div {

</style>`;
customElements.define('s-2', class extends LitElement {
_render() { return html`
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`
<style>

@@ -81,11 +86,13 @@ div {

});
const el = document.createElement('s-2');
const el = document.createElement(name);
container.appendChild(el);
await (el as LitElement).updateComplete;
const div = el.shadowRoot!.querySelector('div');
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '4px');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '4px');
});
test('custom properties render', () => {
customElements.define('s-3', class extends LitElement {
_render() { return html`
test('custom properties render', async () => {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`
<style>

@@ -102,11 +109,12 @@ :host {

});
const el = document.createElement('s-3');
const el = document.createElement(name);
container.appendChild(el);
await (el as LitElement).updateComplete;
const div = el.shadowRoot!.querySelector('div');
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '8px');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '8px');
});
test('custom properties flow to nested elements', () => {
customElements.define('s-4-inner', class extends LitElement {
_render() { return html`
test('custom properties flow to nested elements', async () => {
customElements.define('x-inner', class extends LitElement {
render() { return html`
<style>

@@ -120,21 +128,23 @@ div {

});
customElements.define('s-4', class extends LitElement {
_render() { return html`
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`
<style>
s-4-inner {
x-inner {
--border: 8px solid red;
}
</style>
<s-4-inner></s-4-inner>`;
<x-inner></x-inner>`;
}
});
const el = document.createElement('s-4');
const el = document.createElement(name);
container.appendChild(el);
const div = el.shadowRoot!.querySelector('s-4-inner')!.shadowRoot!.querySelector('div');
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '8px');
await nextFrame();
const div = el.shadowRoot!.querySelector('x-inner')!.shadowRoot!.querySelector('div');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '8px');
});
test('elements with custom properties can move between elements', (done) => {
customElements.define('s-5-inner', class extends LitElement {
_render() { return html`
test('elements with custom properties can move between elements', async () => {
customElements.define('x-inner1', class extends LitElement {
render() { return html`
<style>

@@ -148,16 +158,18 @@ div {

});
customElements.define('s-5', class extends LitElement {
_render() { return html`
const name1 = generateElementName();
customElements.define(name1, class extends LitElement {
render() { return html`
<style>
s-5-inner {
x-inner1 {
--border: 2px solid red;
}
</style>
<s-5-inner></s-5-inner>`;
<x-inner1></x-inner1>`;
}
});
customElements.define('s-6', class extends LitElement {
_render() { return html`
const name2 = generateElementName();
customElements.define(name2, class extends LitElement {
render() { return html`
<style>
s-5-inner {
x-inner1 {
--border: 8px solid red;

@@ -168,19 +180,19 @@ }

});
const el = document.createElement('s-5');
const el2 = document.createElement('s-6');
const el = document.createElement(name1);
const el2 = document.createElement(name2);
container.appendChild(el);
container.appendChild(el2);
const inner = el.shadowRoot!.querySelector('s-5-inner');
const div = inner!.shadowRoot!.querySelector('div');
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '2px');
let div: Element|null;
await nextFrame();
const inner = el.shadowRoot!.querySelector('x-inner1');
div = inner!.shadowRoot!.querySelector('div');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '2px');
el2!.shadowRoot!.appendChild(inner!);
requestAnimationFrame(() => {
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '8px');
done();
});
await nextFrame();
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '8px');
});
test('@apply renders in nested elements', () => {
customElements.define('s-7-inner', class extends LitElement {
_render() { return html`
test('@apply renders in nested elements', async () => {
customElements.define('x-inner2', class extends LitElement {
render() { return html`
<style>

@@ -194,6 +206,7 @@ div {

});
customElements.define('s-7', class extends LitElement {
_render() { return html`
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`
<style>
s-7-inner {
x-inner2 {
--bag: {

@@ -204,9 +217,10 @@ border: 10px solid red;

</style>
<s-7-inner></s-7-inner>`;
<x-inner2></x-inner2>`;
}
});
const el = document.createElement('s-7');
const el = document.createElement(name);
container.appendChild(el);
const div = el.shadowRoot!.querySelector('s-7-inner')!.shadowRoot!.querySelector('div');
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '10px');
await nextFrame();
const div = el.shadowRoot!.querySelector('x-inner2')!.shadowRoot!.querySelector('div');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '10px');
});

@@ -237,4 +251,5 @@

let border = `6px solid blue`;
customElements.define('shady-1', class extends LitElement {
_render() { return html`
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`
<style>

@@ -248,12 +263,13 @@ div {

});
const el = document.createElement('shady-1') as LitElement;
const el = document.createElement(name) as LitElement;
container.appendChild(el);
await el.updateComplete;
const div = el.shadowRoot!.querySelector('div');
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '6px');
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '6px');
border = `4px solid orange`;
el.requestRender();
await el.renderComplete;
assert.equal(getComputedStyle(div!).getPropertyValue('border-top-width').trim(), '6px');
el.invalidate();
await el.updateComplete;
assert.equal(getComputedStyleValue(div!, 'border-top-width').trim(), '6px');
});
});

@@ -15,14 +15,6 @@ /**

import {TemplateResult} from 'lit-html/lit-html.js';
import {html, LitElement, BooleanAttribute, PropertyDeclarations, PropertyValues} from '../lit-element.js';
import {
classString,
html,
LitElement,
renderAttributes,
styleString,
} from '../lit-element.js';
import {stripExpressionDelimeters, generateElementName} from './test-helpers.js';
import {stripExpressionDelimeters} from './test-helpers.js';
const assert = chai.assert;

@@ -44,24 +36,115 @@

test('renders initial content into shadowRoot', () => {
test('renders initial content into shadowRoot', async () => {
const rendered = `hello world`;
customElements.define('x-1', class extends LitElement {
_render() { return html`${rendered}`; }
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`${rendered}`; }
});
const el = document.createElement('x-1');
const el = document.createElement(name);
container.appendChild(el);
assert.ok(el.shadowRoot);
await new Promise((resolve) => {
setTimeout(() => {
assert.ok(el.shadowRoot);
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
rendered);
resolve();
});
});
});
test('invalidate waits until update/rendering', async () => {
class E extends LitElement {
updated = 0;
render() { return html`${++this.updated}`; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.invalidate();
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
rendered);
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'1');
await el.invalidate();
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'2');
await el.invalidate();
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'3');
});
test('can set render target to light dom', () => {
test('updateComplete waits for invalidate but does not trigger invalidation, async', async () => {
class E extends LitElement {
updated = 0;
render() { return html`${++this.updated}`; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'1');
await el.updateComplete;
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'1');
el.invalidate();
await el.updateComplete;
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'2');
await el.updateComplete;
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'2');
});
test('shouldUpdate controls update/rendering',
async () => {
class E extends LitElement {
needsUpdate = true;
updated = 0;
shouldUpdate() { return this.needsUpdate; }
render() { return html`${++this.updated}`; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'1');
el.needsUpdate = false;
await el.invalidate();
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'1');
el.needsUpdate = true;
await el.invalidate();
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'2');
await el.invalidate();
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'3');
});
test('can set render target to light dom', async () => {
const rendered = `hello world`;
customElements.define('x-1a', class extends LitElement {
_render() { return html`${rendered}`; }
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html`${rendered}`; }
_createRoot() { return this; }
createRenderRoot() { return this; }
});
const el = document.createElement('x-1a');
const el = document.createElement(name);
container.appendChild(el);
await (el as LitElement).updateComplete;
assert.notOk(el.shadowRoot);

@@ -71,104 +154,491 @@ assert.equal(stripExpressionDelimeters(el.innerHTML), rendered);

test('renders when created via constructor', () => {
test('renders when created via constructor', async () => {
const rendered = `hello world`;
class E extends LitElement {
_render() { return html`${rendered}`; }
render() { return html`${rendered}`; }
}
customElements.define('x-2', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.ok(el.shadowRoot);
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
rendered);
});
test('renders changes when properties change', (done) => {
test('property options', async() => {
const shouldInvalidate = (value: any, old: any) => old === undefined || value > old;
const fromAttribute = (value: any) => parseInt(value);
const toAttribute = (value: any) => `${value}-attr`;
class E extends LitElement {
static get properties() { return {foo : String}; }
static get properties() {
return {
noAttr: {attribute: false},
atTr: {attribute: true},
customAttr: {attribute: 'custom', reflect: true},
shouldInvalidate: {shouldInvalidate},
fromAttribute: {type: fromAttribute},
toAttribute: {reflect: true, type: {toAttribute}},
all: {attribute: 'all-attr', shouldInvalidate, type: {fromAttribute, toAttribute}, reflect: true},
};
}
noAttr = 'noAttr';
atTr = 'attr';
customAttr = 'customAttr';
shouldInvalidate = 10;
fromAttribute = 1;
toAttribute = 1;
all = 10;
render() { return html``; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr');
assert.equal(el.customAttr, 'customAttr');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 1);
assert.equal(el.toAttribute, 1);
assert.equal(el.getAttribute('toattribute'), '1-attr');
assert.equal(el.all, 10);
assert.equal(el.getAttribute('all-attr'), '10-attr');
el.setAttribute('noattr', 'noAttr2');
el.setAttribute('attr', 'attr2');
el.setAttribute('custom', 'customAttr2');
el.shouldInvalidate = 5;
el.setAttribute('fromattribute', '2attr');
el.toAttribute = 2;
el.all = 5;
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr2');
assert.equal(el.customAttr, 'customAttr2');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 2);
assert.equal(el.toAttribute, 2);
assert.equal(el.getAttribute('toattribute'), '2-attr');
assert.equal(el.all, 10);
el.shouldInvalidate = 15;
el.all = 15;
await el.updateComplete;
assert.equal(el.shouldInvalidate, 15);
assert.equal(el.all, 15);
assert.equal(el.getAttribute('all-attr'), '15-attr');
el.setAttribute('all-attr', '16-attr');
await el.updateComplete;
assert.equal(el.getAttribute('all-attr'), '16-attr');
assert.equal(el.all, 16);
});
test('attributes deserialize from html', async() => {
const shouldInvalidate = (value: any, old: any) => old === undefined || value > old;
const fromAttribute = (value: any) => parseInt(value);
const toAttributeOnly = (value: any) => typeof value === 'string' && value.indexOf(`-attr`) > 0 ? value : `${value}-attr`;
const toAttribute = (value: any) => `${value}-attr`;
class E extends LitElement {
static get properties() {
return {
noAttr: {attribute: false},
atTr: {attribute: true},
customAttr: {attribute: 'custom', reflect: true},
shouldInvalidate: {shouldInvalidate},
fromAttribute: {type: fromAttribute},
toAttribute: {reflect: true, type: {toAttribute: toAttributeOnly}},
all: {attribute: 'all-attr', shouldInvalidate, type: {fromAttribute, toAttribute}, reflect: true},
};
}
noAttr = 'noAttr';
atTr = 'attr';
customAttr = 'customAttr';
shouldInvalidate = 10;
fromAttribute = 1;
toAttribute: string|number = 1;
all = 10;
render() { return html``; }
}
const name = generateElementName();
customElements.define(name, E);
container.innerHTML = `<${name}
noattr="1"
attr="2"
custom="3"
shouldInvalidate="5"
fromAttribute="6-attr"
toAttribute="7"
all-attr="11-attr"></${name}>`;
const el = container.firstChild as E;
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.getAttribute('noattr'), '1');
assert.equal(el.atTr, '2');
assert.equal(el.customAttr, '3');
assert.equal(el.getAttribute('custom'), '3');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.getAttribute('shouldinvalidate'), '5');
assert.equal(el.fromAttribute, 6);
assert.equal(el.toAttribute, '7');
assert.equal(el.getAttribute('toattribute'), '7-attr');
assert.equal(el.all, 11);
assert.equal(el.getAttribute('all-attr'), '11-attr');
});
if (Object.getOwnPropertySymbols) {
test('properties defined using symbols', async() => {
const zug = Symbol();
class E extends LitElement {
static get properties() {
return {
foo: {},
[zug]: {}
};
}
updated = 0;
foo = 5;
[zug] = 6;
render() {
return html``;
}
update() {
this.updated++;
}
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.updated, 1);
assert.equal(el.foo, 5);
assert.equal(el[zug], 6);
el.foo = 55;
await el.updateComplete;
assert.equal(el.updated, 2);
assert.equal(el.foo, 55);
assert.equal(el[zug], 6);
el[zug] = 66;
await el.updateComplete;
assert.equal(el.updated, 3);
assert.equal(el.foo, 55);
assert.equal(el[zug], 66);
});
}
test('property options compose when subclassing', async() => {
const shouldInvalidate = (value: any, old: any) => old === undefined || value > old;
const fromAttribute = (value: any) => parseInt(value);
const toAttribute = (value: any) => `${value}-attr`;
class E extends LitElement {
static get properties(): PropertyDeclarations {
return {
noAttr: {attribute: false},
atTr: {attribute: true},
customAttr: {},
shouldInvalidate: {},
};
}
noAttr = 'noAttr';
atTr = 'attr';
customAttr = 'customAttr';
shouldInvalidate = 10;
fromAttribute = 1;
toAttribute = 1;
all = 10;
render() { return html``; }
}
customElements.define(generateElementName(), E);
class F extends E {
static get properties(): PropertyDeclarations {
return {
customAttr: {attribute: 'custom', reflect: true},
shouldInvalidate: {shouldInvalidate},
fromAttribute: {},
toAttribute: {},
};
}
noAttr = 'noAttr';
atTr = 'attr';
customAttr = 'customAttr';
shouldInvalidate = 10;
fromAttribute = 1;
toAttribute = 1;
all = 10;
render() { return html``; }
}
class G extends F {
static get properties(): PropertyDeclarations {
return {
fromAttribute: {type: fromAttribute},
toAttribute: {reflect: true, type: {toAttribute}},
all: {attribute: 'all-attr', shouldInvalidate, type: {fromAttribute, toAttribute}, reflect: true},
};
}
noAttr = 'noAttr';
atTr = 'attr';
customAttr = 'customAttr';
shouldInvalidate = 10;
fromAttribute = 1;
toAttribute = 1;
all = 10;
render() { return html``; }
}
customElements.define(generateElementName(), G);
const el = new G();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr');
assert.equal(el.customAttr, 'customAttr');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 1);
assert.equal(el.toAttribute, 1);
assert.equal(el.getAttribute('toattribute'), '1-attr');
assert.equal(el.all, 10);
assert.equal(el.getAttribute('all-attr'), '10-attr');
el.setAttribute('noattr', 'noAttr2');
el.setAttribute('attr', 'attr2');
el.setAttribute('custom', 'customAttr2');
el.shouldInvalidate = 5;
el.setAttribute('fromattribute', '2attr');
el.toAttribute = 2;
el.all = 5;
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr2');
assert.equal(el.customAttr, 'customAttr2');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 2);
assert.equal(el.toAttribute, 2);
assert.equal(el.getAttribute('toattribute'), '2-attr');
assert.equal(el.all, 10);
el.shouldInvalidate = 15;
el.all = 15;
await el.updateComplete;
assert.equal(el.shouldInvalidate, 15);
assert.equal(el.all, 15);
assert.equal(el.getAttribute('all-attr'), '15-attr');
el.setAttribute('all-attr', '16-attr');
await el.updateComplete;
assert.equal(el.getAttribute('all-attr'), '16-attr');
assert.equal(el.all, 16);
});
test('superclass properties not affected by subclass', async() => {
class E extends LitElement {
static get properties(): PropertyDeclarations {
return {
foo: {attribute: 'zug', reflect: true},
bar: {reflect: true}
};
}
foo = 5;
bar = 'bar';
render() { return html``; }
}
customElements.define(generateElementName(), E);
class F extends E {
static get properties(): PropertyDeclarations {
return {
foo: {attribute: false},
nug: {}
};
}
foo = 6;
bar = 'subbar';
nug = 5;
render() { return html``; }
}
customElements.define(generateElementName(), F);
const el = new E();
const sub = new F();
container.appendChild(el);
await el.updateComplete;
container.appendChild(sub);
await sub.updateComplete;
assert.equal(el.foo, 5);
assert.equal(el.getAttribute('zug'), '5');
assert.isFalse(el.hasAttribute('foo'));
assert.equal(el.bar, 'bar');
assert.equal(el.getAttribute('bar'), 'bar');
assert.isUndefined((el as any).nug);
assert.equal(sub.foo, 6);
assert.isFalse(sub.hasAttribute('zug'));
assert.isFalse(sub.hasAttribute('foo'));
assert.equal(sub.bar, 'subbar');
assert.equal(sub.getAttribute('bar'), 'subbar');
assert.equal(sub.nug, 5);
});
test('Attributes reflect with type.toAttribute and BooleanAttribute', async () => {
class E extends LitElement {
static get properties() {
return {
foo: {type: Number, reflect: true},
bar: {type: BooleanAttribute, reflect: true}
};
}
foo = 0;
bar = true;
render() { return html``; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.getAttribute('foo'), '0');
assert.equal(el.getAttribute('bar'), '');
el.foo = 5;
el.bar = false;
await el.updateComplete;
assert.equal(el.getAttribute('foo'), '5');
assert.equal(el.hasAttribute('bar'), false);
});
test('updates/renders when properties change', async () => {
class E extends LitElement {
static get properties() { return { foo: {}}; }
foo = 'one';
_render(props: {foo: string}) { return html`${props.foo}`; }
render() { return html`${this.foo}`; }
}
customElements.define('x-3', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.ok(el.shadowRoot);
await el.updateComplete;
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'one');
el.foo = 'changed';
requestAnimationFrame(() => {
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'changed');
done();
});
await el.updateComplete;
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'changed');
});
test('renders changes when attributes change', (done) => {
test('updates/renders when properties and attributes change', async() => {
class E extends LitElement {
static get properties() { return {foo : String}; }
static get properties() {
return {
value: {},
attrValue: {}
};
}
value = '1';
attrValue = 'attr';
updatedValue = '';
updatedAttrValue = '';
render() { return html``; }
update(props: PropertyValues) {
super.update(props);
this.updatedValue = this.value;
this.updatedAttrValue = this.attrValue;
}
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.ok(el.shadowRoot);
await el.updateComplete;
assert.equal(el.updatedValue, '1');
assert.equal(el.updatedAttrValue, 'attr');
el.value = '2';
await el.updateComplete;
assert.equal(el.updatedValue, '2');
assert.equal(el.updatedAttrValue, 'attr');
el.attrValue = 'attr2';
await el.updateComplete;
assert.equal(el.updatedValue, '2');
assert.equal(el.updatedAttrValue, 'attr2');
el.setAttribute('attrvalue', 'attr3');
await el.updateComplete;
assert.equal(el.updatedValue, '2');
assert.equal(el.updatedAttrValue, 'attr3');
el.value = '3';
el.setAttribute('attrvalue', 'attr4');
await el.updateComplete;
assert.equal(el.updatedValue, '3');
assert.equal(el.updatedAttrValue, 'attr4');
});
test('updates/renders changes when attributes change', async () => {
class E extends LitElement {
static get properties() {
return {foo: {}};
}
foo = 'one';
_render(props: {foo: string}) { return html`${props.foo}`; }
render() { return html`${this.foo}`; }
}
customElements.define('x-4', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.ok(el.shadowRoot);
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'one');
el.setAttribute('foo', 'changed');
requestAnimationFrame(() => {
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'changed');
done();
});
await el.updateComplete;
assert.equal(
stripExpressionDelimeters(el.shadowRoot!.innerHTML),
'changed');
});
test('_firstRendered call after first render and not subsequent renders',
async () => {
class E extends LitElement {
static get properties() { return {foo : String}; }
foo = 'one';
firstRenderedCount = 0;
domAtFirstRendered = '';
_firstRendered() {
this.firstRenderedCount++;
this.domAtFirstRendered =
stripExpressionDelimeters(this.shadowRoot!.innerHTML);
}
_render(props: {foo: string}) { return html`${props.foo}`; }
}
customElements.define('x-5', E);
const el = new E();
container.appendChild(el);
assert.equal(el.firstRenderedCount, 1);
assert.ok(el.shadowRoot);
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
el.domAtFirstRendered);
assert.equal(el.foo, el.domAtFirstRendered);
el.foo = 'two';
await el.renderComplete;
assert.equal(el.firstRenderedCount, 1);
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
el.foo);
assert.notEqual(el.foo, el.domAtFirstRendered);
});
test('User defined accessor can trigger rendering', async () => {
test('User defined accessor using setProperty/getProperty can trigger update/render', async () => {
class E extends LitElement {
__bar?: number;
static get properties() { return {foo : Number, bar : Number}; }
static get properties() { return {foo: {}, bar: {}}; }

@@ -178,19 +648,19 @@ info: string[] = [];

get bar() { return this._getProperty('bar'); }
get bar() { return this.getProperty('bar'); }
set bar(value) {
this.__bar = value;
this._setProperty('bar', value);
this.__bar = Number(value);
this.setProperty('bar', value);
}
_render(props: {foo: string, bar: number}) {
render() {
this.info.push('render');
return html`${props.foo}${props.bar}`;
return html`${this.foo}${this.bar}`;
}
}
customElements.define('x-6', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
el.setAttribute('bar', '20');
await el.renderComplete;
await el.updateComplete;
assert.equal(el.bar, 20);

@@ -201,8 +671,8 @@ assert.equal(el.__bar, 20);

test('render attributes, properties, and event listeners via lit-html',
function() {
test('updates/renders attributes, properties, and event listeners via lit-html',
async () => {
class E extends LitElement {
_event?: Event;
_render() {
render() {
const attr = 'attr';

@@ -215,5 +685,6 @@ const prop = 'prop';

}
customElements.define('x-7', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
const d = el.shadowRoot!.querySelector('div')! as (HTMLDivElement &

@@ -228,269 +699,263 @@ {prop: string});

test('renderComplete waits until next rendering', async () => {
class E extends LitElement {
static get properties() { return {foo : Number}; }
foo = 0;
_render(props: {foo: string}) { return html`${props.foo}`; }
}
customElements.define('x-8', E);
const el = new E();
container.appendChild(el);
el.foo++;
await el.renderComplete;
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'1');
el.foo++;
await el.renderComplete;
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'2');
el.foo++;
await el.renderComplete;
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'3');
});
test('_shouldRender controls rendering', async () => {
class E extends LitElement {
static get properties() { return {foo : Number}; }
foo = 0;
renderCount = 0;
allowRender = true;
_render() {
this.renderCount++;
return html`hi`;
}
_shouldRender() { return this.allowRender; }
}
customElements.define('x-9', E);
const el = new E();
container.appendChild(el);
assert.equal(el.renderCount, 1);
el.foo++;
await el.renderComplete;
assert.equal(el.renderCount, 2);
el.allowRender = false;
el.foo++;
await el.renderComplete;
assert.equal(el.renderCount, 2);
el.allowRender = true;
el.foo++;
await el.renderComplete;
assert.equal(el.renderCount, 3);
});
test('renderComplete returns true if rendering happened and false otherwise',
async () => {
class E extends LitElement {
needsRender = true;
static get properties() { return {foo : Number}; }
_shouldRender() { return this.needsRender; }
foo = 0;
_render(props: {foo: string}) { return html`${props.foo}`; }
}
customElements.define('x-9.1', E);
const el = new E();
container.appendChild(el);
el.foo++;
let rendered;
rendered = await el.renderComplete;
assert.equal(rendered, true);
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'1');
el.needsRender = false;
el.foo++;
rendered = await el.renderComplete;
assert.equal(rendered, false);
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'1');
el.needsRender = true;
el.foo++;
rendered = await el.renderComplete;
assert.equal(rendered, true);
assert.equal(
stripExpressionDelimeters((el.shadowRoot as ShadowRoot).innerHTML),
'3');
el.requestRender();
rendered = await el.renderComplete;
assert.equal(rendered, true);
rendered = await el.renderComplete;
assert.equal(rendered, false);
});
test(
'render lifecycle order: _shouldRender, _render, _applyRender, _didRender',
async () => {
'render lifecycle order: shouldUpdate, update, render, updateComplete', async () => {
class E extends LitElement {
static get properties() { return {foo : Number}; }
static get properties() { return {
foo: {type: Number}
}; }
info: Array<string> = [];
_shouldRender() {
this.info.push('_shouldRender');
shouldUpdate() {
this.info.push('shouldUpdate');
return true;
}
_render() {
this.info.push('_render');
render() {
this.info.push('render');
return html`hi`;
}
_applyRender(result: TemplateResult, root: Element|DocumentFragment) {
this.info.push('_applyRender');
super._applyRender(result, root);
update(props: PropertyValues) {
this.info.push('before-update');
super.update(props);
}
_didRender() { this.info.push('_didRender'); }
async finishUpdate() {
this.info.push('finishUpdate');
}
}
customElements.define('x-10', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.renderComplete;
await el.updateComplete;
el.info.push('updateComplete');
assert.deepEqual(
el.info,
[ '_shouldRender', '_render', '_applyRender', '_didRender' ]);
[ 'shouldUpdate', 'before-update', 'render', 'finishUpdate', 'updateComplete' ]);
});
test('renderAttributes renders attributes on element', async () => {
test('setting properties in update does not trigger invalidation', async () => {
class E extends LitElement {
static get properties() { return {foo : Number, bar : Boolean}; }
static get properties() {
return {
foo: {}
};
}
promiseFulfilled = false;
foo = 0;
bar = true;
updated = 0;
_render({foo, bar}: {foo: number, bar: boolean}) {
renderAttributes(this, {foo, bar});
return html`${foo}${bar}`;
update(props: PropertyValues) {
this.updated++;
this.foo++;
super.update(props);
}
render() {
return html`${this.foo}`;
}
}
customElements.define('x-11', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.equal(el.getAttribute('foo'), '0');
assert.equal(el.getAttribute('bar'), '');
await el.updateComplete;
assert.equal(el.foo, 1);
assert.equal(el.updated, 1);
assert.equal(el.shadowRoot!.textContent, '1');
el.foo = 5;
el.bar = false;
await el.renderComplete;
assert.equal(el.getAttribute('foo'), '5');
assert.equal(el.hasAttribute('bar'), false);
await el.updateComplete;
assert.equal(el.foo, 6);
assert.equal(el.updated, 2);
assert.equal(el.shadowRoot!.textContent, '6');
});
test('classString updates classes', async () => {
test('setting properties in finishUpdate does trigger invalidation blocks updateComplete', async () => {
class E extends LitElement {
static get properties() {
return {foo : Number, bar : Boolean, baz : Boolean};
return {
foo: {}
};
}
promiseFulfilled = false;
foo = 0;
bar = true;
baz = false;
updated = 0;
fooMax = 2;
_render({foo, bar, baz}: {foo: number, bar: boolean, baz: boolean}) {
return html
`<div class$="${classString({foo, bar, zonk : baz})}"></div>`;
async finishUpdate() {
this.updated++;
if (this.foo < this.fooMax) {
this.foo++;
}
}
render() {
return html`${this.foo}`;
}
}
customElements.define('x-12', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
const d = el.shadowRoot!.querySelector('div')!;
assert.include(d.className, 'bar');
el.foo = 1;
el.baz = true;
await el.renderComplete;
assert.include(d.className, 'foo bar zonk');
el.bar = false;
await el.renderComplete;
assert.include(d.className, 'foo zonk');
el.foo = 0;
el.baz = false;
await el.renderComplete;
assert.notInclude(d.className, 'foo bar zonk');
await el.updateComplete;
assert.equal(el.foo, 2);
assert.equal(el.updated, 3);
assert.equal(el.shadowRoot!.textContent, '2');
el.fooMax = 10;
el.foo = 5;
await el.updateComplete;
assert.equal(el.foo, 10);
assert.equal(el.updated, 9);
assert.equal(el.shadowRoot!.textContent, '10');
});
test('styleString updates style', async () => {
test('can await promise in finishUpdate', async () => {
class E extends LitElement {
static get properties() {
return {
marginTop : String,
paddingTop : String,
zug : String
foo: {}
};
}
promiseFulfilled = false;
foo = 0;
marginTop = ``;
paddingTop = ``;
zug = `0px`;
render() {
return html`${this.foo}`;
}
_render(
{marginTop, paddingTop, zug}:
{marginTop: string, paddingTop: string, zug: string}) {
return html`<div style$="${
styleString(
{marginTop, paddingTop, height : zug})}"></div>`;
async finishUpdate() {
await new Promise((resolve) => {
setTimeout(() => {
this.promiseFulfilled = true;
resolve();
}, 1);
});
}
}
customElements.define('x-13', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
const d = el.shadowRoot!.querySelector('div')!;
let computed = getComputedStyle(d);
assert.equal(computed.getPropertyValue('margin-top'), '0px');
assert.equal(computed.getPropertyValue('height'), '0px');
el.marginTop = `2px`;
el.paddingTop = `5px`;
await el.renderComplete;
el.offsetWidth;
computed = getComputedStyle(d);
assert.equal(computed.getPropertyValue('margin-top'), '2px');
assert.equal(computed.getPropertyValue('height'), '0px');
assert.equal(computed.getPropertyValue('padding-top'), '5px');
el.marginTop = ``;
el.paddingTop = ``;
el.zug = ``;
await el.renderComplete;
assert.equal(d.style.cssText, '');
await el.updateComplete;
assert.isTrue(el.promiseFulfilled);
});
test('warns when setting properties re-entrantly', async () => {
test('updateComplete resolved after any properties set within finishUpdate', async () => {
class E extends LitElement {
_toggle: boolean = false;
_render() {
this._setProperty('foo', this._toggle ? 'fooToggle' : 'foo');
return html`hi`;
static get properties() {
return {
foo: {}
};
}
_didRender() {
this._setProperty('zonk', this._toggle ? 'zonkToggle' : 'zonk');
foo = 0;
render() {
return html`${this.foo}`;
}
async finishUpdate() {
if (this.foo < 10) {
this.foo++;
}
}
}
const calls: IArguments[] = [];
const orig = console.trace;
console.trace = function() { calls.push(arguments); };
customElements.define('x-14', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.equal(calls.length, 2);
el._toggle = true;
el.requestRender();
await el.renderComplete;
assert.equal(calls.length, 4);
console.trace = orig;
await el.updateComplete;
assert.equal(el.foo, 10);
assert.equal(el.shadowRoot!.textContent, '10');
});
test('can await sub-element updateComplete in finishUpdate', async () => {
class E extends LitElement {
static get properties() {
return {
foo: {}
};
}
promiseFulfilled = false;
foo = 'hi';
render() {
return html`${this.foo}`;
}
async finishUpdate() {
await new Promise((resolve) => {
setTimeout(() => {
this.promiseFulfilled = true;
resolve();
}, 0);
});
}
}
customElements.define('x-1224', E);
class F extends LitElement {
inner: E|null = null;
render() {
return html`<x-1224></x-1224>`;
}
async finishUpdate() {
this.inner = this.shadowRoot!.querySelector('x-1224');
this.inner!.foo = 'yo';
await this.inner!.updateComplete;
}
}
customElements.define(generateElementName(), F);
const el = new F();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.inner!.shadowRoot!.textContent, 'yo');
assert.isTrue(el.inner!.promiseFulfilled);
});
test('properties set before upgrade are applied', async () => {
const name = generateElementName();
const el = document.createElement(name);
container.appendChild(el);
(el as any).foo = 'hi';
(el as any).bar = false;
const objectValue = {};
(el as any).zug = objectValue;
class E extends LitElement {
static get properties() {
return {
foo: {},
bar: {},
zug: {}
};
}
foo = '';
bar = true;
zug = null;
render() {
return html`test`;
}
}
customElements.define(name, E);
await (el as LitElement).updateComplete;
assert.equal((el as any).foo, 'hi');
assert.equal((el as any).bar, false);
assert.equal((el as any).zug, objectValue);
});
});

@@ -17,1 +17,12 @@ /**

html.replace(/<!---->/g, '');
let count = 0;
export const generateElementName = () => `x-${count++}`;
export const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve));
export const getComputedStyleValue = (element: Element, property: string) =>
window.ShadyCSS ? window.ShadyCSS.getComputedStyleValue(element, property) :
getComputedStyle(element).getPropertyValue(property);

@@ -16,2 +16,3 @@ /**

import { html, LitElement, } from '../lit-element.js';
import { generateElementName, nextFrame, getComputedStyleValue } from './test-helpers.js';
const assert = chai.assert;

@@ -29,5 +30,6 @@ suite('Styling', () => {

});
test('content shadowRoot is styled', () => {
customElements.define('s-1', class extends LitElement {
_render() {
test('content shadowRoot is styled', async () => {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() {
return html `

@@ -42,8 +44,9 @@ <style>

});
const el = document.createElement('s-1');
const el = document.createElement(name);
container.appendChild(el);
await el.updateComplete;
const div = el.shadowRoot.querySelector('div');
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '2px');
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '2px');
});
test('shared styling rendered into shadowRoot is styled', () => {
test('shared styling rendered into shadowRoot is styled', async () => {
const style = html `<style>

@@ -54,4 +57,5 @@ div {

</style>`;
customElements.define('s-2', class extends LitElement {
_render() {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() {
return html `

@@ -67,10 +71,12 @@ <style>

});
const el = document.createElement('s-2');
const el = document.createElement(name);
container.appendChild(el);
await el.updateComplete;
const div = el.shadowRoot.querySelector('div');
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '4px');
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '4px');
});
test('custom properties render', () => {
customElements.define('s-3', class extends LitElement {
_render() {
test('custom properties render', async () => {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() {
return html `

@@ -88,10 +94,11 @@ <style>

});
const el = document.createElement('s-3');
const el = document.createElement(name);
container.appendChild(el);
await el.updateComplete;
const div = el.shadowRoot.querySelector('div');
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '8px');
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '8px');
});
test('custom properties flow to nested elements', () => {
customElements.define('s-4-inner', class extends LitElement {
_render() {
test('custom properties flow to nested elements', async () => {
customElements.define('x-inner', class extends LitElement {
render() {
return html `

@@ -106,21 +113,23 @@ <style>

});
customElements.define('s-4', class extends LitElement {
_render() {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() {
return html `
<style>
s-4-inner {
x-inner {
--border: 8px solid red;
}
</style>
<s-4-inner></s-4-inner>`;
<x-inner></x-inner>`;
}
});
const el = document.createElement('s-4');
const el = document.createElement(name);
container.appendChild(el);
const div = el.shadowRoot.querySelector('s-4-inner').shadowRoot.querySelector('div');
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '8px');
await nextFrame();
const div = el.shadowRoot.querySelector('x-inner').shadowRoot.querySelector('div');
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '8px');
});
test('elements with custom properties can move between elements', (done) => {
customElements.define('s-5-inner', class extends LitElement {
_render() {
test('elements with custom properties can move between elements', async () => {
customElements.define('x-inner1', class extends LitElement {
render() {
return html `

@@ -135,18 +144,20 @@ <style>

});
customElements.define('s-5', class extends LitElement {
_render() {
const name1 = generateElementName();
customElements.define(name1, class extends LitElement {
render() {
return html `
<style>
s-5-inner {
x-inner1 {
--border: 2px solid red;
}
</style>
<s-5-inner></s-5-inner>`;
<x-inner1></x-inner1>`;
}
});
customElements.define('s-6', class extends LitElement {
_render() {
const name2 = generateElementName();
customElements.define(name2, class extends LitElement {
render() {
return html `
<style>
s-5-inner {
x-inner1 {
--border: 8px solid red;

@@ -157,18 +168,18 @@ }

});
const el = document.createElement('s-5');
const el2 = document.createElement('s-6');
const el = document.createElement(name1);
const el2 = document.createElement(name2);
container.appendChild(el);
container.appendChild(el2);
const inner = el.shadowRoot.querySelector('s-5-inner');
const div = inner.shadowRoot.querySelector('div');
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '2px');
let div;
await nextFrame();
const inner = el.shadowRoot.querySelector('x-inner1');
div = inner.shadowRoot.querySelector('div');
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '2px');
el2.shadowRoot.appendChild(inner);
requestAnimationFrame(() => {
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '8px');
done();
});
await nextFrame();
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '8px');
});
test('@apply renders in nested elements', () => {
customElements.define('s-7-inner', class extends LitElement {
_render() {
test('@apply renders in nested elements', async () => {
customElements.define('x-inner2', class extends LitElement {
render() {
return html `

@@ -183,7 +194,8 @@ <style>

});
customElements.define('s-7', class extends LitElement {
_render() {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() {
return html `
<style>
s-7-inner {
x-inner2 {
--bag: {

@@ -194,9 +206,10 @@ border: 10px solid red;

</style>
<s-7-inner></s-7-inner>`;
<x-inner2></x-inner2>`;
}
});
const el = document.createElement('s-7');
const el = document.createElement(name);
container.appendChild(el);
const div = el.shadowRoot.querySelector('s-7-inner').shadowRoot.querySelector('div');
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '10px');
await nextFrame();
const div = el.shadowRoot.querySelector('x-inner2').shadowRoot.querySelector('div');
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '10px');
});

@@ -222,4 +235,5 @@ });

let border = `6px solid blue`;
customElements.define('shady-1', class extends LitElement {
_render() {
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() {
return html `

@@ -234,12 +248,13 @@ <style>

});
const el = document.createElement('shady-1');
const el = document.createElement(name);
container.appendChild(el);
await el.updateComplete;
const div = el.shadowRoot.querySelector('div');
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '6px');
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '6px');
border = `4px solid orange`;
el.requestRender();
await el.renderComplete;
assert.equal(getComputedStyle(div).getPropertyValue('border-top-width').trim(), '6px');
el.invalidate();
await el.updateComplete;
assert.equal(getComputedStyleValue(div, 'border-top-width').trim(), '6px');
});
});
//# sourceMappingURL=lit-element_styling_test.js.map

@@ -14,4 +14,4 @@ /**

*/
import { classString, html, LitElement, renderAttributes, styleString, } from '../lit-element.js';
import { stripExpressionDelimeters } from './test-helpers.js';
import { html, LitElement, BooleanAttribute } from '../lit-element.js';
import { stripExpressionDelimeters, generateElementName } from './test-helpers.js';
const assert = chai.assert;

@@ -29,75 +29,524 @@ suite('LitElement', () => {

});
test('renders initial content into shadowRoot', () => {
test('renders initial content into shadowRoot', async () => {
const rendered = `hello world`;
customElements.define('x-1', class extends LitElement {
_render() { return html `${rendered}`; }
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html `${rendered}`; }
});
const el = document.createElement('x-1');
const el = document.createElement(name);
container.appendChild(el);
assert.ok(el.shadowRoot);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), rendered);
await new Promise((resolve) => {
setTimeout(() => {
assert.ok(el.shadowRoot);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), rendered);
resolve();
});
});
});
test('can set render target to light dom', () => {
test('invalidate waits until update/rendering', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.updated = 0;
}
render() { return html `${++this.updated}`; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.invalidate();
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
await el.invalidate();
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '2');
await el.invalidate();
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '3');
});
test('updateComplete waits for invalidate but does not trigger invalidation, async', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.updated = 0;
}
render() { return html `${++this.updated}`; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
el.invalidate();
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '2');
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '2');
});
test('shouldUpdate controls update/rendering', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.needsUpdate = true;
this.updated = 0;
}
shouldUpdate() { return this.needsUpdate; }
render() { return html `${++this.updated}`; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
el.needsUpdate = false;
await el.invalidate();
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
el.needsUpdate = true;
await el.invalidate();
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '2');
await el.invalidate();
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '3');
});
test('can set render target to light dom', async () => {
const rendered = `hello world`;
customElements.define('x-1a', class extends LitElement {
_render() { return html `${rendered}`; }
_createRoot() { return this; }
const name = generateElementName();
customElements.define(name, class extends LitElement {
render() { return html `${rendered}`; }
createRenderRoot() { return this; }
});
const el = document.createElement('x-1a');
const el = document.createElement(name);
container.appendChild(el);
await el.updateComplete;
assert.notOk(el.shadowRoot);
assert.equal(stripExpressionDelimeters(el.innerHTML), rendered);
});
test('renders when created via constructor', () => {
test('renders when created via constructor', async () => {
const rendered = `hello world`;
class E extends LitElement {
_render() { return html `${rendered}`; }
render() { return html `${rendered}`; }
}
customElements.define('x-2', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.ok(el.shadowRoot);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), rendered);
});
test('renders changes when properties change', (done) => {
test('property options', async () => {
const shouldInvalidate = (value, old) => old === undefined || value > old;
const fromAttribute = (value) => parseInt(value);
const toAttribute = (value) => `${value}-attr`;
class E extends LitElement {
constructor() {
super(...arguments);
this.noAttr = 'noAttr';
this.atTr = 'attr';
this.customAttr = 'customAttr';
this.shouldInvalidate = 10;
this.fromAttribute = 1;
this.toAttribute = 1;
this.all = 10;
}
static get properties() {
return {
noAttr: { attribute: false },
atTr: { attribute: true },
customAttr: { attribute: 'custom', reflect: true },
shouldInvalidate: { shouldInvalidate },
fromAttribute: { type: fromAttribute },
toAttribute: { reflect: true, type: { toAttribute } },
all: { attribute: 'all-attr', shouldInvalidate, type: { fromAttribute, toAttribute }, reflect: true },
};
}
render() { return html ``; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr');
assert.equal(el.customAttr, 'customAttr');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 1);
assert.equal(el.toAttribute, 1);
assert.equal(el.getAttribute('toattribute'), '1-attr');
assert.equal(el.all, 10);
assert.equal(el.getAttribute('all-attr'), '10-attr');
el.setAttribute('noattr', 'noAttr2');
el.setAttribute('attr', 'attr2');
el.setAttribute('custom', 'customAttr2');
el.shouldInvalidate = 5;
el.setAttribute('fromattribute', '2attr');
el.toAttribute = 2;
el.all = 5;
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr2');
assert.equal(el.customAttr, 'customAttr2');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 2);
assert.equal(el.toAttribute, 2);
assert.equal(el.getAttribute('toattribute'), '2-attr');
assert.equal(el.all, 10);
el.shouldInvalidate = 15;
el.all = 15;
await el.updateComplete;
assert.equal(el.shouldInvalidate, 15);
assert.equal(el.all, 15);
assert.equal(el.getAttribute('all-attr'), '15-attr');
el.setAttribute('all-attr', '16-attr');
await el.updateComplete;
assert.equal(el.getAttribute('all-attr'), '16-attr');
assert.equal(el.all, 16);
});
test('attributes deserialize from html', async () => {
const shouldInvalidate = (value, old) => old === undefined || value > old;
const fromAttribute = (value) => parseInt(value);
const toAttributeOnly = (value) => typeof value === 'string' && value.indexOf(`-attr`) > 0 ? value : `${value}-attr`;
const toAttribute = (value) => `${value}-attr`;
class E extends LitElement {
constructor() {
super(...arguments);
this.noAttr = 'noAttr';
this.atTr = 'attr';
this.customAttr = 'customAttr';
this.shouldInvalidate = 10;
this.fromAttribute = 1;
this.toAttribute = 1;
this.all = 10;
}
static get properties() {
return {
noAttr: { attribute: false },
atTr: { attribute: true },
customAttr: { attribute: 'custom', reflect: true },
shouldInvalidate: { shouldInvalidate },
fromAttribute: { type: fromAttribute },
toAttribute: { reflect: true, type: { toAttribute: toAttributeOnly } },
all: { attribute: 'all-attr', shouldInvalidate, type: { fromAttribute, toAttribute }, reflect: true },
};
}
render() { return html ``; }
}
const name = generateElementName();
customElements.define(name, E);
container.innerHTML = `<${name}
noattr="1"
attr="2"
custom="3"
shouldInvalidate="5"
fromAttribute="6-attr"
toAttribute="7"
all-attr="11-attr"></${name}>`;
const el = container.firstChild;
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.getAttribute('noattr'), '1');
assert.equal(el.atTr, '2');
assert.equal(el.customAttr, '3');
assert.equal(el.getAttribute('custom'), '3');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.getAttribute('shouldinvalidate'), '5');
assert.equal(el.fromAttribute, 6);
assert.equal(el.toAttribute, '7');
assert.equal(el.getAttribute('toattribute'), '7-attr');
assert.equal(el.all, 11);
assert.equal(el.getAttribute('all-attr'), '11-attr');
});
if (Object.getOwnPropertySymbols) {
test('properties defined using symbols', async () => {
var _a;
const zug = Symbol();
class E extends LitElement {
constructor() {
super(...arguments);
this.updated = 0;
this.foo = 5;
this[_a] = 6;
}
static get properties() {
return {
foo: {},
[zug]: {}
};
}
render() {
return html ``;
}
update() {
this.updated++;
}
}
_a = zug;
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.updated, 1);
assert.equal(el.foo, 5);
assert.equal(el[zug], 6);
el.foo = 55;
await el.updateComplete;
assert.equal(el.updated, 2);
assert.equal(el.foo, 55);
assert.equal(el[zug], 6);
el[zug] = 66;
await el.updateComplete;
assert.equal(el.updated, 3);
assert.equal(el.foo, 55);
assert.equal(el[zug], 66);
});
}
test('property options compose when subclassing', async () => {
const shouldInvalidate = (value, old) => old === undefined || value > old;
const fromAttribute = (value) => parseInt(value);
const toAttribute = (value) => `${value}-attr`;
class E extends LitElement {
constructor() {
super(...arguments);
this.noAttr = 'noAttr';
this.atTr = 'attr';
this.customAttr = 'customAttr';
this.shouldInvalidate = 10;
this.fromAttribute = 1;
this.toAttribute = 1;
this.all = 10;
}
static get properties() {
return {
noAttr: { attribute: false },
atTr: { attribute: true },
customAttr: {},
shouldInvalidate: {},
};
}
render() { return html ``; }
}
customElements.define(generateElementName(), E);
class F extends E {
constructor() {
super(...arguments);
this.noAttr = 'noAttr';
this.atTr = 'attr';
this.customAttr = 'customAttr';
this.shouldInvalidate = 10;
this.fromAttribute = 1;
this.toAttribute = 1;
this.all = 10;
}
static get properties() {
return {
customAttr: { attribute: 'custom', reflect: true },
shouldInvalidate: { shouldInvalidate },
fromAttribute: {},
toAttribute: {},
};
}
render() { return html ``; }
}
class G extends F {
constructor() {
super(...arguments);
this.noAttr = 'noAttr';
this.atTr = 'attr';
this.customAttr = 'customAttr';
this.shouldInvalidate = 10;
this.fromAttribute = 1;
this.toAttribute = 1;
this.all = 10;
}
static get properties() {
return {
fromAttribute: { type: fromAttribute },
toAttribute: { reflect: true, type: { toAttribute } },
all: { attribute: 'all-attr', shouldInvalidate, type: { fromAttribute, toAttribute }, reflect: true },
};
}
render() { return html ``; }
}
customElements.define(generateElementName(), G);
const el = new G();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr');
assert.equal(el.customAttr, 'customAttr');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 1);
assert.equal(el.toAttribute, 1);
assert.equal(el.getAttribute('toattribute'), '1-attr');
assert.equal(el.all, 10);
assert.equal(el.getAttribute('all-attr'), '10-attr');
el.setAttribute('noattr', 'noAttr2');
el.setAttribute('attr', 'attr2');
el.setAttribute('custom', 'customAttr2');
el.shouldInvalidate = 5;
el.setAttribute('fromattribute', '2attr');
el.toAttribute = 2;
el.all = 5;
await el.updateComplete;
assert.equal(el.noAttr, 'noAttr');
assert.equal(el.atTr, 'attr2');
assert.equal(el.customAttr, 'customAttr2');
assert.equal(el.shouldInvalidate, 10);
assert.equal(el.fromAttribute, 2);
assert.equal(el.toAttribute, 2);
assert.equal(el.getAttribute('toattribute'), '2-attr');
assert.equal(el.all, 10);
el.shouldInvalidate = 15;
el.all = 15;
await el.updateComplete;
assert.equal(el.shouldInvalidate, 15);
assert.equal(el.all, 15);
assert.equal(el.getAttribute('all-attr'), '15-attr');
el.setAttribute('all-attr', '16-attr');
await el.updateComplete;
assert.equal(el.getAttribute('all-attr'), '16-attr');
assert.equal(el.all, 16);
});
test('superclass properties not affected by subclass', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.foo = 5;
this.bar = 'bar';
}
static get properties() {
return {
foo: { attribute: 'zug', reflect: true },
bar: { reflect: true }
};
}
render() { return html ``; }
}
customElements.define(generateElementName(), E);
class F extends E {
constructor() {
super(...arguments);
this.foo = 6;
this.bar = 'subbar';
this.nug = 5;
}
static get properties() {
return {
foo: { attribute: false },
nug: {}
};
}
render() { return html ``; }
}
customElements.define(generateElementName(), F);
const el = new E();
const sub = new F();
container.appendChild(el);
await el.updateComplete;
container.appendChild(sub);
await sub.updateComplete;
assert.equal(el.foo, 5);
assert.equal(el.getAttribute('zug'), '5');
assert.isFalse(el.hasAttribute('foo'));
assert.equal(el.bar, 'bar');
assert.equal(el.getAttribute('bar'), 'bar');
assert.isUndefined(el.nug);
assert.equal(sub.foo, 6);
assert.isFalse(sub.hasAttribute('zug'));
assert.isFalse(sub.hasAttribute('foo'));
assert.equal(sub.bar, 'subbar');
assert.equal(sub.getAttribute('bar'), 'subbar');
assert.equal(sub.nug, 5);
});
test('Attributes reflect with type.toAttribute and BooleanAttribute', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.foo = 0;
this.bar = true;
}
static get properties() {
return {
foo: { type: Number, reflect: true },
bar: { type: BooleanAttribute, reflect: true }
};
}
render() { return html ``; }
}
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
assert.equal(el.getAttribute('foo'), '0');
assert.equal(el.getAttribute('bar'), '');
el.foo = 5;
el.bar = false;
await el.updateComplete;
assert.equal(el.getAttribute('foo'), '5');
assert.equal(el.hasAttribute('bar'), false);
});
test('updates/renders when properties change', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.foo = 'one';
}
static get properties() { return { foo: String }; }
_render(props) { return html `${props.foo}`; }
static get properties() { return { foo: {} }; }
render() { return html `${this.foo}`; }
}
customElements.define('x-3', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.ok(el.shadowRoot);
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), 'one');
el.foo = 'changed';
requestAnimationFrame(() => {
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), 'changed');
done();
});
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), 'changed');
});
test('renders changes when attributes change', (done) => {
test('updates/renders when properties and attributes change', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.foo = 'one';
this.value = '1';
this.attrValue = 'attr';
this.updatedValue = '';
this.updatedAttrValue = '';
}
static get properties() { return { foo: String }; }
_render(props) { return html `${props.foo}`; }
static get properties() {
return {
value: {},
attrValue: {}
};
}
render() { return html ``; }
update(props) {
super.update(props);
this.updatedValue = this.value;
this.updatedAttrValue = this.attrValue;
}
}
customElements.define('x-4', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.ok(el.shadowRoot);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), 'one');
el.setAttribute('foo', 'changed');
requestAnimationFrame(() => {
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), 'changed');
done();
});
await el.updateComplete;
assert.equal(el.updatedValue, '1');
assert.equal(el.updatedAttrValue, 'attr');
el.value = '2';
await el.updateComplete;
assert.equal(el.updatedValue, '2');
assert.equal(el.updatedAttrValue, 'attr');
el.attrValue = 'attr2';
await el.updateComplete;
assert.equal(el.updatedValue, '2');
assert.equal(el.updatedAttrValue, 'attr2');
el.setAttribute('attrvalue', 'attr3');
await el.updateComplete;
assert.equal(el.updatedValue, '2');
assert.equal(el.updatedAttrValue, 'attr3');
el.value = '3';
el.setAttribute('attrvalue', 'attr4');
await el.updateComplete;
assert.equal(el.updatedValue, '3');
assert.equal(el.updatedAttrValue, 'attr4');
});
test('_firstRendered call after first render and not subsequent renders', async () => {
test('updates/renders changes when attributes change', async () => {
class E extends LitElement {

@@ -107,27 +556,19 @@ constructor() {

this.foo = 'one';
this.firstRenderedCount = 0;
this.domAtFirstRendered = '';
}
static get properties() { return { foo: String }; }
_firstRendered() {
this.firstRenderedCount++;
this.domAtFirstRendered =
stripExpressionDelimeters(this.shadowRoot.innerHTML);
static get properties() {
return { foo: {} };
}
_render(props) { return html `${props.foo}`; }
render() { return html `${this.foo}`; }
}
customElements.define('x-5', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.equal(el.firstRenderedCount, 1);
await el.updateComplete;
assert.ok(el.shadowRoot);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), el.domAtFirstRendered);
assert.equal(el.foo, el.domAtFirstRendered);
el.foo = 'two';
await el.renderComplete;
assert.equal(el.firstRenderedCount, 1);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), el.foo);
assert.notEqual(el.foo, el.domAtFirstRendered);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), 'one');
el.setAttribute('foo', 'changed');
await el.updateComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), 'changed');
});
test('User defined accessor can trigger rendering', async () => {
test('User defined accessor using setProperty/getProperty can trigger update/render', async () => {
class E extends LitElement {

@@ -139,18 +580,18 @@ constructor() {

}
static get properties() { return { foo: Number, bar: Number }; }
get bar() { return this._getProperty('bar'); }
static get properties() { return { foo: {}, bar: {} }; }
get bar() { return this.getProperty('bar'); }
set bar(value) {
this.__bar = value;
this._setProperty('bar', value);
this.__bar = Number(value);
this.setProperty('bar', value);
}
_render(props) {
render() {
this.info.push('render');
return html `${props.foo}${props.bar}`;
return html `${this.foo}${this.bar}`;
}
}
customElements.define('x-6', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
el.setAttribute('bar', '20');
await el.renderComplete;
await el.updateComplete;
assert.equal(el.bar, 20);

@@ -160,5 +601,5 @@ assert.equal(el.__bar, 20);

});
test('render attributes, properties, and event listeners via lit-html', function () {
test('updates/renders attributes, properties, and event listeners via lit-html', async () => {
class E extends LitElement {
_render() {
render() {
const attr = 'attr';

@@ -170,5 +611,6 @@ const prop = 'prop';

}
customElements.define('x-7', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.updateComplete;
const d = el.shadowRoot.querySelector('div');

@@ -181,142 +623,140 @@ assert.equal(d.getAttribute('attr'), 'attr');

});
test('renderComplete waits until next rendering', async () => {
test('render lifecycle order: shouldUpdate, update, render, updateComplete', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.foo = 0;
this.info = [];
}
static get properties() { return { foo: Number }; }
_render(props) { return html `${props.foo}`; }
}
customElements.define('x-8', E);
const el = new E();
container.appendChild(el);
el.foo++;
await el.renderComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
el.foo++;
await el.renderComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '2');
el.foo++;
await el.renderComplete;
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '3');
});
test('_shouldRender controls rendering', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.foo = 0;
this.renderCount = 0;
this.allowRender = true;
static get properties() {
return {
foo: { type: Number }
};
}
static get properties() { return { foo: Number }; }
_render() {
this.renderCount++;
shouldUpdate() {
this.info.push('shouldUpdate');
return true;
}
render() {
this.info.push('render');
return html `hi`;
}
_shouldRender() { return this.allowRender; }
update(props) {
this.info.push('before-update');
super.update(props);
}
async finishUpdate() {
this.info.push('finishUpdate');
}
}
customElements.define('x-9', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.equal(el.renderCount, 1);
el.foo++;
await el.renderComplete;
assert.equal(el.renderCount, 2);
el.allowRender = false;
el.foo++;
await el.renderComplete;
assert.equal(el.renderCount, 2);
el.allowRender = true;
el.foo++;
await el.renderComplete;
assert.equal(el.renderCount, 3);
await el.updateComplete;
el.info.push('updateComplete');
assert.deepEqual(el.info, ['shouldUpdate', 'before-update', 'render', 'finishUpdate', 'updateComplete']);
});
test('renderComplete returns true if rendering happened and false otherwise', async () => {
test('setting properties in update does not trigger invalidation', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.needsRender = true;
this.promiseFulfilled = false;
this.foo = 0;
this.updated = 0;
}
static get properties() { return { foo: Number }; }
_shouldRender() { return this.needsRender; }
_render(props) { return html `${props.foo}`; }
static get properties() {
return {
foo: {}
};
}
update(props) {
this.updated++;
this.foo++;
super.update(props);
}
render() {
return html `${this.foo}`;
}
}
customElements.define('x-9.1', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
el.foo++;
let rendered;
rendered = await el.renderComplete;
assert.equal(rendered, true);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
el.needsRender = false;
el.foo++;
rendered = await el.renderComplete;
assert.equal(rendered, false);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '1');
el.needsRender = true;
el.foo++;
rendered = await el.renderComplete;
assert.equal(rendered, true);
assert.equal(stripExpressionDelimeters(el.shadowRoot.innerHTML), '3');
el.requestRender();
rendered = await el.renderComplete;
assert.equal(rendered, true);
rendered = await el.renderComplete;
assert.equal(rendered, false);
await el.updateComplete;
assert.equal(el.foo, 1);
assert.equal(el.updated, 1);
assert.equal(el.shadowRoot.textContent, '1');
el.foo = 5;
await el.updateComplete;
assert.equal(el.foo, 6);
assert.equal(el.updated, 2);
assert.equal(el.shadowRoot.textContent, '6');
});
test('render lifecycle order: _shouldRender, _render, _applyRender, _didRender', async () => {
test('setting properties in finishUpdate does trigger invalidation blocks updateComplete', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.info = [];
this.promiseFulfilled = false;
this.foo = 0;
this.updated = 0;
this.fooMax = 2;
}
static get properties() { return { foo: Number }; }
_shouldRender() {
this.info.push('_shouldRender');
return true;
static get properties() {
return {
foo: {}
};
}
_render() {
this.info.push('_render');
return html `hi`;
async finishUpdate() {
this.updated++;
if (this.foo < this.fooMax) {
this.foo++;
}
}
_applyRender(result, root) {
this.info.push('_applyRender');
super._applyRender(result, root);
render() {
return html `${this.foo}`;
}
_didRender() { this.info.push('_didRender'); }
}
customElements.define('x-10', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
await el.renderComplete;
assert.deepEqual(el.info, ['_shouldRender', '_render', '_applyRender', '_didRender']);
await el.updateComplete;
assert.equal(el.foo, 2);
assert.equal(el.updated, 3);
assert.equal(el.shadowRoot.textContent, '2');
el.fooMax = 10;
el.foo = 5;
await el.updateComplete;
assert.equal(el.foo, 10);
assert.equal(el.updated, 9);
assert.equal(el.shadowRoot.textContent, '10');
});
test('renderAttributes renders attributes on element', async () => {
test('can await promise in finishUpdate', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.promiseFulfilled = false;
this.foo = 0;
this.bar = true;
}
static get properties() { return { foo: Number, bar: Boolean }; }
_render({ foo, bar }) {
renderAttributes(this, { foo, bar });
return html `${foo}${bar}`;
static get properties() {
return {
foo: {}
};
}
render() {
return html `${this.foo}`;
}
async finishUpdate() {
await new Promise((resolve) => {
setTimeout(() => {
this.promiseFulfilled = true;
resolve();
}, 1);
});
}
}
customElements.define('x-11', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
assert.equal(el.getAttribute('foo'), '0');
assert.equal(el.getAttribute('bar'), '');
el.foo = 5;
el.bar = false;
await el.renderComplete;
assert.equal(el.getAttribute('foo'), '5');
assert.equal(el.hasAttribute('bar'), false);
await el.updateComplete;
assert.isTrue(el.promiseFulfilled);
});
test('classString updates classes', async () => {
test('updateComplete resolved after any properties set within finishUpdate', async () => {
class E extends LitElement {

@@ -326,97 +766,103 @@ constructor() {

this.foo = 0;
this.bar = true;
this.baz = false;
}
static get properties() {
return { foo: Number, bar: Boolean, baz: Boolean };
return {
foo: {}
};
}
_render({ foo, bar, baz }) {
return html `<div class$="${classString({ foo, bar, zonk: baz })}"></div>`;
render() {
return html `${this.foo}`;
}
async finishUpdate() {
if (this.foo < 10) {
this.foo++;
}
}
}
customElements.define('x-12', E);
customElements.define(generateElementName(), E);
const el = new E();
container.appendChild(el);
const d = el.shadowRoot.querySelector('div');
assert.include(d.className, 'bar');
el.foo = 1;
el.baz = true;
await el.renderComplete;
assert.include(d.className, 'foo bar zonk');
el.bar = false;
await el.renderComplete;
assert.include(d.className, 'foo zonk');
el.foo = 0;
el.baz = false;
await el.renderComplete;
assert.notInclude(d.className, 'foo bar zonk');
await el.updateComplete;
assert.equal(el.foo, 10);
assert.equal(el.shadowRoot.textContent, '10');
});
test('styleString updates style', async () => {
test('can await sub-element updateComplete in finishUpdate', async () => {
class E extends LitElement {
constructor() {
super(...arguments);
this.marginTop = ``;
this.paddingTop = ``;
this.zug = `0px`;
this.promiseFulfilled = false;
this.foo = 'hi';
}
static get properties() {
return {
marginTop: String,
paddingTop: String,
zug: String
foo: {}
};
}
_render({ marginTop, paddingTop, zug }) {
return html `<div style$="${styleString({ marginTop, paddingTop, height: zug })}"></div>`;
render() {
return html `${this.foo}`;
}
async finishUpdate() {
await new Promise((resolve) => {
setTimeout(() => {
this.promiseFulfilled = true;
resolve();
}, 0);
});
}
}
customElements.define('x-13', E);
const el = new E();
customElements.define('x-1224', E);
class F extends LitElement {
constructor() {
super(...arguments);
this.inner = null;
}
render() {
return html `<x-1224></x-1224>`;
}
async finishUpdate() {
this.inner = this.shadowRoot.querySelector('x-1224');
this.inner.foo = 'yo';
await this.inner.updateComplete;
}
}
customElements.define(generateElementName(), F);
const el = new F();
container.appendChild(el);
const d = el.shadowRoot.querySelector('div');
let computed = getComputedStyle(d);
assert.equal(computed.getPropertyValue('margin-top'), '0px');
assert.equal(computed.getPropertyValue('height'), '0px');
el.marginTop = `2px`;
el.paddingTop = `5px`;
await el.renderComplete;
el.offsetWidth;
computed = getComputedStyle(d);
assert.equal(computed.getPropertyValue('margin-top'), '2px');
assert.equal(computed.getPropertyValue('height'), '0px');
assert.equal(computed.getPropertyValue('padding-top'), '5px');
el.marginTop = ``;
el.paddingTop = ``;
el.zug = ``;
await el.renderComplete;
assert.equal(d.style.cssText, '');
await el.updateComplete;
assert.equal(el.inner.shadowRoot.textContent, 'yo');
assert.isTrue(el.inner.promiseFulfilled);
});
test('warns when setting properties re-entrantly', async () => {
test('properties set before upgrade are applied', async () => {
const name = generateElementName();
const el = document.createElement(name);
container.appendChild(el);
el.foo = 'hi';
el.bar = false;
const objectValue = {};
el.zug = objectValue;
class E extends LitElement {
constructor() {
super(...arguments);
this._toggle = false;
this.foo = '';
this.bar = true;
this.zug = null;
}
_render() {
this._setProperty('foo', this._toggle ? 'fooToggle' : 'foo');
return html `hi`;
static get properties() {
return {
foo: {},
bar: {},
zug: {}
};
}
_didRender() {
this._setProperty('zonk', this._toggle ? 'zonkToggle' : 'zonk');
render() {
return html `test`;
}
}
const calls = [];
const orig = console.trace;
console.trace = function () { calls.push(arguments); };
customElements.define('x-14', E);
const el = new E();
container.appendChild(el);
assert.equal(calls.length, 2);
el._toggle = true;
el.requestRender();
await el.renderComplete;
assert.equal(calls.length, 4);
console.trace = orig;
customElements.define(name, E);
await el.updateComplete;
assert.equal(el.foo, 'hi');
assert.equal(el.bar, false);
assert.equal(el.zug, objectValue);
});
});
//# sourceMappingURL=lit-element_test.js.map

@@ -15,1 +15,4 @@ /**

export declare const stripExpressionDelimeters: (html: string) => string;
export declare const generateElementName: () => string;
export declare const nextFrame: () => Promise<{}>;
export declare const getComputedStyleValue: (element: Element, property: string) => any;

@@ -15,2 +15,7 @@ /**

export const stripExpressionDelimeters = (html) => html.replace(/<!---->/g, '');
let count = 0;
export const generateElementName = () => `x-${count++}`;
export const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve));
export const getComputedStyleValue = (element, property) => window.ShadyCSS ? window.ShadyCSS.getComputedStyleValue(element, property) :
getComputedStyle(element).getPropertyValue(property);
//# sourceMappingURL=test-helpers.js.map

@@ -17,3 +17,4 @@ {

// Only necessary because @types/uglify-js can't find types for source-map
"skipLibCheck": true
"skipLibCheck": true,
"experimentalDecorators": true
},

@@ -20,0 +21,0 @@ "include": [

@@ -9,3 +9,2 @@ {

],
"no-any": true,
"prefer-const": true,

@@ -12,0 +11,0 @@ "no-duplicate-variable": true,

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc