├── .npmrc ├── cjs ├── package.json └── index.js ├── .gitignore ├── .npmignore ├── test ├── compo-nent.js ├── what-ever.js ├── whatever-else.js ├── index.html ├── lazy.js ├── my-counter.uce ├── uce-lazy.js └── my-counter.html ├── rollup ├── es.config.js └── babel.config.js ├── es.js ├── min.js ├── LICENSE ├── esm └── index.js ├── package.json ├── README.md └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .travis.yml 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | -------------------------------------------------------------------------------- /test/compo-nent.js: -------------------------------------------------------------------------------- 1 | customElements.define('compo-nent', class extends HTMLElement { 2 | connectedCallback() { 3 | this.textContent = 'Hello loader!'; 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /test/what-ever.js: -------------------------------------------------------------------------------- 1 | customElements.define('what-ever', class extends HTMLElement { 2 | connectedCallback() { 3 | this.innerHTML = ` 4 | ${this.nodeName.toLowerCase()} is connected
5 | 6 | `; 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /test/whatever-else.js: -------------------------------------------------------------------------------- 1 | customElements.define('whatever-else', class extends HTMLElement { 2 | connectedCallback() { 3 | this.appendChild( 4 | this.ownerDocument.createElement('p') 5 | ).textContent = this.nodeName.toLowerCase() + ' is connected too'; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /rollup/es.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | 8 | resolve({module: true}), 9 | terser() 10 | ], 11 | 12 | output: { 13 | exports: 'named', 14 | file: './es.js', 15 | format: 'iife', 16 | name: 'uceLoader' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | 8 | resolve({module: true}), 9 | babel({presets: ['@babel/preset-env']}) 10 | ], 11 | 12 | output: { 13 | exports: 'named', 14 | file: './index.js', 15 | format: 'iife', 16 | name: 'uceLoader' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uce-loader 7 | 8 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | self.uceLoader=function(e){"use strict";const t=/^(?:annotation-xml|color-profile|font-face(?:|-format|-name|-src|-uri)|missing-glyph)$/i,n=new Set;return e.default=e=>{const o=e.container||document,r=o=>{for(let r=0,{length:c}=o;r{r([{addedNodes:e}])};s([document==o?o.documentElement:o]);const c=new MutationObserver(r);return c.observe(o,{subtree:!0,childList:!0}),c},e}({}).default; 2 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | self.uceLoader=function(e){"use strict";var t=/^(?:annotation-xml|color-profile|font-face(?:|-format|-name|-src|-uri)|missing-glyph)$/i,n=new Set;return e.default=function(e){var r=e.container||document,o=function(r){for(var o=0,i=r.length;o 2 | 5 | {{state.count}} 6 | 9 | 10 | 11 | 24 | 25 | 46 | -------------------------------------------------------------------------------- /test/uce-lazy.js: -------------------------------------------------------------------------------- 1 | addEventListener( 2 | 'DOMContentLoaded', 3 | function () { 4 | uceLoader({ 5 | on: function (name) { 6 | if (name !== 'uce-template') { 7 | var self = this; 8 | if (!self.queue) { 9 | self.queue = [name]; 10 | var script = document.createElement('script'); 11 | script.src = '//unpkg.com/uce-template'; 12 | document.body.appendChild(script).onload = function () { 13 | self.Template = customElements.get('uce-template'); 14 | for (var i = 0; i < self.queue.length; i++) 15 | self.loader(self.queue[i]); 16 | }; 17 | } 18 | else if (self.Template) { 19 | var xhr = new XMLHttpRequest; 20 | xhr.open('get', name + '.uce', true); 21 | xhr.send(null); 22 | xhr.onload = function () { 23 | document.body.appendChild( 24 | self.Template.from(xhr.responseText) 25 | ); 26 | }; 27 | } 28 | else 29 | self.queue.push(name); 30 | } 31 | } 32 | }); 33 | }, 34 | {once: true} 35 | ); 36 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name 2 | const ignore = /^(?:annotation-xml|color-profile|font-face(?:|-format|-name|-src|-uri)|missing-glyph)$/i; 3 | const loaded = new Set; 4 | 5 | /** 6 | * @typedef {Object} Options 7 | * @prop {Node} [container=document] - where to monitor Custom Elements 8 | * @prop {function} on - a callback invoked per each new Custom Element 9 | */ 10 | 11 | /** 12 | * Start observing a document, or a specific container, and automatically 13 | * download once Custom Elements from a specific path. 14 | * @param {Options} options configuration options 15 | * @returns {MutationObserver} the disconnect-able `container` observer 16 | */ 17 | export default (options) => { 18 | const target = options.container || document; 19 | const load = mutations => { 20 | for (let i = 0, {length} = mutations; i < length; i++) { 21 | for (let 22 | {addedNodes} = mutations[i], 23 | j = 0, {length} = addedNodes; j < length; j++ 24 | ) { 25 | const node = addedNodes[j]; 26 | const {children, getAttribute, tagName} = node; 27 | if (getAttribute) { 28 | const is = (getAttribute.call(node, 'is') || tagName).toLowerCase(); 29 | if (0 < is.indexOf('-') && !loaded.has(is) && !ignore.test(is)) { 30 | loaded.add(is); 31 | customElements.get(is) || options.on(is); 32 | } 33 | crawl(children); 34 | } 35 | } 36 | } 37 | }; 38 | const crawl = addedNodes => { load([{addedNodes}]) }; 39 | crawl([document == target ? target.documentElement : target]); 40 | const observer = new MutationObserver(load); 41 | observer.observe(target, {subtree: true, childList: true}); 42 | return observer; 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uce-loader", 3 | "version": "2.0.0", 4 | "description": "A minimalistic, framework agnostic, lazy Custom Elements loader", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:es && npm run rollup:babel && npm run min && npm run fix:default", 8 | "cjs": "ascjs --no-default esm cjs", 9 | "rollup:es": "rollup --config rollup/es.config.js && sed -i.bck 's/^var /self./' es.js && rm -rf es.js.bck", 10 | "rollup:babel": "rollup --config rollup/babel.config.js && sed -i.bck 's/^var /self./' index.js && rm -rf index.js.bck", 11 | "min": "terser index.js --comments='/^!/' -c -m -o min.js", 12 | "fix:default": "sed -i 's/({})/({}).default/' index.js && sed -i 's/({})/({}).default/' es.js && sed -i 's/({})/({}).default/' min.js" 13 | }, 14 | "keywords": [ 15 | "custom", 16 | "elements", 17 | "loader", 18 | "auto", 19 | "lazy" 20 | ], 21 | "author": "Andrea Giammarchi", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@babel/core": "^7.11.6", 25 | "@babel/preset-env": "^7.11.5", 26 | "ascjs": "^4.0.1", 27 | "rollup": "^2.28.2", 28 | "rollup-plugin-babel": "^4.4.0", 29 | "rollup-plugin-node-resolve": "^5.2.0", 30 | "rollup-plugin-terser": "^7.0.2", 31 | "terser": "^5.3.2" 32 | }, 33 | "module": "./esm/index.js", 34 | "type": "module", 35 | "exports": { 36 | "import": "./esm/index.js", 37 | "default": "./cjs/index.js" 38 | }, 39 | "unpkg": "min.js", 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/WebReflection/uce-loader.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/WebReflection/uce-loader/issues" 46 | }, 47 | "homepage": "https://github.com/WebReflection/uce-loader#readme" 48 | } 49 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name 3 | const ignore = /^(?:annotation-xml|color-profile|font-face(?:|-format|-name|-src|-uri)|missing-glyph)$/i; 4 | const loaded = new Set; 5 | 6 | /** 7 | * @typedef {Object} Options 8 | * @prop {Node} [container=document] - where to monitor Custom Elements 9 | * @prop {function} on - a callback invoked per each new Custom Element 10 | */ 11 | 12 | /** 13 | * Start observing a document, or a specific container, and automatically 14 | * download once Custom Elements from a specific path. 15 | * @param {Options} options configuration options 16 | * @returns {MutationObserver} the disconnect-able `container` observer 17 | */ 18 | module.exports = (options) => { 19 | const target = options.container || document; 20 | const load = mutations => { 21 | for (let i = 0, {length} = mutations; i < length; i++) { 22 | for (let 23 | {addedNodes} = mutations[i], 24 | j = 0, {length} = addedNodes; j < length; j++ 25 | ) { 26 | const node = addedNodes[j]; 27 | const {children, getAttribute, tagName} = node; 28 | if (getAttribute) { 29 | const is = (getAttribute.call(node, 'is') || tagName).toLowerCase(); 30 | if (0 < is.indexOf('-') && !loaded.has(is) && !ignore.test(is)) { 31 | loaded.add(is); 32 | customElements.get(is) || options.on(is); 33 | } 34 | crawl(children); 35 | } 36 | } 37 | } 38 | }; 39 | const crawl = addedNodes => { load([{addedNodes}]) }; 40 | crawl([document == target ? target.documentElement : target]); 41 | const observer = new MutationObserver(load); 42 | observer.observe(target, {subtree: true, childList: true}); 43 | return observer; 44 | }; 45 | -------------------------------------------------------------------------------- /test/my-counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uce-template lazy load example 7 | 8 | 45 | 58 | 59 | 60 |
61 | 62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µce-loader 2 | 3 | **Social Media Photo by [Guillaume Bolduc](https://unsplash.com/@guibolduc) on [Unsplash](https://unsplash.com/)** 4 | 5 | A minimalistic, framework agnostic, lazy Custom Elements loader. 6 | 7 | ### Example 8 | 9 | ```html 10 | 11 | 12 | 13 | 14 | 15 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | ``` 43 | 44 | If `loader({container: document, on(tagName){}})` API is too simplified, feel free to check [lazytag](https://github.com/WebReflection/lazytag#readme) out. 45 | 46 | ### About ShadowDOM 47 | 48 | If your components use `attachShadow` and internally use custom elements that should be lazy loaded, be sure the `shadowRoot` is observed. 49 | 50 | ```js 51 | const shadowRoot = this.attachShadow({mode: any}); 52 | loader({ 53 | container: shadowRoot, 54 | on(newTag) { 55 | // ... load components 56 | } 57 | }); 58 | ``` 59 | 60 | ### V2 vs V1 61 | 62 | Current version of this module does *not* invoke the `.on(...)` method if the element is already registered as Custom Element. 63 | 64 | In *V1* any tag name would've passed through the loader instead. 65 | 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | self.uceLoader = (function (exports) { 2 | 'use strict'; 3 | 4 | // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name 5 | var ignore = /^(?:annotation-xml|color-profile|font-face(?:|-format|-name|-src|-uri)|missing-glyph)$/i; 6 | var loaded = new Set(); 7 | /** 8 | * @typedef {Object} Options 9 | * @prop {Node} [container=document] - where to monitor Custom Elements 10 | * @prop {function} on - a callback invoked per each new Custom Element 11 | */ 12 | 13 | /** 14 | * Start observing a document, or a specific container, and automatically 15 | * download once Custom Elements from a specific path. 16 | * @param {Options} options configuration options 17 | * @returns {MutationObserver} the disconnect-able `container` observer 18 | */ 19 | 20 | var index = (function (options) { 21 | var target = options.container || document; 22 | 23 | var load = function load(mutations) { 24 | for (var i = 0, length = mutations.length; i < length; i++) { 25 | for (var addedNodes = mutations[i].addedNodes, j = 0, _length = addedNodes.length; j < _length; j++) { 26 | var node = addedNodes[j]; 27 | var children = node.children, 28 | getAttribute = node.getAttribute, 29 | tagName = node.tagName; 30 | 31 | if (getAttribute) { 32 | var is = (getAttribute.call(node, 'is') || tagName).toLowerCase(); 33 | 34 | if (0 < is.indexOf('-') && !loaded.has(is) && !ignore.test(is)) { 35 | loaded.add(is); 36 | customElements.get(is) || options.on(is); 37 | } 38 | 39 | crawl(children); 40 | } 41 | } 42 | } 43 | }; 44 | 45 | var crawl = function crawl(addedNodes) { 46 | load([{ 47 | addedNodes: addedNodes 48 | }]); 49 | }; 50 | 51 | crawl([document == target ? target.documentElement : target]); 52 | var observer = new MutationObserver(load); 53 | observer.observe(target, { 54 | subtree: true, 55 | childList: true 56 | }); 57 | return observer; 58 | }); 59 | 60 | exports.default = index; 61 | 62 | return exports; 63 | 64 | }({}).default); 65 | --------------------------------------------------------------------------------