├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── css.js ├── example.js ├── guard.js ├── html ├── index.js └── raw.js ├── index.js ├── morph.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: node_js 3 | node_js: 4 | - lts/* 5 | deploy: 6 | provider: npm 7 | on: 8 | tags: true 9 | repo: hyperdivision/hui 10 | email: 11 | secure: 10a/5j+1PWsfy9aX0unfhR1u4XKrxBXxKKFvWqlE/kYNcTnhiCBnuDvwH4bsx11cojWMfLZURYBCadQE0aEP5YLeY/ZPtKz5natsaoVNX4WQxfJNHj8KZh362+5lyX3b7Btr+P/kp5Fv3E4IPkw6Rx5qysjSjuBbn4zIwT+X/IAWRl9byUds/Mqj24wYYE24fzHI61KIumFZ47t4CWnQZ++M69AYR4g4NsWZ2x8ls6BDA+mhVCA9md3SDpMvfr6NfhRw921gw+qzfekbRER/dxovQ4aWecYKr3t52RUmn6GiHVZsVsb9d62cf9HdWftI61o9VvSIn23wlsnS8uTc6BPlXSm0dlcQry/2adEP3j4R8ScS5tI21dtnQezxDusEX7EdQ/Gkt3lgkrL+hZ8Y1JGvX+na/f1NvCuwgXUeUG0Y/uAhUL/Li7wiPEI80Sk5pmisJqzFpK5FjazoTt5LwAUNyinVOlWPM8mnKl2IObe68zEx5NvrMoi5kIAAAfyw5znyZoAj9RVYzpjZxwvU7XaWr9o2D6ol++qGwBPpvP9JIOpu+dIlZNb4lxSCakQmKBCwo5fehcyZ4eHYlNxdf+Rdpww3+CiKgusLgW4M/Vrlh2ArpCYL+kZ9MBRnulhhd/MnvG+JGIVC65/HF1ZB9O6fL3/Tf1HuFFycX3fwg9k= 12 | api_key: 13 | secure: bBd5krFVfwG1ForNM+zNtr1AW6jFNTiyT3FLb0dPUxUbx4BRU4FS2EWE8Q68iYXMwO+i8MU00J6e5iLDYa1tYGy7898YhCCfS9ygDRtlb7GeRjp5QDNsISRa2iZ9dHgmsdc4+ekLEK29uZDW5Jc+4xlsqEOdCbpDUTVDXnlZW5p6qRdJc6cMqhjxy7Ky3oU6H9GX3bUfdohAUTXXvXc4gzbbmQHqTYo7k82RxbRyfQwSKBok2/p3eDzoxQEbdVIqdyFUD3WGal71OzxI+dP8Lba6FB457jatmluM2H/5Yvoyo1/5uFiOFVD5uH9X9rEheaKzO3n+8+U7pDUCKKKu7wPKcTuUv1z5/Fs4AClFa2+7bxJXsE/RyadwTldj5E4ftM+GId4t3vvfFmQf3qprmnO4/lQe8DgfhtDfwlgmpLUjnbhCOe+YRTdxITnF4/PWyCYCFkiZvMIcuXFQxRRkWxd266HKd+DNLlKRPAxXMK24UTDFjkleEDfYiSxA2ZUKZcZoLxwV0OylMdu6asS/ulW3r/Pgade5eicHi08B11FuYCWNEm7zjiMT22G6TYtz2du0nOVPDDyMJVbRhO2rf0nnCoGRQZbzr54RQgWxf9cHBtjh2o8n8JERTurA1KkOpyxjuxEHaDp2rV8zyTOZev+hRFQHT1W4IYRIL38mZV0= 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Hyperdivision ApS 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `hui` 2 | 3 | [![Build Status](https://travis-ci.org/hyperdivision/hui.svg?branch=master)](https://travis-ci.org/hyperdivision/hui) 4 | 5 | > Generic UI component class 6 | 7 | ## Usage 8 | 9 | ``` js 10 | const Component = require('hui') 11 | 12 | class View extends Component { 13 | constructor () { 14 | super() 15 | } 16 | 17 | // only called once on first .element access 18 | createElement () { 19 | return someDomElement 20 | } 21 | 22 | onload () { 23 | console.log('component loaded') 24 | this.on(someEmitter, 'eventName', () => { 25 | console.log('this event listener is auto removed on unload') 26 | }) 27 | } 28 | 29 | onunload () { 30 | console.log('component unloaded') 31 | } 32 | 33 | render () { 34 | console.log('you should update the rendering of your component here') 35 | console.log('called on the next raf tick when you call .update() debounced') 36 | this.element.someUpdates() 37 | } 38 | } 39 | ``` 40 | 41 | Or you can use the shorthand if you are not subclassing 42 | 43 | ``` js 44 | const view = new Component({ 45 | createElement () { 46 | return someDomElement 47 | }, 48 | onload () { 49 | console.log('component loaded') 50 | }, 51 | onunload () { 52 | console.log('component unloaded') 53 | }, 54 | render () { 55 | console.log('update the rendering') 56 | } 57 | }) 58 | ``` 59 | 60 | Call `component.update()` to trigger a rendering on the next raf. Multiple calls to `.update()` are automatically debounced. 61 | 62 | ### Morph components 63 | 64 | If your render method just involves reconstructing the entire dom of your element and then diffing it against the mounted one 65 | you can use a morph component as a conveinience. 66 | 67 | ``` js 68 | const Component = require('hui/morph') 69 | 70 | const el = new Component({ 71 | createElement () { 72 | return html`...` // this is called on every render 73 | } 74 | }) 75 | ``` 76 | 77 | ## API 78 | 79 | ### `const Component = require('hui')` 80 | 81 | Base component with manual rendering and hooks for load, unload and 82 | automatic event life cycle 83 | 84 | ### `const MorphComponent = require('hui/morph')` 85 | 86 | A auto morphing component using DOM diffing. 87 | 88 | ### `component.on(emitter, name, fn)` 89 | 90 | `addEventListener` / `on` helper that auto gc's the listener on unload. 91 | 92 | ### `component.off(emitter, name, fn)` 93 | 94 | `removeEventListener` / `off` helper that cancels out the above method. 95 | 96 | ### `component.loaded` 97 | 98 | Boolean wheather or not the component is currently loaded. 99 | 100 | ### `component.element` 101 | 102 | The DOM element attached. 103 | 104 | ### `component.update()` 105 | 106 | Trigger a render in the next animation frame. 107 | 108 | ### `const html = require('hui/html')` 109 | 110 | HTML with template strings 111 | 112 | ### `const raw = require('hui/html/raw')` 113 | 114 | Prevent escaping with template strings 115 | 116 | ### `const css = require('hui/css')` 117 | 118 | Inline css styles 119 | 120 | ### `const guard = require('hui/guard')` 121 | 122 | Protect a DOM subtree against morphing 123 | 124 | ## Install 125 | 126 | ```sh 127 | npm install hui 128 | ``` 129 | 130 | ## License 131 | 132 | [ISC](LICENSE) 133 | -------------------------------------------------------------------------------- /css.js: -------------------------------------------------------------------------------- 1 | module.exports = require('sheetify') 2 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const Component = require('./') 2 | const { EventEmitter } = require('events') 3 | 4 | let tick = 0 5 | const e = new EventEmitter() 6 | 7 | const c = new Component({ 8 | createElement () { 9 | return document.createElement('div') 10 | }, 11 | 12 | onload () { 13 | console.log('component loaded') 14 | 15 | this.on(e, 'tick', tick => { 16 | console.log('ontick') 17 | this.element.innerText = 'tick: ' + tick 18 | }) 19 | }, 20 | 21 | onunload () { 22 | console.log('got unloaded') 23 | } 24 | }) 25 | 26 | document.body.appendChild(c.element) 27 | 28 | setInterval(() => e.emit('tick', tick++), 100) 29 | setTimeout(() => document.body.removeChild(c.element), 1000) 30 | -------------------------------------------------------------------------------- /guard.js: -------------------------------------------------------------------------------- 1 | module.exports = require('nanomorph-guard') 2 | -------------------------------------------------------------------------------- /html/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('nanohtml') 2 | -------------------------------------------------------------------------------- /html/raw.js: -------------------------------------------------------------------------------- 1 | module.exports = require('nanohtml/raw') 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const onload = require('fast-on-load') 2 | 3 | const unloaders = Symbol('unloaders') 4 | const updating = Symbol('updating') 5 | const first = Symbol('first') 6 | const element = Symbol('element') 7 | 8 | module.exports = class Component { 9 | constructor (opts) { 10 | if (!opts) opts = {} 11 | 12 | this[element] = opts.element || null 13 | this[unloaders] = [] 14 | this[updating] = false 15 | this[first] = true 16 | 17 | this.loaded = false 18 | 19 | if (opts.onload) this.onload = opts.onload 20 | if (opts.onunload) this.onunload = opts.onunload 21 | if (opts.render) this.render = opts.render 22 | if (opts.createElement) this.createElement = opts.createElement 23 | } 24 | 25 | get element () { 26 | if (!this[element]) this[element] = this.createElement() 27 | 28 | if (this[element] && this[first]) { 29 | this[first] = false 30 | onload(this[element], 31 | this._onload.bind(this), 32 | this._onunload.bind(this), 33 | this.constructor 34 | ) 35 | } 36 | 37 | return this[element] 38 | } 39 | 40 | set element (el) { 41 | this[element] = el 42 | } 43 | 44 | on (emitter, name, fn) { 45 | this[unloaders].push([emitter, name, fn]) 46 | on(emitter, name, fn) 47 | } 48 | 49 | once (emitter, name, fn) { 50 | this[unloaders].push([emitter, name, fn]) 51 | once(emitter, name, fn) 52 | } 53 | 54 | off (emitter, name, fn) { 55 | off(emitter, name, fn) 56 | 57 | // swap and pop 58 | for (let i = 0; i < this[unloaders].length; i++) { 59 | const [e, n, f] = this[unloaders][i] 60 | if (e === emitter && n === name && f === fn) { 61 | this[unloaders][i] = this[unloaders][this[unloaders].length - 1] 62 | this[unloaders].pop() 63 | } 64 | } 65 | } 66 | 67 | update () { 68 | if (this[updating]) return 69 | this[updating] = true 70 | window.requestAnimationFrame(this._reallyUpdate.bind(this)) 71 | } 72 | 73 | _reallyUpdate () { 74 | if (!this[updating]) return 75 | this[updating] = false 76 | this.render() 77 | } 78 | 79 | createElement () { 80 | // overwrite me 81 | return null 82 | } 83 | 84 | render () { 85 | // overwrite me 86 | } 87 | 88 | onload () { 89 | // overwrite me 90 | } 91 | 92 | onunload () { 93 | // overwrite me 94 | } 95 | 96 | _onload () { 97 | this.loaded = true 98 | this.onload() 99 | // if any listeners were attached in onload we trigger a rerender as state 100 | // may have changed between createElement or unload/load 101 | if (this[unloaders].length) this.render() 102 | } 103 | 104 | _onunload () { 105 | this.loaded = false 106 | this[updating] = false 107 | 108 | const list = this[unloaders] 109 | 110 | while (list.length) { 111 | const [emitter, name, fn] = list.pop() 112 | off(emitter, name, fn) 113 | } 114 | 115 | this.onunload() 116 | } 117 | } 118 | 119 | function on (e, name, fn) { 120 | if (e.on) e.on(name, fn) 121 | else if (e.addEventListener) e.addEventListener(name, fn) 122 | } 123 | 124 | function off (e, name, fn) { 125 | if (e.off) e.off(name, fn) 126 | else if (e.removeEventListener) e.removeEventListener(name, fn) 127 | else e.removeListener(name, fn) 128 | } 129 | 130 | function once (e, name, fn) { 131 | if (e.once) { 132 | e.once(name, fn) 133 | } else if (e.addEventListener) { 134 | e.addEventListener(name, fn, { once: true }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /morph.js: -------------------------------------------------------------------------------- 1 | const Component = require('./') 2 | const morph = require('nanomorph') 3 | 4 | module.exports = class MorphComponent extends Component { 5 | render () { 6 | morph(this.element, this.createElement(), { childrenOnly: true }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hui", 3 | "version": "1.3.0", 4 | "description": "Generic UI Component class", 5 | "dependencies": { 6 | "nanohtml": "^1.8.0", 7 | "nanomorph": "^5.4.0", 8 | "nanomorph-guard": "^2.1.0", 9 | "fast-on-load": "^1.0.0", 10 | "sheetify": "^8.0.0" 11 | }, 12 | "devDependencies": { 13 | "standard": "^14.0.0", 14 | "dependency-check": "^4.0.0", 15 | "npm-run-all": "^4.1.5" 16 | }, 17 | "scripts": { 18 | "test": "run-s test:*", 19 | "test:deps": "dependency-check package.json morph.js guard.js css.js html/index.js html/raw.js --entry 'lib/*.js' --missing --unused --no-dev -i sheetify-nested -i sheetify-postcss", 20 | "test:lint": "standard" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/hyperdivision/hui.git" 25 | }, 26 | "keywords": [], 27 | "author": "Hyperdivision", 28 | "contributors": [ 29 | "Emil Bay ", 30 | "Mathias Buus " 31 | ], 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/hyperdivision/hui/issues" 35 | }, 36 | "homepage": "https://github.com/hyperdivision/hui#readme" 37 | } 38 | --------------------------------------------------------------------------------