├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── index.html ├── gulpfile.js ├── package.json ├── src ├── lit-element-decorators.ts └── lit-element.ts ├── test ├── index.html └── ts │ ├── index.html │ └── test.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lit-element.d.ts 3 | lit-element.js 4 | lit-element.js.map 5 | lit-element-decorators.d.ts 6 | lit-element-decorators.js 7 | lit-element-decorators.js.map 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | 12 | Fixed bug where removing a boolean attribute didn't make the property false. 13 | Fixed import paths 14 | 15 | ## Unreleased 16 | 17 | ### Changed 18 | - Changed the name of the rendering method from ´renderCallback´ to just ´render´ as per discussion (https://github.com/kenchris/lit-element/issues/5) 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, The LitElement Authors. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lit-element 2 | A base class for creating web components using [lit-html](https://travis-ci.org/PolymerLabs/lit-html) 3 | 4 | `lit-element` can be installed via the [lit-html-element](https://www.npmjs.com/package/lit-html-element) NPM package. 5 | 6 | ## Overview 7 | 8 | `lit-element` lets you create web components with [HTML templates](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) expressed with JavaScript [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), and efficiently render and _re-render_ those templates to DOM. 9 | 10 | `lit-element` accomplishes this by integrating [lit-html](https://github.com/PolymerLabs/lit-html) and has the following features: 11 | * Depends on ES modules and Web Components (polyfills can also be used) 12 | * Quite small (around 1kB compressed), with only a dependency on [lit-html](https://github.com/PolymerLabs/lit-html) 13 | * Works great with TypeScript with additional features such as decorators. 14 | * Good test coverage 15 | * Easy rendering by implementing ```render()``` methods 16 | * DOM updates are batched and rendered asynchronously 17 | * Pre/post render hooks possible via ```renderCallback``` 18 | * Manually trigger re-rendering by calling ```invalidate()``` 19 | * Access properties and methods using ```this``` or destructuring 20 | * Allows defining properties with additional powers 21 | * Content is invalidated as properties change 22 | * Properties can define types used for conversion 23 | * Properties can have default values 24 | * Properties/attributes can auto-reflect 25 | * Mapping name is up to user, no automatical case-conversion happens 26 | * Default values of auto-reflected properties depend on presence of attributes 27 | * Properties can be automatically calculated from other properties 28 | * Easy querying of element by `id` in the shadow root using `this.$(...)` 29 | 30 | ### Demos 31 | 32 | Demos can be found [here](https://kenchris.github.io/lit-element/). 33 | 34 | ### Basic example 35 | 36 | Simple write your HTML code using ```lit-html``` by creating a ```render()``` method. 37 | 38 | ```javascript 39 | import { LitElement, html } from '/src/lit-element.js'; 40 | 41 | class HelloWorld extends LitElement { 42 | render() { 43 | return html` 44 |
Hello World
45 | `; 46 | } 47 | } 48 | customElements.define('hello-world', HelloWorld) 49 | ``` 50 | ```html 51 | 52 | ``` 53 | 54 | ### Example: Querying elements by `id` 55 | 56 | After contents has been rendered the first time (ie. after ```connectedCallback()``` fires), then you can access elements in the shadow root by ```id``` using ```this.$(...)```. 57 | 58 | In the below example, we call ```this.changeColor()``` whenever the button is pressed, which in result accesses the div using ```this.$("wrapper")``` and modifies its background color. 59 | 60 | ```javascript 61 | class ColorMarker extends LitElement { 62 | changeColor() { 63 | const color = Math.random().toString(16).substr(2, 6); 64 | // Easily query the element by id: 65 | this.$("wrapper").style.backgroundColor = `#${color}`; 66 | } 67 | 68 | render() { 69 | return html` 70 | 75 | 78 |
79 | `; 80 | } 81 | } 82 | customElements.define('color-marker', ColorMarker); 83 | ``` 84 | ```html 85 | Horse 86 | ``` 87 | 88 | ### Example: using properties 89 | 90 | In this example we will use properties. Every property defined in the static getter ```properties()``` will make sure the content is re-rendered at the right time when modified. 91 | 92 | Properties can have default values and can even be reflected via attributes (changes go both ways). Instead of doing magic and converting cases after special rules like ```upper-case``` vs ```upperCase```, you instead define example which attribute name the property should reflect to, and thus avoid any ambiguity. 93 | 94 | NOTE, when using properties, you MUST call ```this.withProperties``` before using the elements. As the method returns the class itself, this can be done as part of ```customElements.define(...)``` 95 | 96 | NOTE, attributes default values are set from the element attributes themselves (present or missing) and thus default values set via 'value' are ignored. 97 | 98 | ```javascript 99 | import { LitElement, html } from '/src/lit-element.js'; 100 | 101 | class HelloWorld extends LitElement { 102 | static get properties() { 103 | return { 104 | uppercase: { 105 | type: Boolean, 106 | attrName: "uppercase" 107 | } 108 | } 109 | } 110 | 111 | render() { 112 | return html` 113 | 118 |
119 | Hello World 120 |
121 | `; 122 | } 123 | } 124 | customElements.define('hello-world', HelloWorld.withProperties()); 125 | ``` 126 | ```html 127 | 128 | ¡Hola, mundo! 129 | ``` 130 | 131 | ## Attribute reflection 132 | 133 | When creating custom elements, a good pattern is to use attributes instead of methods or properties. This allows using the element declaratively like ``````. 134 | 135 | For custom elements only consumed internally in other custom elements, it is often faster just relying on properties. This is also the case if you need to pass along complex data such as arrays or objects. 136 | 137 | In order to make it easy to work with attributes, ```lit-html-element``` supports mapping between attributes and properties automatically, just by defining the name of the attribute the property should map with via ```attrName:```. 138 | 139 | The presence of attributes or not (on elements) results in *actual values*, ie. a missing attribute for a boolean property, means the property will be ```false``` and for all other property types, ```undefined```. This means that when mapping properties to attributes, there is no such thing as a default value as values are always defined depending on the presence, or not, of attributes. This means that setting ```value:``` is ignored when ```attrName:``` is present. 140 | 141 | Values are converted using their type constructors, ie ```String(attributeValue)``` for ```String```, ```Number(attributeValue)``` for ```Number```, etc. 142 | 143 | ```Boolean``` has special handling in order to follow the patterns of the Web Platform. 144 | 145 | From the HTML standard: 146 | 147 | > The presence of a boolean attribute on an element represents the true value, and the absence of the attribute represents the false value. 148 | > 149 | > 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. 150 | 151 | ```Array``` and ```Object``` are disencouraged for attributes and have no special handling, thus values are converted using their constructors as any other value types, except boolean. 152 | 153 | ## Access element properties and methods from Destructuring 154 | 155 | ```this``` is passed to render() for you, which is cleaner. particularly when destructuring. You can still reference them manually, though. 156 | 157 | ```javascript 158 | class RenderShorthand extends LitElement { 159 | static get properties() { 160 | return { 161 | greeting: { 162 | type: String, 163 | value: "Hello" 164 | } 165 | } 166 | } 167 | 168 | render({ greeting }) { 169 | return html`${greeting} World!`; 170 | } 171 | } 172 | customElements.define('render-shorthand', RenderShorthand.withProperties()); 173 | ``` 174 | 175 | ## Advanced 176 | 177 | ### Automatical re-rendering 178 | 179 | When any of the properties in ```properties()``` change, `lit-element` will automatically re-render. The same goes for attributes which are mapped to properties via ```attrName```. 180 | 181 | If you need to re-render manually, you can trigger a re-render via a call to ```invalidate()```. This will schedule a microtask which will render the content just before next ```requestAnimationFrame```. 182 | 183 | ### Element upgrading 184 | 185 | Custom elements need to be upgraded before they work. This happens automatically by the browser when it has all the resources it needs. 186 | 187 | This mean that if you do a custom element which depends on other custom elements and use properties for data flow, then setting those properties before the element is upgraded, mean that you will end up shadowing the ```lit-html-element``` properties, meaning that the property updates and attribute reflection won't work as expected. 188 | 189 | There is an API ```whenAllDefined(result, container)``` for working around this issue, by allowing to wait until all of the dependencies have been upgraded. One way to use it is overwriting the ```renderCallback()```: 190 | 191 | ```javascript 192 | renderCallback() { 193 | if ("resolved" in this) { 194 | super.renderCallback(); 195 | } else { 196 | whenAllDefined(this.render(this)).then(() => { 197 | this.resolved = true; 198 | this.renderCallback(); 199 | }); 200 | } 201 | } 202 | ``` 203 | 204 | But you might still manage to shadow properties if you manual set values before upgraded like 205 | 206 | ```javascript 207 | document.getElementById('ninja').firstName = "Ninja"; 208 | ``` 209 | 210 | So guard these the following way: 211 | 212 | ```javascript 213 | customElements.whenDefined('computed-world').then(() => { 214 | document.getElementById('ninja').firstName = "Ninja"; 215 | }); 216 | ``` 217 | 218 | ### Computed properties 219 | 220 | If you need some properties that are calculated and updates depending on other properties, that is possible using the 'computed' value, which defined an object method with arguments as a string. 221 | 222 | Computed properties *only* update when *all dependent properties are defined*. Default value can be set using ```value:``` 223 | 224 | NOTE, computed properties can not be reflected to attributes. 225 | 226 | Eg. 227 | 228 | ```javascript 229 | import { LitElement, html } from '/node_modules/lit-html-element/lit-element.js'; 230 | 231 | class ComputedWorld extends LitElement { 232 | static get properties() { 233 | return { 234 | firstName: { 235 | type: String, 236 | attrName: "first-name" 237 | }, 238 | doubleMessage: { 239 | type: String, 240 | computed: 'computeDoubleMessage(message)' 241 | }, 242 | message: { 243 | type: String, 244 | computed: 'computeMessage(firstName)', 245 | value: 'Hej Verden' 246 | } 247 | } 248 | } 249 | computeDoubleMessage(message) { 250 | return message + " " + message; 251 | } 252 | computeMessage(firstName) { 253 | return `Konichiwa ${firstName}`; 254 | } 255 | render() { 256 | return html` 257 |
${this.doubleMessage}
258 | `; 259 | } 260 | } 261 | customElements.define('computed-world', ComputedWorld.withProperties()) 262 | ``` 263 | ```html 264 | 265 | 266 | ``` 267 | 268 | ## Extensions for TypeScript 269 | 270 | It is possible to use ```lit-html-element``` from TypeScript instead of JavaScript. When using TypeScript, you can opt into using decorators instead of defining the static properties accessor ```static get properties()```. 271 | 272 | When using property decorators any such static property accessor will be ignored, and you don't need to call ```.withProperties()``` either. 273 | 274 | ```typescript 275 | import { 276 | LitElement, 277 | html, 278 | TemplateResult, 279 | customElement, 280 | property, 281 | attribute, 282 | computed 283 | } from '../../src/lit-element.js'; 284 | 285 | @customElement('test-element') 286 | export class TestElement extends LitElement { 287 | @computed('firstName', 'lastName') 288 | get fullName(): string { 289 | return `${this.firstName} ${this.lastName}`; 290 | } 291 | 292 | @property() firstName: string = 'John'; 293 | @property() lastName: string = 'Doe'; 294 | 295 | @property() human: boolean = true; 296 | @property() favorite: any = { fruit: 'pineapple'}; 297 | @property() kids: Array = ['Peter', 'Anna']; 298 | 299 | @attribute('mother') mother: string; 300 | @attribute('super-star') superStar: boolean; 301 | 302 | render(): TemplateResult { 303 | return html` 304 |

