`). Setting the property to `false` and the attribute is removed, ie. `
`.
424 |
425 | ##### Mapping from attribute to property
426 |
427 | When the attributes are set, the values are converted using their type constructors, ie ```String(attributeValue)``` for ```String```, ```Number(attributeValue)``` for ```Number```, etc.
428 |
429 | ```Boolean``` has special handling in order to follow the patterns of the Web Platform.
430 |
431 | From the HTML standard:
432 |
433 | > The presence of a boolean attribute on an element represents the true value, and the absence of the attribute represents the false value.
434 | >
435 | > If the attribute is present, its value must either be the empty string or a value that is an ASCII case-insensitive match for the attribute's canonical name, with no leading or trailing whitespace.
436 |
437 | Read more in the [Attribute reflection](#attribute-reflection) section above.
438 |
439 | #### The `computed` property
440 |
441 | Properties can be calculated from other properties using ```computed```, it takes a string like `'methodName(property1, property2)'`, where `methodName` is a method on the element and `property1` and `property2` are defined.
442 |
443 | Computed properties *only* update when *all dependent properties are defined*. Default value can be set using ```value:```
444 |
445 | NOTE, computed properties can not be reflected to attributes.
446 |
447 | ### `renderCallback`
448 |
449 | The `renderCallback` allows for custom hooks before and after rendering.
450 |
451 | If you need to do extra work before rendering, like setting a property based on another property, a subclass can override ```renderCallback()``` to do work before or after the base class calls ```render()```, including setting the dependent property before ```render()```.
452 |
453 | ### `withProperties()`
454 |
455 | TODO:
456 |
457 | ### `render(HTMLElement this)`
458 |
459 | TODO: Move docs here
460 |
461 | ### `async invalidate()`
462 |
463 | TODO: Move docs here
464 |
465 | ### `$(DOMString id)`
466 |
467 | TODO: Move docs here
468 |
469 | ### `whenAllDefined(TemplateResult result)`
470 |
471 | TODO: Move docs here
472 |
473 | ## Decorators
474 |
475 | ### `@customElement(USVString tagname)`
476 |
477 | A class decorator for registering the custom element
478 |
479 | ```typescript
480 | @customElement('my-element')
481 | class extends HTMLElement {
482 | ...
483 | }
484 | ```
485 |
486 | ### `@property(optional PropertyOptions options)`
487 |
488 | A property decorator for hooking into the `lit-html-element` property system.
489 |
490 | When using the property decorator you don't need to define the static properties accessor ```static get properties()```.
491 |
492 | When using property decorators any such static property accessor will be ignored, and you don't need to call ```.withProperties()``` either.
493 |
494 | ```typescript
495 | @property({type: String})
496 | myProperty: string;
497 | ```
498 |
499 | Check [Extensions for TypeScript](#extensions-for-typescript) for more info.
500 |
501 | ### `@attribute(USVString attrName)`
502 |
503 | A property decorator for hooking into the `lit-html-element` property system and associating a property with a custom element attribute.
504 |
505 | Check [The `attrName` property](#the-attrname-property) for more info.
506 |
507 | ### `@computed(any dependency1, any dependency2, ...)`
508 |
509 | A property decorator for hooking into the `lit-html-element` property system and create a property auto-computed from other properties.
510 |
511 | Check [The `computed` property](#the-computed-property) for more info.
512 |
513 | ### `@listen(USVString eventName, (USVString or EventTarget) target)`
514 |
515 | A method decorator for adding an event listener. You can use a string for target and it will search for an element in the shadowRoot with that `id`.
516 |
517 | Event listeners are added after the first rendering, which creates the shadow DOM.
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
50 |
51 |
52 |
53 |
54 |
80 | Horse
81 |
82 |
83 |
84 |
113 |
114 | ¡Hola, mundo!
115 |
116 |
117 |
118 |
156 |
174 |
175 |
176 |
177 |
178 |
213 |
214 |
215 |
216 |
224 |
225 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const typescript = require('gulp-tsc');
3 | const replace = require('gulp-replace-path');
4 |
5 | const config = {
6 | target: "es2017",
7 | module: "es2015",
8 | lib: ["es2017", "dom"],
9 | declaration: true,
10 | sourceMap: true,
11 | inlineSources: true,
12 | outDir: "./lib",
13 | baseUrl: ".",
14 | strict: true,
15 | noUnusedLocals: true,
16 | noUnusedParameters: true,
17 | noImplicitReturns: true,
18 | noFallthroughCasesInSwitch: true,
19 | experimentalDecorators: true,
20 | emitDecoratorMetadata: true
21 | };
22 |
23 | gulp.task('compile', function(){
24 | gulp.src(['src/*.ts'])
25 | .pipe(typescript(config))
26 | .pipe(replace(/..\/node_modules/g, '..'))
27 | .pipe(gulp.dest('.'));
28 |
29 | gulp.src('lit-element.js')
30 | .pipe(gulp.dest('node_modules/lit-html-element'));
31 |
32 | let testConfig = config;
33 | testConfig.emitDecoratorMetadata = true;
34 |
35 | gulp.src(['test/ts/*.ts'])
36 | .pipe(typescript(config))
37 | .pipe(gulp.dest('.'));
38 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lit-html-element",
3 | "version": "0.9.1",
4 | "description": "A base class for creating web components using lit-html",
5 | "main": "lit-element.js",
6 | "module": "lit-element.js",
7 | "files": [
8 | "lit-element.js",
9 | "/src/",
10 | "/lib/",
11 | "!/docs"
12 | ],
13 | "directories": {
14 | "test": "tests"
15 | },
16 | "scripts": {
17 | "build": "gulp compile",
18 | "lint": "tslint --project ./",
19 | "test": "npm run build && wct --npm && npm run lint",
20 | "checksize": "uglifyjs lit-element.js -mc --toplevel | gzip -9 | wc -c"
21 | },
22 | "author": "Kenneth Rohde Christiansen
",
23 | "homepage": "https://github.com/kenchris/lit-element",
24 | "license": "BSD-3-Clause",
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/kenchris/lit-element.git"
28 | },
29 | "dependencies": {
30 | "lit-html": "^0.8.0",
31 | "reflect-metadata": "github:rbuckton/reflect-metadata"
32 | },
33 | "devDependencies": {
34 | "gulp": "^3.9.1",
35 | "gulp-replace-path": "^0.4.0",
36 | "gulp-tsc": "^1.3.2",
37 | "tslint": "^5.9.1",
38 | "typescript": "^2.6.2",
39 | "uglify-es": "^3.3.5",
40 | "@types/chai": "^4.1.0",
41 | "@types/mocha": "^2.2.46",
42 | "chai": "^4.1.2",
43 | "mocha": "^3.5.3",
44 | "wct-browser-legacy": "0.0.1-pre.11",
45 | "web-component-tester": "^6.4.3",
46 | "reflect-metadata": "^0.1.10"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/lit-element-decorators.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { PropertyOptions, createProperty } from './lit-element.js';
4 |
5 | export function customElement(tagname: string) {
6 | return (clazz: any) => {
7 | window.customElements.define(tagname!, clazz);
8 | };
9 | }
10 |
11 | export function property(options?: PropertyOptions) {
12 | return (prototype: any, propertyName: string): any => {
13 | options = options || {};
14 | options.type = options.type || reflectType(prototype, propertyName);
15 | createProperty(prototype, propertyName, options);
16 | };
17 | }
18 |
19 | export function attribute(attrName: string) {
20 | return (prototype: any, propertyName: string): any => {
21 | const type = reflectType(prototype, propertyName);
22 | createProperty(prototype, propertyName, { attrName, type });
23 | };
24 | }
25 |
26 | export function computed(...targets: (keyof T)[]) {
27 | return (prototype: any, propertyName: string, descriptor: PropertyDescriptor): void => {
28 | const fnName = `__compute${propertyName}`;
29 |
30 | // Store a new method on the object as a property.
31 | Object.defineProperty(prototype, fnName, { value: descriptor.get });
32 | descriptor.get = undefined;
33 |
34 | createProperty(prototype, propertyName, { computed: `${fnName}(${targets.join(',')})` });
35 | };
36 | }
37 |
38 | export function listen(eventName: string, target: string|EventTarget) {
39 | return (prototype: any, methodName: string) => {
40 | if (!prototype.constructor.hasOwnProperty('listeners')) {
41 | prototype.constructor.listeners = [];
42 | }
43 | prototype.constructor.listeners.push({ target, eventName, handler: prototype[methodName] });
44 | }
45 | };
46 |
47 | function reflectType(prototype: any, propertyName: string): any {
48 | const { hasMetadata = () => false, getMetadata = () => null } = Reflect;
49 | if (hasMetadata('design:type', prototype, propertyName)) {
50 | return getMetadata('design:type', prototype, propertyName);
51 | }
52 | return null;
53 | }
--------------------------------------------------------------------------------
/src/lit-element.ts:
--------------------------------------------------------------------------------
1 | import { html, render } from '../node_modules/lit-html/lib/lit-extended.js';
2 | import { TemplateResult } from '../node_modules/lit-html/lit-html.js';
3 |
4 | export { html } from '../node_modules/lit-html/lib/lit-extended.js';
5 | export { TemplateResult } from '../node_modules/lit-html/lit-html.js';
6 |
7 | export interface PropertyOptions {
8 | type?: BooleanConstructor | DateConstructor | NumberConstructor | StringConstructor|
9 | ArrayConstructor | ObjectConstructor;
10 | value?: any;
11 | attrName?: string;
12 | computed?: string;
13 | }
14 |
15 | export interface ListenerOptions {
16 | target: string | EventTarget,
17 | eventName: string,
18 | handler: Function
19 | }
20 |
21 | export interface Map {
22 | [key: string]: T;
23 | }
24 |
25 | export function createProperty(prototype: any, propertyName: string, options: PropertyOptions = {}): void {
26 | if (!prototype.constructor.hasOwnProperty('properties')) {
27 | Object.defineProperty(prototype.constructor, 'properties', { value: {} });
28 | }
29 | prototype.constructor.properties[propertyName] = options;
30 | // Cannot attach from the decorator, won't override property.
31 | Promise.resolve().then(() => attachProperty(prototype, propertyName, options));
32 | }
33 |
34 | function attachProperty(prototype: any, propertyName: string, options: PropertyOptions) {
35 | const { type: typeFn, attrName } = options;
36 |
37 | function get(this: LitElement) { return this.__values__[propertyName]; }
38 | function set(this: LitElement, v: any) {
39 | // @ts-ignore
40 | let value = (v === null || v === undefined) ? v : (typeFn === Array ? v : typeFn(v));
41 | this._setPropertyValue(propertyName, value);
42 | if (attrName) {
43 | this._setAttributeValue(attrName, value, typeFn);
44 | }
45 | this.invalidate();
46 | }
47 |
48 | Object.defineProperty(prototype, propertyName, options.computed ? {get} : {get, set});
49 | }
50 |
51 | export function whenAllDefined(result: TemplateResult) {
52 | const template = result.template;
53 | const rootNode = template.element.content;
54 | const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT, null as any, false);
55 |
56 | const deps = new Set();
57 | while (walker.nextNode()) {
58 | const element = walker.currentNode as Element;
59 | if (element.tagName.includes('-')) {
60 | deps.add(element.tagName.toLowerCase());
61 | }
62 | }
63 |
64 | return Promise.all(Array.from(deps).map(tagName => customElements.whenDefined(tagName)));
65 | }
66 |
67 | export class LitElement extends HTMLElement {
68 | private _needsRender: boolean = false;
69 | private _lookupCache: Map = {};
70 | private _attrMap: Map = {};
71 | private _deps: Map> = {};
72 | __values__: Map = {};
73 |
74 | _setPropertyValue(propertyName: string, newValue: any) {
75 | this.__values__[propertyName] = newValue;
76 | if (this._deps[propertyName]) {
77 | this._deps[propertyName].map((fn: Function) => fn());
78 | }
79 | }
80 |
81 | _setPropertyValueFromAttributeValue(attrName: string, newValue: any) {
82 | const propertyName = this._attrMap[attrName];
83 | const { type: typeFn } = (this.constructor as any).properties[propertyName];
84 |
85 | let value;
86 | if (typeFn.name === 'Boolean') {
87 | value = (newValue === '') || (!!newValue && newValue === attrName.toLowerCase());
88 | } else {
89 | value = (newValue !== null) ? typeFn(newValue) : undefined;
90 | }
91 | this._setPropertyValue(propertyName, value);
92 | }
93 |
94 | _setAttributeValue(attrName: string, value: any, typeFn: any) {
95 | // @ts-ignore
96 | if (typeFn.name === 'Boolean') {
97 | if (!value) {
98 | this.removeAttribute(attrName);
99 | } else {
100 | this.setAttribute(attrName, '');
101 | }
102 | } else {
103 | this.setAttribute(attrName, value);
104 | }
105 | }
106 |
107 | static get properties(): Map {
108 | return {};
109 | }
110 |
111 | static get listeners(): Array {
112 | return [];
113 | }
114 |
115 | static get observedAttributes(): string[] {
116 | return Object.keys(this.properties)
117 | .map(key => (this.properties)[key].attrName)
118 | .filter(name => name);
119 | }
120 |
121 | constructor() {
122 | super();
123 | this.attachShadow({ mode: 'open' });
124 |
125 | for (const propertyName in (this.constructor as any).properties) {
126 | const options = (this.constructor as any).properties[propertyName];
127 | const { value, attrName, computed } = options;
128 |
129 | // We can only handle properly defined attributes.
130 | if (typeof(attrName) === 'string' && attrName.length) {
131 | this._attrMap[attrName] = propertyName;
132 | }
133 | // Properties backed by attributes have default values set from attributes, not 'value'.
134 | if (!attrName && value !== undefined) {
135 | this._setPropertyValue(propertyName, value);
136 | }
137 |
138 | const match = /(\w+)\((.+)\)/.exec(computed);
139 | if (match) {
140 | const fnName = match[1];
141 | const targets = match[2].split(/,\s*/);
142 |
143 | const computeFn = () => {
144 | const values = targets.map(target => (this)[target]);
145 | if ((this)[fnName] && values.every(entry => entry !== undefined)) {
146 | const computedValue = (this)[fnName].apply(this, values);
147 | this._setPropertyValue(propertyName, computedValue);
148 | }
149 | };
150 |
151 | for (const target of targets) {
152 | if (!this._deps[target]) {
153 | this._deps[target] = [ computeFn ];
154 | } else {
155 | this._deps[target].push(computeFn);
156 | }
157 | }
158 | computeFn();
159 | }
160 | }
161 | }
162 |
163 | static withProperties() {
164 | for (const propertyName in this.properties) {
165 | attachProperty(this.prototype, propertyName, this.properties[propertyName]);
166 | }
167 | return this;
168 | }
169 |
170 | renderCallback() {
171 | render(this.render(this), this.shadowRoot as ShadowRoot);
172 | }
173 |
174 | // @ts-ignore
175 | render(self: any): TemplateResult {
176 | return html``;
177 | }
178 |
179 | attributeChangedCallback(attrName: string, _oldValue: string, newValue: string) {
180 | this._setPropertyValueFromAttributeValue(attrName, newValue);
181 | this.invalidate();
182 | }
183 |
184 | connectedCallback() {
185 | for (const attrName of (this.constructor as any).observedAttributes) {
186 | this._setPropertyValueFromAttributeValue(attrName, this.getAttribute(attrName));
187 | }
188 |
189 | this.invalidate().then(() => {
190 | for (const listener of (this.constructor as any).listeners as Array) {
191 | const target = typeof listener.target === 'string' ? this.$(listener.target) : listener.target;
192 | target.addEventListener(listener.eventName, listener.handler.bind(this));
193 | }
194 | });
195 | }
196 |
197 | async invalidate() {
198 | if (!this._needsRender) {
199 | this._needsRender = true;
200 | // Schedule the following as micro task, which runs before
201 | // requestAnimationFrame. All additional invalidate() calls
202 | // before will be ignored.
203 | // https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
204 | this._needsRender = await false;
205 | this.renderCallback();
206 | }
207 | }
208 |
209 | $(id: string) {
210 | let value = this._lookupCache[id];
211 | if (!value && this.shadowRoot) {
212 | const element = this.shadowRoot.getElementById(id);
213 | if (element) {
214 | value = element;
215 | this._lookupCache[id] = element;
216 | }
217 | }
218 | return value;
219 | }
220 | }
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
389 |
390 |