├── docs ├── assets │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ └── search.js ├── interfaces │ ├── directive.html │ ├── directiveprocessoptions.html │ ├── basedescriptor.html │ ├── mountpointdescriptor.html │ └── tagdescriptor.html ├── index.html └── globals.html ├── src ├── enums.ts ├── index.ts ├── selector.ts ├── directives │ ├── propertyInjector.ts │ └── ref.ts ├── expression.ts ├── template.ts ├── proxy.ts ├── nodes.ts ├── component.ts └── interfaces.ts ├── examples ├── properties │ ├── pass-properties.html │ └── pass-properties.js ├── reactive.html ├── smiley-nomodule.html └── popup.html ├── test ├── ref-directive.js ├── karma.config.js ├── events.js ├── property-injector.js ├── properties.js └── creation.js ├── LICENSE ├── .gitignore ├── .npmignore ├── package.json ├── rollup.config.js ├── README.md └── tsconfig.json /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterthancode/ottavino/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterthancode/ottavino/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterthancode/ottavino/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betterthancode/ottavino/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /src/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * @ignore 4 | */ 5 | export enum SYMBOLS { 6 | NF, // not found 7 | IS, // invalid selector 8 | TM, // template_meta 9 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { component, registerDirective, ComponentHandler } from './component'; 2 | export { ExpressionResolution, parse } from './expression'; 3 | export { ComponentDescriptor, AttributeChangeHandler } from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/selector.ts: -------------------------------------------------------------------------------- 1 | import { SYMBOLS } from './enums'; 2 | 3 | /** 4 | * @internal 5 | * @ignore 6 | */ 7 | export const selector = (selector: string):HTMLElement|SYMBOLS => { 8 | try { 9 | const el = document.querySelector(selector); 10 | if (el) { 11 | return el; 12 | } else { 13 | return SYMBOLS.NF; 14 | } 15 | } catch { 16 | return SYMBOLS.IS; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /examples/properties/pass-properties.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/directives/propertyInjector.ts: -------------------------------------------------------------------------------- 1 | // @tsdoc-ignore 2 | 3 | type ProcessOptions = { 4 | targetNode: HTMLElement, 5 | attribute: Attr 6 | } 7 | 8 | /** 9 | * @ignore 10 | */ 11 | export default { 12 | attribute: (attr: any) => attr.nodeName.startsWith('[') && attr.nodeName.endsWith(']'), 13 | process: ({ targetNode, attribute }: ProcessOptions) => { 14 | const prop = attribute.nodeName.slice(1, -1); 15 | return (value: any) => { 16 | (targetNode as any)[prop] = value; 17 | }; 18 | }, 19 | registerAsGlobal: function (register: Function|undefined) { 20 | if (typeof register === 'function') { 21 | register(this); 22 | } else { 23 | (window).ottavino.registerDirective(this); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/expression.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * @ignore 4 | */ 5 | const stripCurlies = /(\{\{([^\{|^\}]+)\}\})/gi; 6 | 7 | /** 8 | * @internal 9 | * @ignore 10 | */ 11 | export type ExpressionResolution = { 12 | paths: string[]; 13 | expression: string | null; 14 | expressions: string[]; 15 | }; 16 | 17 | /** 18 | * @internal 19 | * @ignore 20 | */ 21 | export const parse = (expression: string): ExpressionResolution => { 22 | let match; 23 | let paths = []; 24 | const regexp = /(this\.[\w+|\d*]*)+/gi; 25 | while (match = regexp.exec(expression)) { 26 | paths.push(match[1]); 27 | } 28 | return { 29 | paths, 30 | expression, 31 | expressions: paths.length ? expression.match(stripCurlies) || [] : [] 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /test/ref-directive.js: -------------------------------------------------------------------------------- 1 | ottavinoDirectives.ref.registerAsGlobal(); 2 | 3 | describe('#ref Directive', () => { 4 | beforeEach(() => { 5 | document.body.innerHTML = ''; 6 | }); 7 | 8 | it('Should inject reference', done => { 9 | ottavino.component({ 10 | tag: 'ref-injector-1', 11 | shadow: true, 12 | template: /*html*/ ` 13 |
{{this.test}}
14 | `, 15 | properties: { 16 | test: 'passed' 17 | } 18 | }); 19 | const el = document.createElement('ref-injector-1'); 20 | document.body.appendChild(el); 21 | const div = el.shadowRoot.querySelector('#target-ref'); 22 | assert.strictEqual(div, el.ref.myDiv); 23 | assert.strictEqual(div, el.proxy.$.myDiv); 24 | done(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/karma.config.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env; 2 | console.log('ENV: ' + NODE_ENV); 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | frameworks: ['mocha', 'chai'], 7 | files: [ 8 | '../dist/index.nomodule.js', 9 | '../dist/directives/propertyInjector.nomodule.js', 10 | '../dist/directives/ref.nomodule.js', 11 | './**/*.js'], 12 | reporters: ['mocha'], 13 | port: 9876, 14 | colors: true, 15 | logLevel: config.LOG_INFO, 16 | browsers: ['ChromeHeadless', 'FirefoxHeadless'], 17 | autoWatch: NODE_ENV === 'development', 18 | singleRun: NODE_ENV !== 'development', 19 | concurrency: true, 20 | customLaunchers: { 21 | FirefoxHeadless: { 22 | base: 'Firefox', 23 | flags: ['-headless'] 24 | } 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/directives/ref.ts: -------------------------------------------------------------------------------- 1 | // @tsdoc-ignore 2 | 3 | import { DirectiveProcessOptions, Directive } from '../interfaces'; 4 | 5 | // @tsdoc-ignore 6 | 7 | /** 8 | * @ignore 9 | */ 10 | export default { 11 | attribute: (attr: any) => attr.nodeName === '#ref', 12 | process: ({ 13 | componentNode, 14 | componentHandler, 15 | targetNode, 16 | attribute 17 | }: DirectiveProcessOptions) => { 18 | const key = attribute.nodeValue as string; 19 | (componentHandler).$ = (componentNode).ref = { 20 | ...((componentNode).ref || {}), 21 | [key]: targetNode 22 | }; 23 | }, 24 | registerAsGlobal: function(register: Function | undefined) { 25 | if (typeof register === 'function') { 26 | register(this); 27 | } else { 28 | (window).ottavino.registerDirective(this); 29 | } 30 | } 31 | } as Directive; 32 | -------------------------------------------------------------------------------- /test/events.js: -------------------------------------------------------------------------------- 1 | describe('Event Handling', () => { 2 | let wasClicked = 0; 3 | before(() => { 4 | ottavino.component({ 5 | tag: 'test-click-event', 6 | shadow: true, 7 | template: /*html*/` 8 |

CLICK THE TEXT

9 |

This is the paragraph

10 | `, 11 | this: { 12 | captureClick: function () { 13 | wasClicked++; 14 | } 15 | } 16 | }); 17 | }); 18 | beforeEach(() => document.body.innerHTML = ''); 19 | it('Should capture clicks', done => { 20 | const el = document.createElement('test-click-event'); 21 | document.body.appendChild(el); 22 | el.shadowRoot.querySelector('p').click(); 23 | assert.equal(1, wasClicked); 24 | el.shadowRoot.querySelector('p').click(); 25 | assert.equal(2, wasClicked); 26 | done(); 27 | }); 28 | }); -------------------------------------------------------------------------------- /test/property-injector.js: -------------------------------------------------------------------------------- 1 | ottavinoDirectives.propertyInjector.registerAsGlobal(); 2 | 3 | describe('Property Injector Directive', () => { 4 | beforeEach(() => { 5 | document.body.innerHTML = ''; 6 | }); 7 | 8 | it('Should inject properties', (done) => { 9 | ottavino.component({ 10 | tag: 'prop-injector-1', 11 | shadow: true, 12 | template: /*html*/` 13 |
{{this.test}}
14 | `, 15 | properties: { 16 | test: 'passed' 17 | }, 18 | }); 19 | const el = document.createElement('prop-injector-1'); 20 | document.body.appendChild(el); 21 | const div = el.shadowRoot.querySelector('#pass-prop'); 22 | assert.equal('passed', div.test); 23 | el.proxy.test = 'passed again'; 24 | assert.equal('passed again', div.test); 25 | done(); 26 | }); 27 | }); -------------------------------------------------------------------------------- /examples/reactive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 30 | 31 | -------------------------------------------------------------------------------- /examples/smiley-nomodule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ottavino Demo 8 | 9 | 10 | 11 | 18 |

ottavino Demo

19 |

No module (iife) "hello" component

20 |

Insert any text within the shadow DOM

21 | ottavino 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /src/template.ts: -------------------------------------------------------------------------------- 1 | import { selector } from './selector'; 2 | 3 | /** 4 | * @internal 5 | * @ignore 6 | */ 7 | export type SelectorResult = HTMLTemplateElement | DocumentFragment; 8 | 9 | /** 10 | * @internal 11 | * @ignore 12 | */ 13 | export const template = (raw: string|HTMLTemplateElement): HTMLTemplateElement => { 14 | if (raw instanceof HTMLTemplateElement) { 15 | return raw; 16 | } else { 17 | const templateElement = document.createElement('template'); 18 | templateElement.innerHTML = raw; 19 | return templateElement; 20 | } 21 | }; 22 | 23 | /** 24 | * @internal 25 | * @ignore 26 | */ 27 | export const getMountPoint = (raw: string | HTMLElement) => { 28 | if (raw instanceof HTMLElement) { 29 | return raw; 30 | } 31 | const result = selector(raw); 32 | if (result instanceof HTMLElement) { 33 | return result 34 | } else { 35 | return template(raw); 36 | } 37 | } -------------------------------------------------------------------------------- /src/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * @internal 4 | */ 5 | export const createProxy = (target: any, update: Function, prefix = '') => { 6 | const store: any = {}; 7 | return [ 8 | store, 9 | new Proxy(target, { 10 | get: (t, key) => { 11 | if (!store.hasOwnProperty(key)) { 12 | if (typeof target[key] === 'object' && !(target[key] instanceof HTMLElement)) { 13 | store[key] = createProxy( 14 | target[key], 15 | update, 16 | prefix + key + '.' 17 | )[1]; 18 | store[key].__prefix = prefix; 19 | } else { 20 | store[key] = target[key]; 21 | } 22 | } 23 | return store[key]; 24 | }, 25 | set: (t, key, v) => { 26 | if (typeof v === 'object') { 27 | store[key] = createProxy(v, update, prefix + key + '.')[1]; 28 | store[key].__prefix = prefix; 29 | } else { 30 | store[key] = v; 31 | } 32 | update(prefix + key); 33 | return true; 34 | } 35 | }) 36 | ]; 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 betterthancode, Avichay Eyal 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | dist 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | .rpt* 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test 3 | docs 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | .rpt* 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /examples/properties/pass-properties.js: -------------------------------------------------------------------------------- 1 | import { component, registerDirective } from '../../dist/index.js'; 2 | 3 | import propertiesDirectives from '../../dist/directives/propertyInjector.js'; 4 | propertiesDirectives.registerAsGlobal(registerDirective); 5 | 6 | component({ 7 | tag: 'parent-component', 8 | shadow: true, 9 | directives: [], 10 | template: /*html*/ ` 11 |

This is the number: {{this.myNumber}} {{this.myName}}

12 | 13 | 14 | 15 | 16 | `, 17 | init: function() { 18 | }, 19 | properties: { 20 | myNumber: 0, 21 | myName: 'ottavino' 22 | } 23 | }); 24 | 25 | component({ 26 | tag: 'child-component', 27 | template: /*html*/` 28 |

This is the [othernumber] {{this.othernumber}}

29 | `, 30 | properties: { 31 | othernumber: 0 32 | }, 33 | attributes: { 34 | pass: function (value, oldValue) { 35 | console.log(value, oldValue); 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ottavino", 3 | "version": "0.2.4", 4 | "description": "tiny, fast and declarative custom elements library (but not only custom elements)", 5 | "main": "dist/index.js", 6 | "iife": "dist/index.nomodule.js", 7 | "scripts": { 8 | "test": "karma start test/karma.config.js", 9 | "test-dev": "NODE_ENV=development karma start test/karma.config.js", 10 | "build": "rollup -c", 11 | "dev": "NODE_ENV=development rollup -c -w", 12 | "prepublishOnly": "npm run build && npm run test && npm run docs", 13 | "docs": "typedoc --out docs src --excludeNotExported --mode file" 14 | }, 15 | "author": "Avichay Eyal ", 16 | "license": "MIT", 17 | "repository": "https://github.com/betterthancode/ottavino/", 18 | "devDependencies": { 19 | "karma-firefox-launcher": "^1.1.0", 20 | "karma-safari-launcher": "^1.0.0", 21 | "@microsoft/tsdoc": "^0.12.9", 22 | "typedoc": "^0.14.2", 23 | "chai": "^4.2.0", 24 | "karma": "^4.1.0", 25 | "karma-chai": "^0.1.0", 26 | "karma-chrome-launcher": "^2.2.0", 27 | "karma-mocha": "^1.3.0", 28 | "karma-mocha-reporter": "^2.2.5", 29 | "mocha": "^6.1.4", 30 | "rollup": "^1.12.1", 31 | "rollup-plugin-terser": "^4.0.4", 32 | "rollup-plugin-typescript2": "^0.21.0", 33 | "typescript": "^3.4.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/properties.js: -------------------------------------------------------------------------------- 1 | describe('Custom Element Properties propogation', () => { 2 | beforeEach(() => { 3 | document.body.innerHTML = ''; 4 | }); 5 | it('Should propagate property and update template', () => { 6 | ottavino.component({ 7 | tag: 'prop-test-1', 8 | template: /*html*/ ` 9 |
Value is {{this.magicNumber}}
10 |
Value is {{this.nested.name}}
11 | 12 | `, 13 | properties: { 14 | magicNumber: 0, 15 | nested: { 16 | name: 'ottavino' 17 | } 18 | }, 19 | shadow: true 20 | }); 21 | document.body.appendChild(document.createElement('prop-test-1')); 22 | const el = document.querySelector('prop-test-1'); 23 | const div = el.shadowRoot.querySelector('#prop-test-1-div'); 24 | const nestedEl = el.shadowRoot.querySelector('#nested'); 25 | const btn = el.shadowRoot.querySelector('button'); 26 | assert.equal('Value is 0', div.innerText); 27 | el.magicNumber = 42; 28 | assert.equal('Value is 42', div.innerText); 29 | btn.click(); 30 | assert.equal('Value is 10', div.innerText); 31 | el.nested = { 32 | name: 'Test Passed' 33 | }; 34 | assert.equal('Value is Test Passed', nestedEl.innerText); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import pkg from './package.json'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | 6 | const defaultPlugins = [ 7 | typescript({ 8 | typescript: require('typescript') 9 | }) 10 | ]; 11 | 12 | if (process.env.NODE_ENV !== 'development') { 13 | defaultPlugins.push(terser()); 14 | } 15 | 16 | 17 | const common = { 18 | external: [ 19 | ...Object.keys(pkg.dependencies | {}), 20 | ...Object.keys(pkg.peerDependencies || {}) 21 | ], 22 | plugins: defaultPlugins 23 | } 24 | 25 | const directiveConfig = (name) => { 26 | return { 27 | input: 'src/directives/' + name + '.ts', 28 | output: [ 29 | { 30 | file: 'dist/directives/' + name + '.js', 31 | format: 'es' 32 | }, 33 | { 34 | file: 'dist/directives/' + name + '.nomodule.js', 35 | format: 'iife', 36 | name: 'ottavinoDirectives.' + name 37 | }, 38 | ], 39 | ...common 40 | }; 41 | } 42 | 43 | const config = { 44 | ottavino: { 45 | input: 'src/index.ts', 46 | output: [ 47 | { 48 | file: pkg.main, 49 | format: 'es' 50 | }, 51 | { 52 | file: pkg.iife, 53 | format: 'iife', 54 | name: 'ottavino' 55 | } 56 | ], 57 | ...common 58 | } 59 | }; 60 | 61 | const module = [ 62 | config.ottavino, 63 | directiveConfig('propertyInjector'), 64 | directiveConfig('ref') 65 | ] 66 | 67 | 68 | 69 | export default module; -------------------------------------------------------------------------------- /test/creation.js: -------------------------------------------------------------------------------- 1 | describe('Creation of components', () => { 2 | beforeEach(() => { 3 | document.body.innerHTML = ''; 4 | }); 5 | describe('Should define a custom element', () => { 6 | it('with shadow DOM', () => { 7 | ottavino.component({ 8 | tag: 'hello-world-with-shadow', 9 | template: '

With Shadow

', 10 | shadow: true 11 | }); 12 | document.body.appendChild( 13 | document.createElement('hello-world-with-shadow') 14 | ); 15 | const el = document.querySelector('hello-world-with-shadow'); 16 | const shadow = el.shadowRoot; 17 | const inner = shadow.querySelector('h1'); 18 | assert(el instanceof HTMLElement); 19 | assert(!(el instanceof HTMLUnknownElement)); 20 | assert.equal('With Shadow', inner.innerText); 21 | }); 22 | 23 | it('without shadow DOM', done => { 24 | ottavino.component({ 25 | tag: 'hello-world-no-shadow', 26 | template: '

No Shadow

', 27 | shadow: false 28 | }); 29 | document.body.appendChild( 30 | document.createElement('hello-world-no-shadow') 31 | ); 32 | setTimeout(() => { 33 | // using timeout to push beyond the no-shadow render microtask 34 | const el = document.querySelector('hello-world-no-shadow'); 35 | const shadow = el.shadowRoot; 36 | const inner = el.querySelector('h1'); 37 | assert(el instanceof HTMLElement); 38 | assert(!(el instanceof HTMLUnknownElement)); 39 | assert.notExists(shadow); 40 | assert.equal('No Shadow', inner.innerText); 41 | done(); 42 | }, 20); 43 | }); 44 | }); 45 | 46 | describe('Should upgrade a legacy element', () => { 47 | it('with shadow DOM', () => { 48 | const div = document.createElement('div'); 49 | document.body.appendChild(div); 50 | ottavino.component({ 51 | mount: div, 52 | template: '

With Shadow

', 53 | shadow: true 54 | }); 55 | const el = document.querySelector('div'); 56 | const shadow = el.shadowRoot; 57 | const inner = shadow.querySelector('h1'); 58 | assert(el instanceof HTMLElement); 59 | assert(!(el instanceof HTMLUnknownElement)); 60 | assert.equal('With Shadow', inner.innerText); 61 | }); 62 | 63 | it('without shadow DOM', done => { 64 | const div = document.createElement('div'); 65 | document.body.appendChild(div); 66 | ottavino.component({ 67 | mount: div, 68 | template: '

No Shadow

' 69 | }); 70 | setTimeout(() => { 71 | // using timeout to push beyond the no-shadow render microtask 72 | const el = document.querySelector('div'); 73 | const shadow = el.shadowRoot; 74 | const inner = el.querySelector('h1'); 75 | assert(el instanceof HTMLElement); 76 | assert(!(el instanceof HTMLUnknownElement)); 77 | assert.notExists(shadow); 78 | assert.equal('No Shadow', inner.innerText); 79 | done(); 80 | }, 20); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ottavino 2 | > The piccolo /ˈpɪkəloʊ/ (Italian pronunciation: [ˈpikkolo]; Italian for "small", but named ottavino in Italy) 3 | 4 | ![](https://user-images.githubusercontent.com/1084459/57871139-55c10100-7811-11e9-8499-cbb4cfafb315.png) 5 | 6 | ## Tiny, Fast and Declarative User Interface Development 7 | Using native custom elements API (but not only) 8 | 9 | [![Build Status](https://semaphoreci.com/api/v1/eavichay/ottavino/branches/master/badge.svg)](https://semaphoreci.com/eavichay/ottavino) 10 | 11 | 12 | As simple as it gets. 13 | 14 | ### [See the docs](https://betterthancode.github.io/ottavino/globals.html) 15 | 16 | ```typescript 17 | import { component } from 'ottavino' 18 | 19 | component({ 20 | tag: 'tiny-moji', 21 | template: '{{this.moodIcon}}', 22 | shadow: true, // optional shadow DOM 23 | properties: { 24 | moodIcon: '😃' 25 | }, 26 | attributes: { 27 | mood: function(newValue /*, oldValue, domElement */) { 28 | this.moodIcon = newValue === 'sad' ? '😢' : '😃' 29 | } 30 | } 31 | }) 32 | ``` 33 | ```html 34 | 35 | 36 | ``` 37 | 38 | IIFE Version (no es-modules) 39 | ```html 40 | 41 | ``` 42 | ```javascript 43 | window.ottavino.component({ 44 | // here you go 45 | }); 46 | ``` 47 | 48 | ## Footprint (KBGzipped) ~1.5 49 | Small: ![npm bundle size](https://img.shields.io/bundlephobia/minzip/ottavino?label=bundle%20size) 50 | 51 | ## API 52 | `component(options: ComponentDescriptor) => ComponentProxy` 53 | 54 | ### ComponentDescriptor 55 | - **tag**? When creating a custom element, this will be the DOM tag name. 56 | - **mount**? When you want to "upgrade" a legacy element on the DOM (like a `
`) and make it behave just like it was a custom element. It can be a queryselector to be executed on the document or a reference to an element instance 57 | - If no `tag` nor `mount` are used (or both used) - expect dragons. 58 | - **template**: string (or reference for an existing HTML template element for reuse) 59 | - **properties**? key-value properties that are reflected from the DOM element into the component handler ("proxy") 60 | - **attributes**? key-value of functions handling attribute changes. Functions receives the new and old values and a reference to the DOM element. `this` will refer to the handler 61 | - **init**? Initialization function during component construction. The passed argument is the DOM reference 62 | - **this**? Your (optional) component handler. Anything can go here. From within the template, and usage of `this` would reach this object. If none defined, it will generate a default component proxy 63 | - **shadow**? opt in for shadow DOM 64 | - **closed**? opt in for **closed** mode shadow DOM 65 | - **connectedCallback**? linked with the DOM connectedCallback 66 | - **disconnectedCallback**? linked with the DOM disconnectedCallback 67 | 68 | ### Under the hood 69 | ```html 70 | {{this.counter}} 71 | 72 | 73 |