Name: ${this.fullName}

305 |

Is human?: ${human ? "yup" : "nope"}

306 |

Favorites: ${JSON.stringify(this.favorite)}

307 |

Kids: ${JSON.stringify(this.kids)}

308 |

Mother: '${this.mother}'

309 |

Superstar?: '${this.superStar}'

310 | `; 311 | } 312 | } 313 | 314 | ``` 315 | 316 | ```html 317 | 318 | ``` 319 | 320 | ### How to enable 321 | 322 | In order to use decorators from TypeScript you need to enabled the ```experimentalDecorators``` compiler setting in your ```tsconfig.json``` or use the ```--experimentalDecorators``` flag. 323 | 324 | ```json 325 | { 326 | "compilerOptions": { 327 | "experimentalDecorators": true 328 | } 329 | } 330 | ``` 331 | 332 | With the above enabled, you can start using decorators but MUST specify the type information manually: 333 | 334 | ```typescript 335 | @property({type: String}) 336 | myProperty: string; 337 | ``` 338 | 339 | As the type often can be derives from the property, especially in TypeScript where you define the type, this feels like a bit of double work. Luckily there is a new specification proposal called [Metadata Reflection](https://rbuckton.github.io/reflect-metadata/) which aims at solving this problem. This proposal has yet to be formally proposed to the TC39 working group (defines the JavaScript standard) but there is already a working polyfill available and experimental support in TypeScript. 340 | 341 | With Metadata Reflection enabled it is possible to define property types more concisely: 342 | 343 | ```typescript 344 | @property() myProperty: string; 345 | ``` 346 | 347 | In order to use decorators from TypeScript follow the following steps. 348 | 349 | 1. You need to enabled the ```emitDecoratorMetadata``` compiler setting in your ```tsconfig.json``` or use the ```--emitDecoratorMetadata``` flag. 350 | 351 | ```json 352 | { 353 | "compilerOptions": { 354 | "emitDecoratorMetadata": true 355 | } 356 | } 357 | ``` 358 | 359 | 2. Install the Metadata Reflection API runtime polyfill from [rbuckton/reflect-metadata](https://github.com/rbuckton/reflect-metadata): 360 | 361 | ```bash 362 | $ npm install --save-dev rbuckton/reflect-metadata 363 | ``` 364 | 365 | 3. Load the polyfill at the top-level of your application: 366 | 367 | ```html 368 | 369 | ``` 370 | 371 | # API documentation 372 | 373 | The following API documentation uses Web IDL. 374 | 375 | ### Static property accessor and `PropertyOptions` 376 | 377 | PropertyOptions are used for configuring the properties for the custom element. In JavaScript you need to implement a static property accessor called `properties`, which returns an object where each property of that object has an associated `PropertyOptions`: 378 | 379 | ```javascript 380 | class { 381 | static get properties() { 382 | return { selfDefinedObjectProperty: ... } 383 | } 384 | } 385 | ``` 386 | 387 | The `PropertyOptions` dictionary has 4 optional properties, shown below in Web IDL format. 388 | 389 | ```idl 390 | typedef (BooleanConstructor or DateConstructor or NumberConstructor or StringConstructor or ArrayConstructor or ObjectConstructor) PropertyType; 391 | 392 | dictionary PropertyOptions { 393 | attribute PropertyType type; 394 | attribute any value; 395 | attribute USVString attrName; 396 | attribute USVString computed; 397 | } 398 | ``` 399 | 400 | #### The `type` property 401 | The `type` property is only optional when using decorators and Metadata Reflection. 402 | 403 | #### The `value` property 404 | 405 | The `value` property defines a default value for the property. In case of attribute / property mapping via `attrName` (see below), `value` is ignored. When using decorators, the value is taking from the property definition itself: 406 | 407 | ```typescript 408 | @property() myProperty: string = "Hello World"; 409 | ``` 410 | 411 | #### The `attrName` property 412 | 413 | The `attrName` defines the name of the attribute which should be reflected with the property and the other way around. With `attrName`, default values are ignored and determined from the custom element instead, ie. depending on the presence or not of the attributes. 414 | 415 | The attribute name, much be in Latin letters (a-z) including '-' (hyphen). All attributes on HTML elements in HTML documents get ASCII-lowercased automatically, and initial hyphen ('-') gets ignored. 416 | 417 | Be aware that data attributes, ie. attributes starting with `data-` are accessible as properties automatically via `element.dataset`. 418 | 419 | ##### Mapping from property to attribute 420 | 421 | When mapped properties get set on the element, the attribute gets updated with the string representation of the new value, unless the new value is `undefined` in which the attribute gets removed. 422 | 423 | There is one exception to this, as boolean properties as reflected differently. Setting the property to `true` and the attribute (say `attr`) is set to the empty string `''` (meaning attribute is present, ie. `
`). 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 | -------------------------------------------------------------------------------- /test/ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | -------------------------------------------------------------------------------- /test/ts/test.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, TemplateResult } from '../../src/lit-element.js'; 2 | import { customElement, property, attribute, computed, listen } from '../../src/lit-element-decorators.js'; 3 | 4 | @customElement('test-element') 5 | export class TestElement extends LitElement { 6 | @property({ computed: 'calcName(firstName, lastName)' }) 7 | name: string; 8 | 9 | @computed('firstName', 'lastName') 10 | get realName(): string { 11 | return `${this.firstName} ${this.lastName}`; 12 | } 13 | 14 | @property() firstName: string = 'John'; 15 | @property() lastName: string = 'Doe'; 16 | 17 | @property() stringProp: string = 'Example'; 18 | @property() booleanProp: boolean = true; 19 | @property() objectProp: any = { fruit: 'pineapple '}; 20 | @property() arrayProp: Array = ['apple']; 21 | 22 | @attribute('string-attr') stringAttr: string; 23 | @attribute('boolean-attr') booleanAttr: boolean; 24 | 25 | calcName(firstName: string, lastName: string) { 26 | return `${firstName} ${lastName}`; 27 | } 28 | 29 | @listen('click', 'button') 30 | onButtonClicked() { 31 | this.booleanProp = !this.booleanProp; 32 | } 33 | 34 | render({ stringProp, booleanProp, objectProp, arrayProp }): TemplateResult { 35 | const props = TestElement.properties; 36 | const getType = (name: string): string => props[name].type.name; 37 | 38 | return html` 39 | RealName: ${this.realName}
40 | Name: ${this.name}
41 | 42 |

