├── .gitignore ├── package.json ├── LICENSE ├── index.html ├── template.js ├── README.md ├── template.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template", 3 | "version": "1.0.0", 4 | "description": "Lightweight UI Framework", 5 | "main": "template.js", 6 | "scripts": { 7 | "build": "tsc" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "^20.5.4", 11 | "typescript": "^5.2.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright William Blankenship 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Neither the name of the copyright holder nor the names of its 7 | contributors may be used to endorse or promote products derived from 8 | this software without specific prior written permission. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL 11 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 12 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 13 | FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 14 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 15 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 16 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /template.js: -------------------------------------------------------------------------------- 1 | class Template { 2 | constructor(element, style) { 3 | this.fragment = element; 4 | this.style = style; 5 | this.host = null; 6 | this.eventHandlers = {}; 7 | this.elements = {}; 8 | this.children = {}; 9 | this.state = {}; 10 | this.destroyed = false; 11 | } 12 | getElement(selector) { 13 | const element = this.elements[selector]; 14 | if (element != undefined) { 15 | return element; 16 | } 17 | if (this.host == undefined || this.host.shadowRoot == undefined) { 18 | throw new Error("Template has not been mounted"); 19 | } 20 | const result = this.host.shadowRoot.querySelector(selector); 21 | if (result == undefined) { 22 | throw new Error(`Element ${selector} not found`); 23 | } 24 | if (!(result instanceof HTMLElement)) { 25 | throw new Error(`${selector} did not return an HTMLElement`); 26 | } 27 | this.elements[selector] = result; 28 | return result; 29 | } 30 | setState(obj) { 31 | let changed = false; 32 | for (let key in obj) { 33 | if (this.state[key] !== obj[key]) { 34 | const value = obj[key]; 35 | changed = true; 36 | if (value == undefined) { 37 | delete this.state[key]; 38 | } 39 | else { 40 | this.state[key] = value; 41 | } 42 | } 43 | } 44 | if (changed) { 45 | this.emit("change", this.state); 46 | } 47 | return this; 48 | } 49 | removeChild(name) { 50 | const child = this.children[name]; 51 | if (child == undefined) { 52 | return this; 53 | } 54 | child.unmount(); 55 | delete this.children[name]; 56 | return this; 57 | } 58 | addChild(selector, child) { 59 | if (this.children[selector] != undefined) { 60 | throw new Error("Child already mounted"); 61 | } 62 | const element = this.getElement(selector); 63 | child.mount(element); 64 | this.children[selector] = child; 65 | return this; 66 | } 67 | addChildren(obj) { 68 | for (let query in obj) { 69 | const child = obj[query]; 70 | if (child == undefined) { 71 | continue; 72 | } 73 | this.addChild(query, child); 74 | } 75 | return this; 76 | } 77 | getChild(query) { 78 | const child = this.children[query]; 79 | if (child == undefined) { 80 | throw new Error(`Unknown child ${query}`); 81 | } 82 | return child; 83 | } 84 | mount(host) { 85 | if (this.host) { 86 | throw new Error("Already mounted"); 87 | } 88 | this.host = host; 89 | this.host.innerText = ""; 90 | if (!this.host.shadowRoot) { 91 | this.host.attachShadow({ mode: "open" }); 92 | } 93 | if (!this.host.shadowRoot) { 94 | throw new Error("Failed to create shadow root"); 95 | } 96 | this.host.shadowRoot.appendChild(this.style); 97 | this.host.shadowRoot.appendChild(this.fragment.content.cloneNode(true)); 98 | return this; 99 | } 100 | unmount() { 101 | for (const query in this.children) { 102 | const child = this.children[query]; 103 | if (child == undefined) { 104 | continue; 105 | } 106 | child.unmount(); 107 | } 108 | if (this.host) { 109 | this.host.innerText = ""; 110 | if (this.host.shadowRoot) { 111 | this.host.shadowRoot.innerHTML = ""; 112 | } 113 | } 114 | this.host = null; 115 | this.eventHandlers = {}; 116 | this.elements = {}; 117 | this.children = {}; 118 | this.state = {}; 119 | this.destroyed = true; 120 | return; 121 | } 122 | on(event, handler) { 123 | const handlers = this.eventHandlers[event] || []; 124 | this.eventHandlers[event] = handlers; 125 | handlers.push(handler); 126 | return this; 127 | } 128 | emit(event, ...args) { 129 | const handlers = this.eventHandlers[event]; 130 | if (handlers == undefined) { 131 | return this; 132 | } 133 | handlers.forEach((handler) => { 134 | handler(...args); 135 | }); 136 | return this; 137 | } 138 | static createElement(html) { 139 | const template = document.createElement("template"); 140 | template.innerHTML = html; 141 | return template; 142 | } 143 | static createStyle(css) { 144 | const style = document.createElement("style"); 145 | style.textContent = css; 146 | return style; 147 | } 148 | } 149 | export default Template; 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Template 2 | 3 | Template is a simple JS framework for creating interactive applications. 4 | 5 | It focuses on using web-native patterns. 6 | 7 | Calling it a framework is a bit of an exaggeration, it's a single `class` that 8 | manages HTML `