├── .gitignore ├── .npmignore ├── .prettierignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── browsersync.config.js ├── examples ├── index.html └── index.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── gluon.js └── test ├── browser.html ├── index.html ├── index.js ├── test-$.js └── test-is.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples/index.es5.js 3 | test/index.es5.js 4 | gluon.* 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | Dockerfile 3 | src 4 | test 5 | index.html 6 | .prettierignore 7 | .travis.yml 8 | browsersync.config.js 9 | rollup.config.js 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | 4 | install: npm install && npm run build 5 | script: npm test 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruphin/webserve 2 | 3 | COPY . /usr/share/nginx/html 4 | COPY ./node_modules/lit-html /usr/share/nginx/html/lit-html 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Goffert van Gool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: dev 2 | dev: 3 | docker run -it --rm -v $$PWD:/app -p 5000:5000 ruphin/webdev npm run dev 4 | 5 | .PHONY: shell 6 | shell: 7 | docker run -it --rm -v $$PWD:/app ruphin/webdev bash 8 | 9 | .PHONY: test 10 | test: 11 | docker run -it --rm -v $$PWD:/app ruphin/webdev npm run test 12 | 13 | .PHONY: guard 14 | guard: 15 | docker run -it --rm -v $$PWD:/app ruphin/webdev npm run guard 16 | 17 | .PHONY: build 18 | build: 19 | docker run -it --rm -v $$PWD:/app ruphin/webdev npm run build 20 | 21 | .PHONY: release 22 | release: build 23 | docker run -v $$PWD:/app \ 24 | -v $$HOME/.gitconfig:/home/app/.gitconfig \ 25 | -v $$HOME/.npmrc:/home/app/.npmrc \ 26 | -v $$HOME/.ssh:/home/app/.ssh \ 27 | -it --rm ruphin/webdev npm run release 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gluonjs 2 | 3 | [![Build Status](https://api.travis-ci.org/ruphin/gluonjs.svg?branch=master)](https://travis-ci.org/ruphin/gluonjs) 4 | [![NPM Latest version](https://img.shields.io/npm/v/@gluon/gluon.svg)](https://www.npmjs.com/package/@gluon/gluon) 5 | [![Code Style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | 7 | _A lightweight library for building web components and applications_ 8 | 9 | --- 10 | 11 | - **Platform Based:** GluonJS is designed to leverage the latest web platform capabilities, making it extremely small in size, and very performant on modern browsers. Additionally, it means that **build/compile steps are optional**; GluonJS components work on modern browsers without any pre-processing. 12 | - **Component Model:** Build components with encapsulated logic and style, then compose them to make complex interfaces. Uses the Web Component standards, with all related APIs available directly to developers. 13 | - **Highly Reusable:** Because GluonJS creates standards-compliant Web Components, you can use components created with GluonJS in almost any existing application. Check [Custom Elements Everywhere](https://custom-elements-everywhere.com/) for up-to-date compatibility tables with existing frameworks. 14 | - **Powerful Templating:** GluonJS uses [lit-html](https://github.com/PolymerLabs/lit-html) for templating, making it highly expressive and flexible. 15 | 16 | ## Concepts 17 | 18 | ### 19 | 20 | ```javascript 21 | import { GluonElement } from '/node_modules/@gluon/gluon/gluon.js'; 22 | 23 | class MyElement extends GluonElement { 24 | // ... 25 | } 26 | 27 | customElements.define(MyElement.is, MyElement); 28 | ``` 29 | 30 | ### Rendering 31 | 32 | Gluon uses [lit-html](https://github.com/PolymerLabs/lit-html) to efficiently render DOM into elements. The template to render is defined in the `template()` getter of an element, using JavaScript tagged template literals. 33 | 34 | If a `template` is defined, Gluon will render the template during the initialization of the element (when `super.connectedCallback()` is called). 35 | 36 | ## API 37 | 38 | ### $ 39 | 40 | All nodes in the template with an `id` attribute are automatically mappped in the `$` property of an element. This provides an easy way to access named nodes without `querySelector` or `getElementById`. 41 | 42 | The map is created during the initial render of the template. Use `this.shadowRoot.getElementById()` to access nodes that are added after the initial render. 43 | 44 | ### static get is() 45 | 46 | Returns a kebab-cased version of the element ClassName, as an easy default tagname for the customElementRegistry. This allows easy Custom Element registration using `customElements.define(MyElement.is, MyElement)`. 47 | 48 | \*\* NOTE: JavaScript minifiers like es-uglify may break this feature. Use the `{ keep_fnames: true, mangle: {keep_fnames: true} }` options in es-uglify to avoid breaking this feature. Alternatively, override the method to return a string to define a fixed tagname: ` 49 | 50 | ‡ Pending IE support in [lit-html](https://github.com/PolymerLabs/lit-html)static get is() { return 'my-element' }`. 51 | 52 | ### get template() 53 | 54 | ### render() 55 | 56 | Calling `render()` on an element will queue a render at the next [microtask timing](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/). Multiple calls are automatically batched into a single render. 57 | 58 | Returns a promise object that is fulfilled after the render is complete. 59 | 60 | To render synchronously, `render({sync: true})` 61 | 62 | ## Common Patterns 63 | 64 | ### Defining properties with getters and setters 65 | 66 | This basic pattern works by defining a property getter and setter that wrap some other storage location for the property value. 67 | 68 | ```javascript 69 | get someProp() { 70 | return this._someProp; 71 | } 72 | 73 | set someProp(value) { 74 | this._someProp = value; 75 | } 76 | ``` 77 | 78 | Defining properties with getters and setters has no benefits in itself, but it makes for a more flexible system when adding more features such as property defaults, synchronising between properties and attributes, typed properties, or observing property changes, examples of which are listed below. 79 | 80 | ### Computed properties 81 | 82 | Computed properties can be created by defining a property getter that computes the value for the property. 83 | 84 | ```javascript 85 | get computedProp() { 86 | return this.someProp + this.otherProp; 87 | } 88 | ``` 89 | 90 | \*\* NOTE: Computed properties are re-computed for every reference to the property. When the computation is expensive, it may be worthwhile to implement a cache: 91 | 92 | ```javascript 93 | get computedProp() { 94 | if (this.__previousSomeProp == this.someProp && this.__previousOtherProp === this.otherProp) { 95 | return this.__cachedComputedProp; 96 | } 97 | 98 | this.__previousSomeProp = this.someProp; 99 | this.__previousOtherProp = this.otherProp; 100 | this.__cachedComputedProp = this.someProp + this.otherProp; 101 | return this.__cachedComputedProp; 102 | } 103 | ``` 104 | 105 | ### Typed properties 106 | 107 | Define typed properties by adding type coercion in the property getter or setter. 108 | 109 | ```javascript 110 | get numberProperty() { 111 | return this._numberProperty; 112 | } 113 | 114 | set numberProperty(value) { 115 | this._numberProperty = Number(value); 116 | } 117 | ``` 118 | 119 | ### Synchronising properties and attributes 120 | 121 | ### Observing attribute changes 122 | 123 | Observing attribute changes is done using the Web Component attribute observer standards. 124 | 125 | To observe changes on attributes, define a `static get observedAttributes()` on the class that returns an array of all attributes to observe: 126 | 127 | ```javascript 128 | static get observedAttributes() { 129 | return ['some-attr', 'other-attr'] 130 | } 131 | ``` 132 | 133 | With this defined, `attributeChangedCallback(attr, oldValue, newValue)` will be called for all attributes listed in the array. 134 | 135 | ```javascript 136 | attributeChangedCallback(attr, oldValue, newValue) { 137 | if (attr === 'some-attr') { 138 | // some-attr changed 139 | } else if (attr === 'other-attr') { 140 | // other-attr changed 141 | } 142 | } 143 | ``` 144 | 145 | \*\* NOTE: attributeChangedCallback is also called for the initial attributes set on an element. It is in fact called before the `connectedCallback()`, which means the template has not yet been rendered. If you need to interact with child nodes, use the promise returned by `render()` to guarantee the template has been rendered and the child nodes exist: 146 | 147 | ```javascript 148 | attributeChangedCallback(attr, oldValue, newValue) { 149 | if (attr === 'some-attr') { 150 | this.render().then( () => this.$.child.someFunction() ); 151 | } 152 | } 153 | ``` 154 | 155 | ### Observing property changes 156 | 157 | Observing property changes is done by calling the observer at the end of the property setter function. 158 | 159 | ```javascript 160 | get someProp() { 161 | return this._someProp; 162 | } 163 | 164 | set someProp(value) { 165 | this._someProp = value; 166 | this.somePropChanged(); 167 | } 168 | ``` 169 | 170 | ## Examples 171 | 172 | Here is an example of a GluonJS component: 173 | 174 | ```javascript 175 | // helloMessage.js 176 | import { GluonElement, html } from '/node_modules/@gluon/gluon/gluon.js'; 177 | 178 | class HelloMessage extends GluonElement { 179 | get template() { 180 | return html`
Hello ${this.getAttribute('name')}
`; 181 | } 182 | } 183 | 184 | customElements.define(HelloMessage.is, HelloMessage); 185 | ``` 186 | 187 | We can import and use this component from anywhere: 188 | 189 | ```html 190 | 191 | 192 | 193 | 194 | ``` 195 | 196 | This example will render "Hello World". 197 | 198 | ## Installation 199 | 200 | GluonJS is available through [npm](https://www.npmjs.com) as `@gluon/gluon`. 201 | 202 | ## Compatibility 203 | 204 | | Chrome | Safari | Firefox | Edge | IE | 205 | | ------ | ------ | ------- | ---- | ---- | 206 | | ✔ | ✔ | \* | \* | \* † | 207 | 208 | \* Requires [Web Component polyfill](https://www.webcomponents.org/polyfills/) 209 | 210 | † Requires transpiling to ES5 211 | 212 | ## Contributing 213 | 214 | All work on GluonJS happens in the open on [Github](https://github.com/ruphin/gluonjs). A development environment is available at `localhost:5000` with `npm install && npm run dev`, or `make dev` if you use [Docker](https://www.docker.com/). All issue reports and pull requests are welcome. 215 | 216 | ## License 217 | 218 | [MIT](http://opensource.org/licenses/MIT) 219 | 220 | Copyright © 2017-present, Goffert van Gool 221 | -------------------------------------------------------------------------------- /browsersync.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 5000, 3 | notify: false, 4 | open: false, 5 | ui: false, 6 | online: false, 7 | logPrefix: 'APP', 8 | snippetOptions: { 9 | rule: { 10 | match: '', 11 | fn: function (snippet) { 12 | return snippet; 13 | } 14 | } 15 | }, 16 | server: { 17 | baseDir: ['examples', '.', 'node_modules'] 18 | }, 19 | files: ['*.js', 'src/*.js', 'examples/*.html', 'examples/*.js'] 20 | }; 21 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import { html, GluonElement } from '../src/gluon.js'; 2 | 3 | class HelloMessage extends GluonElement { 4 | get style() { 5 | return html` 6 | 7 | `; 8 | } 9 | get template() { 10 | return html` 11 | ${this.style} 12 |

Hello ${this.getAttribute('name')}

13 | `; 14 | } 15 | } 16 | 17 | class LoudMessage extends HelloMessage { 18 | get style() { 19 | return html` 20 | ${super.style} 21 | 22 | `; 23 | } 24 | } 25 | 26 | customElements.define(LoudMessage.is, LoudMessage); 27 | customElements.define(HelloMessage.is, HelloMessage); 28 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | singleRun: true, 4 | frameworks: ['mocha', 'chai'], 5 | files: [ 6 | { pattern: 'test/index.html', type: 'html' }, 7 | { pattern: 'test/**/*.js', included: false }, 8 | { pattern: 'src/**/*.js', included: false }, 9 | { pattern: 'node_modules/lit-html/**', included: false, watched: false }, 10 | { pattern: 'node_modules/@webcomponents/webcomponentsjs/**', included: false, watched: false }, 11 | { pattern: 'node_modules/babel-polyfill/dist/polyfill.min.js', included: false, watched: false } 12 | ], 13 | proxies: { 14 | '/lit-html/': { 15 | target: '/base/node_modules/lit-html/', 16 | changeOrigin: true 17 | } 18 | }, 19 | reporters: ['mocha'], 20 | browsers: ['DockerChromeHeadless'], 21 | customLaunchers: { 22 | DockerChromeHeadless: { 23 | base: 'ChromeHeadless', 24 | flags: ['--disable-gpu', '--no-sandbox'] 25 | }, 26 | FirefoxHeadless: { 27 | base: 'Firefox', 28 | flags: ['-headless'] 29 | } 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gluon/gluon", 3 | "version": "2.5.3", 4 | "description": "A tiny WebComponent library", 5 | "main": "gluon.js", 6 | "umd:main": "gluon.umd.js", 7 | "module": "gluon.js", 8 | "jsnext:main": "gluon.js", 9 | "files": [ 10 | "gluon.*" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ruphin/gluonjs.git" 15 | }, 16 | "author": "Goffert van Gool ", 17 | "keywords": [ 18 | "web-components", 19 | "webcomponents", 20 | "lit-html", 21 | "lit" 22 | ], 23 | "license": "MIT", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/ruphin/gluonjs/issues" 29 | }, 30 | "scripts": { 31 | "build": "rollup -c && cp src/gluon.js gluon.js", 32 | "dev": "browser-sync start --config browsersync.config.js", 33 | "release": "np", 34 | "test": "karma start", 35 | "guard": "karma start --no-single-run" 36 | }, 37 | "homepage": "https://github.com/ruphin/gluonjs", 38 | "dependencies": { 39 | "@babel/core": "^7.3.3", 40 | "lit-html": "1.0.0" 41 | }, 42 | "devDependencies": { 43 | "@webcomponents/webcomponentsjs": "2.2.7", 44 | "@babel/core": "7.3.3", 45 | "@babel/plugin-external-helpers": "7.2.0", 46 | "@babel/preset-env": "7.3.1", 47 | "@babel/polyfill": "7.2.5", 48 | "browser-sync": "2.26.3", 49 | "chai": "4.2.0", 50 | "karma": "3.0.0", 51 | "karma-chai": "0.1.0", 52 | "karma-chrome-launcher": "2.2.0", 53 | "karma-firefox-launcher": "1.1.0", 54 | "karma-mocha": "1.3.0", 55 | "karma-mocha-reporter": "2.2.5", 56 | "mocha": "6.0.0", 57 | "np": "4.0.2", 58 | "rollup": "1.2.2", 59 | "rollup-plugin-babel": "4.3.2", 60 | "rollup-plugin-commonjs": "9.2.0", 61 | "rollup-plugin-cleanup": "3.1.1", 62 | "rollup-plugin-filesize": "6.0.1", 63 | "rollup-plugin-includepaths": "0.2.3", 64 | "rollup-plugin-node-resolve": "4.0.0", 65 | "rollup-plugin-terser": "4.0.4" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import filesize from 'rollup-plugin-filesize'; 2 | import cleanup from 'rollup-plugin-cleanup'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import babel from 'rollup-plugin-babel'; 5 | import includePaths from 'rollup-plugin-includepaths'; 6 | import resolve from 'rollup-plugin-node-resolve'; 7 | import commonjs from 'rollup-plugin-commonjs'; 8 | import * as path from 'path'; 9 | 10 | const license = min => 11 | min 12 | ? '' 13 | : `/** 14 | * @license 15 | * MIT License 16 | * 17 | * Copyright (c) 2019 Goffert van Gool 18 | * 19 | * Permission is hereby granted, free of charge, to any person obtaining a copy 20 | * of this software and associated documentation files (the "Software"), to deal 21 | * in the Software without restriction, including without limitation the rights 22 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | * copies of the Software, and to permit persons to whom the Software is 24 | * furnished to do so, subject to the following conditions: 25 | * 26 | * The above copyright notice and this permission notice shall be included in all 27 | * copies or substantial portions of the Software. 28 | * 29 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | * SOFTWARE. 36 | */ 37 | `; 38 | 39 | const includePathOptions = { 40 | paths: ['node_modules/lit-html/lib', '.'], 41 | extensions: ['.js'] 42 | }; 43 | 44 | function getConfig({ dest, format, minified = false, transpiled = false, bundled = true }) { 45 | const conf = { 46 | input: 'src/gluon.js', 47 | output: { banner: license(minified), file: dest, name: 'GluonJS', format, sourcemap: !minified }, 48 | external: [!bundled && path.resolve('./node_modules/lit-html/lib/shady-render.js')].filter(Boolean), 49 | plugins: [ 50 | includePaths(includePathOptions), 51 | transpiled && resolve(), 52 | transpiled && 53 | commonjs({ 54 | include: 'node_modules/**' 55 | }), 56 | transpiled && 57 | babel({ 58 | presets: [['@babel/preset-env', { modules: false }]] 59 | }), 60 | // Remove duplicate license 61 | !minified && 62 | cleanup({ 63 | maxEmptyLines: 1, 64 | comments: [/^((?!\(c\) \d{4} Goffert)[\s\S])*$/] 65 | }), 66 | minified && 67 | terser({ 68 | warnings: true, 69 | mangle: { 70 | module: true 71 | }, 72 | output: { preamble: license(minified) } 73 | }), 74 | minified && filesize() 75 | ].filter(Boolean) 76 | }; 77 | 78 | return conf; 79 | } 80 | 81 | const example = { 82 | input: 'examples/index.js', 83 | output: { file: 'examples/index.es5.js', format: 'iife', sourcemap: false }, 84 | plugins: [ 85 | includePaths(includePathOptions), 86 | babel({ 87 | presets: [['@babel/preset-env', { modules: false }]] 88 | }) 89 | ] 90 | }; 91 | 92 | const test = { 93 | input: 'test/index.js', 94 | output: { file: 'test/index.es5.js', format: 'iife', sourcemap: false }, 95 | plugins: [ 96 | includePaths(includePathOptions), 97 | babel({ 98 | presets: [['@babel/preset-env', { modules: false }]] 99 | }) 100 | ], 101 | onwarn: (warning, warn) => { 102 | if (warning.code === 'THIS_IS_UNDEFINED') return; 103 | warn(warning); 104 | } 105 | }; 106 | 107 | const config = [ 108 | getConfig({ dest: 'gluon.es5.js', format: 'iife', transpiled: true }), 109 | getConfig({ dest: 'gluon.umd.js', format: 'umd' }), 110 | // Test bundled file sizes 111 | getConfig({ dest: '/dev/null', format: 'es', minified: true, bundled: false }), 112 | getConfig({ dest: '/dev/null', format: 'es', minified: true }), 113 | example, 114 | test 115 | ]; 116 | 117 | export default config; 118 | -------------------------------------------------------------------------------- /src/gluon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * MIT License 4 | * 5 | * Copyright (c) 2019 Goffert van Gool 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | 26 | import { render } from '../../lit-html/lib/shady-render.js'; 27 | export { html } from '../../lit-html/lib/shady-render.js'; 28 | 29 | // Key to store the HTML tag in a custom element class 30 | const TAG = Symbol('tag'); 31 | 32 | // Key to store render status in a custom element instance 33 | const NEEDSRENDER = Symbol('needsRender'); 34 | 35 | // Transforms a camelCased string into a kebab-cased string 36 | const camelToKebab = camel => camel.replace(/([a-z](?=[A-Z]))|([A-Z](?=[A-Z][a-z]))/g, '$1$2-').toLowerCase(); 37 | 38 | // Creates an ID cache in the `$` property of a custom element instance 39 | const createIdCache = element => { 40 | element.$ = {}; 41 | element.renderRoot.querySelectorAll('[id]').forEach(node => { 42 | element.$[node.id] = node; 43 | }); 44 | }; 45 | 46 | /** 47 | * A lightweight base class for custom elements 48 | * 49 | * Features: 50 | * 51 | * - Determines an appropriate HTML tagname based on an element's class name 52 | * - Efficient rendering engine using lit-html (https://github.com/Polymer/lit-html) 53 | * - Creates a cache for descendant nodes with an `id` in the `$` property 54 | */ 55 | export class GluonElement extends HTMLElement { 56 | constructor() { 57 | super(); 58 | this.renderRoot = this.createRenderRoot(); 59 | 60 | // This ensures that any properties that are set prior to upgrading this element 61 | // have their instance setters called 62 | Object.getOwnPropertyNames(this).forEach(property => { 63 | const propertyValue = this[property]; 64 | delete this[property]; 65 | this[property] = propertyValue; 66 | }); 67 | } 68 | 69 | /** 70 | * Returns an open shadowRoot as the default rendering root 71 | * 72 | * Override this method to provide an alternative rendering root 73 | * For example, return `this` to render the template as childNodes 74 | */ 75 | createRenderRoot() { 76 | return this.attachShadow({ mode: 'open' }); 77 | } 78 | 79 | /** 80 | * Returns the HTML tagname for elements of this class 81 | * 82 | * It defaults to the kebab-cased version of the class name. To override, 83 | * defined a `static get is()` property on your custom element class, and return 84 | * whatever string you want to use for the HTML tagname 85 | */ 86 | static get is() { 87 | return (this.hasOwnProperty(TAG) && this[TAG]) || (this[TAG] = camelToKebab(this.name)); 88 | } 89 | 90 | /** 91 | * Called when an element is connected to the DOM 92 | * 93 | * When an element has a `template`, attach a shadowRoot to the element, 94 | * and render the template. Once the template is rendered, creates an ID cache 95 | * in the `$` property 96 | * 97 | * When adding a `connectedCallback` to your custom element, you should call 98 | * `super.connectedCallback()` before doing anything other than actions 99 | * that alter the result of the template rendering. 100 | */ 101 | connectedCallback() { 102 | if ('template' in this) { 103 | this.render({ sync: true }); 104 | createIdCache(this); 105 | } 106 | } 107 | 108 | /** 109 | * Renders the template for this element into the shadowRoot 110 | * 111 | * @param { sync }: perform a synchronous (blocking) render. The default render 112 | * is asynchronous, and multiple calls to `render()` are batched by default 113 | * 114 | * @returns a Promise that resolves once template has been rendered 115 | */ 116 | async render({ sync = false } = {}) { 117 | this[NEEDSRENDER] = true; 118 | if (!sync) { 119 | await 0; 120 | } 121 | if (this[NEEDSRENDER]) { 122 | this[NEEDSRENDER] = false; 123 | render(this.template, this.renderRoot, { scopeName: this.constructor.is, eventContext: this }); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /test/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import './test-$.js'; 2 | import './test-is.js'; 3 | -------------------------------------------------------------------------------- /test/test-$.js: -------------------------------------------------------------------------------- 1 | import { GluonElement, html } from '../src/gluon.js'; 2 | 3 | const expect = chai.expect; 4 | 5 | class TestDollarElement extends GluonElement { 6 | get template() { 7 | return html`
test
`; 8 | } 9 | } 10 | 11 | customElements.define(TestDollarElement.is, TestDollarElement); 12 | 13 | const container = document.createElement('div'); 14 | container.style.display = 'none'; 15 | document.body.appendChild(container); 16 | 17 | let testElement; 18 | const setup = () => { 19 | container.innerHTML = ''; 20 | container.innerHTML = `<${TestDollarElement.is}>`; 21 | testElement = container.querySelector(TestDollarElement.is); 22 | }; 23 | 24 | describe(`'$' property`, () => { 25 | beforeEach(() => setup()); 26 | 27 | it('should contain a key for each child with an ID', () => { 28 | expect(Object.keys(testElement.$).length).to.be.equal(2); 29 | }); 30 | 31 | it('should map to the elements by ID', () => { 32 | expect(testElement.$.one).to.equal(testElement.shadowRoot.getElementById('one')); 33 | expect(testElement.$.two).to.equal(testElement.shadowRoot.getElementById('two')); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/test-is.js: -------------------------------------------------------------------------------- 1 | import { GluonElement } from '../src/gluon.js'; 2 | 3 | const expect = chai.expect; 4 | 5 | describe(`'is' property`, () => { 6 | it(`should be 'regular-element' for RegularElement`, () => { 7 | expect(class RegularElement extends GluonElement {}.is).to.be.equal('regular-element'); 8 | }); 9 | 10 | it(`should be 'caps-first-element' for CAPSFirstElement`, () => { 11 | expect(class CAPSFirstElement extends GluonElement {}.is).to.be.equal('caps-first-element'); 12 | }); 13 | 14 | it(`should be 'caps-middle-element' for CapsMIDDLEElement`, () => { 15 | expect(class CapsMIDDLEElement extends GluonElement {}.is).to.be.equal('caps-middle-element'); 16 | }); 17 | 18 | it(`should be 'caps-last-element' for CapsLastELEMENT`, () => { 19 | expect(class CapsLastELEMENT extends GluonElement {}.is).to.be.equal('caps-last-element'); 20 | }); 21 | 22 | it(`should be 'as-sh-ol-ec-as-e-element' for AsShOlEcAsEElement`, () => { 23 | expect(class AsShOlEcAsEElement extends GluonElement {}.is).to.be.equal('as-sh-ol-ec-as-e-element'); 24 | }); 25 | }); 26 | --------------------------------------------------------------------------------