${getType('stringProp')}: ${stringProp}

43 |

${getType('booleanProp')}: ${booleanProp}

44 |

${getType('objectProp')}: ${JSON.stringify(objectProp)}

45 |

${getType('arrayProp')}: ${JSON.stringify(arrayProp)}

46 |

${getType('stringAttr')}: '${this.getAttribute('string-attr')}'

47 |

${getType('booleanAttr')}: '${this.getAttribute('boolean-attr')}'

48 | `; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "lib": ["es2017", "dom"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "inlineSources": true, 9 | "outDir": "./lib", 10 | "baseUrl": ".", 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true 18 | }, 19 | "include": [ 20 | "src/*.ts", 21 | "test/ts/*.ts" 22 | ], 23 | "exclude": [] 24 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": [ 4 | "error", 5 | "as-needed" 6 | ], 7 | "class-name": true, 8 | "indent": [ 9 | true, 10 | "spaces", 11 | 2 12 | ], 13 | "prefer-const": [ 14 | "error", 15 | { 16 | "destructuring": "all", 17 | "ignoreReadBeforeAssign": false 18 | } 19 | ], 20 | "no-duplicate-variable": true, 21 | "no-eval": true, 22 | "no-internal-module": true, 23 | "no-trailing-whitespace": true, 24 | "no-var-keyword": true, 25 | "one-line": [ 26 | true, 27 | "check-open-brace", 28 | "check-whitespace" 29 | ], 30 | "quotemark": [ 31 | true, 32 | "single", 33 | "avoid-escape" 34 | ], 35 | "semicolon": [ 36 | true, 37 | "always" 38 | ], 39 | "trailing-comma": [ 40 | true, 41 | "multiline" 42 | ], 43 | "triple-equals": [ 44 | true, 45 | "allow-null-check" 46 | ], 47 | "typedef-whitespace": [ 48 | true, 49 | { 50 | "call-signature": "nospace", 51 | "index-signature": "nospace", 52 | "parameter": "nospace", 53 | "property-declaration": "nospace", 54 | "variable-declaration": "nospace" 55 | } 56 | ], 57 | "variable-name": [ 58 | true, 59 | "ban-keywords" 60 | ], 61 | "whitespace": [ 62 | true, 63 | "check-branch", 64 | "check-decl", 65 | "check-operator", 66 | "check-separator", 67 | "check-type" 68 | ] 69 | } 70 | } --------------------------------------------------------------------------------