From 0438bb19528a038f87feec27398df3f89e992f2f Mon Sep 17 00:00:00 2001 From: lelanthran Date: Sun, 6 Apr 2025 08:56:03 +0200 Subject: [PATCH] Adds ZjsComponent files and docs. --- LICENSE | 13 +- ZjsComponent/README.md | 196 +++++++++++++++++++++++++++++ ZjsComponent/zjs-component.html | 61 +++++++++ ZjsComponent/zjs-component.js | 96 ++++++++++++++ ZjsComponent/zjs-component.txt | 64 ++++++++++ ZjsComponent/zjsc/component-1.zjsc | 40 ++++++ 6 files changed, 466 insertions(+), 4 deletions(-) create mode 100644 ZjsComponent/README.md create mode 100644 ZjsComponent/zjs-component.html create mode 100644 ZjsComponent/zjs-component.js create mode 100644 ZjsComponent/zjs-component.txt create mode 100644 ZjsComponent/zjsc/component-1.zjsc diff --git a/LICENSE b/LICENSE index 3398413..1e3434b 100644 --- a/LICENSE +++ b/LICENSE @@ -11,11 +11,16 @@ furnished to do so, subject to the following conditions: 1. The Software, including any part thereof, shall not be used, in whole or in part, for the purpose of training machine learning or artificial -intelligence models, whether commercial, research, or otherwise. This -includes, but is not limited to, large language models, computer vision -systems, and other data-driven model architectures. + intelligence models, whether commercial, research, or otherwise. This + includes, but is not limited to, large language models, computer vision + systems, and other data-driven model architectures. -2. The above copyright notice and this permission notice shall be included in +2. Usage of The Software, or any part thereof, for the purpose of training + machine learning or artificial intelligence models, whether commercial, + research or others, is liable to a per-user royalty based on the number of + users of the aforesaid models. + +3. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR diff --git a/ZjsComponent/README.md b/ZjsComponent/README.md new file mode 100644 index 0000000..1d14787 --- /dev/null +++ b/ZjsComponent/README.md @@ -0,0 +1,196 @@ +# ZjsComponent + +**ZjsComponent** is a lightweight, zero-dependency Web Component for building +modular, reusable front-end UI components. It allows dynamic loading of +HTML+JS fragments with local script scoping, simple lifecycle hooks, and +isolated DOM composition without needing a full framework. + +A single component is a fragment of valid HTML that contains: + +1. Zero or more HTML elements, +2. Zero or more ` +``` + +You can load it from your server or bundle it with your app. + +--- + +## 🧩 Usage + +To use a `zjs-component`, place the custom tag in your HTML and set the +`remote-src` attribute to point to an external `.zjsc` HTML fragment. +All attributes are passed to the fragment script as component attributes. + +> The `display=...` attribute is special: it is used to set the +> `style.display` of the element, allowing the caller/user of the component to +> set the display to `inline`, `block`, `inline-block`, `none`, etc. + + +```html + + +``` + +> **Note** to ease development, change your editor/IDE settings to treat +> `.zjsc` files exactly the same as it does `.html` files. You definitely want +> this so that your editor/IDE does all the correct syntax highlighting, +> autocompletion and code-formatting for `.zjsc` files that it does for +> `.html` files. + +--- + +## 📦 Fragment Structure (`.zjsc` file) + +Each remote fragment may contain: + +- Any HTML content +- Multiple ` +``` + +--- + +## 🧠 Lifecycle Hooks + +The following optional functions can be defined in the fragment script: + +- **`onConnected()`** called after the fragment loads and scripts are bound +- **`onDisconnected()`** called when the component is removed from the DOM + +These functions, like all functions defined in the script element within a +.zjsc` page, have `this` bound to the `zjs-component` instance. + +--- + +## 📡 Calling Component Methods + +Use the global `ZjsComponent.send()` function to call exported methods from +within the fragment: + +```html + +``` + +You can also invoke methods from outside the component: + +```js +ZjsComponent.send("#my-component", "someMethod", arg1, arg2); +``` + +Where: +- First argument: selector, DOM node, or internal instance ID +- Second argument: name of the exported method +- Remaining arguments: passed to the method + +Within methods the `this` variable works as you would expect, referencing the +current instance of the component. + +--- + +## 🔍 Debugging + +If the component has a `debug` attribute, its internal script `exports` object +will be accessible as `window.__zjsDebugClosure` in the console. + +```html + +``` + +--- + +## 🔐 Security Note + +ZjsComponent uses `new Function()` to execute remote scripts, so only load +fragments from **trusted sources**. Avoid including user-generated content. + +--- + +## ✅ Features Summary + +- ✅ Load reusable HTML+JS fragments into any page +- ✅ DOM isolation (children stay inside component tag) +- ✅ Lifecycle hooks (`onConnected`, `onDisconnected`) +- ✅ Method calling via `ZjsComponent.send()` +- ✅ Script scoping per fragment +- ✅ Pass attributes as parameters + +--- + +## 🚫 Limitations + +- No reactive state (manual DOM updates) +- No Shadow DOM or scoped styles (yet) +- Breakpoints in DevTools may behave oddly due to dynamic script loading + +--- + +## 📄 License + +[MIT4H License](../LICENSE) + diff --git a/ZjsComponent/zjs-component.html b/ZjsComponent/zjs-component.html new file mode 100644 index 0000000..7bed3c4 --- /dev/null +++ b/ZjsComponent/zjs-component.html @@ -0,0 +1,61 @@ + + + + + JSON-HTML Test + + + + + + + + + + + + + + + + + +
+ +
+ LHS Menu stuff +
+ + +
+ +
+ + + +
+ + + +
+
+ + + + + + \ No newline at end of file diff --git a/ZjsComponent/zjs-component.js b/ZjsComponent/zjs-component.js new file mode 100644 index 0000000..db76963 --- /dev/null +++ b/ZjsComponent/zjs-component.js @@ -0,0 +1,96 @@ + +class ZjsComponent extends HTMLElement { + + static _instanceCount = 0; + static _instances = new Map(); + + static send(objOrId, method, ...args) { + const instance = (objOrId instanceof Number) + ? ZjsComponent._instances.get(objOrId) + : (objOrId instanceof String) + ? document.querySelector(objOrId) + : objOrId.closest("zjs-component"); + return instance[method](...args); + } + + constructor() { + super(); + this.instanceCount = ZjsComponent._instanceCount++; + ZjsComponent._instances.set(this.instanceCount, this); + } + + disconnectedCallback() { + if (typeof this["onDisconnected"] === "function") { + this["onDisconnected"](); + } + ZjsComponent._instances.delete(this.instanceCount); + } + + async connectedCallback() { + let remoteSrc = this.getAttribute("remote-src"); + if (!remoteSrc) return; + + if (this.hasAttribute("display")) { + this.style.display = this.getAttribute("display"); + } + + let response = await fetch(remoteSrc); + let htmlText = await response.text(); + + let tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlText; + + this.extractAndExecuteScripts(tempDiv); + + Array.from(tempDiv.children).forEach(child => this.appendChild(child)); + } + + extractAndExecuteScripts(fragment) { + let scripts = fragment.querySelectorAll("script"); + let scriptContent = ""; + + scripts.forEach(script => { + scriptContent += script.textContent + "\n"; + script.remove(); + }); + + const myClosure = this.executeInClosure(scriptContent); + for (let key in myClosure) { + this[key] = myClosure[key]; + } + if (typeof this["onConnected"] === "function") { + this["onConnected"](); + } + } + + executeInClosure(scriptContent) { + try { + let closureFunction = new Function("exports", ` + (function () { + try { + ${scriptContent} + } catch (error) { + console.error("Runtime error in ZjsComponent.executeInClosure:", error); + } + })(); + return exports; + //# sourceURL=zjs-component-${this.getAttribute("remote-src")} + `); + + let closure = closureFunction({}); + + if (this.hasAttribute("debug")) { + window.__zjsDebugClosure = closure; + console.log("Debug mode: myClosure available at window.__zjsDebugClosure"); + } + + return closure; + } catch (error) { + console.error("Syntax error in fragment script:", error); + return {}; + } + } + +} + +customElements.define("zjs-component", ZjsComponent); diff --git a/ZjsComponent/zjs-component.txt b/ZjsComponent/zjs-component.txt new file mode 100644 index 0000000..21cafe8 --- /dev/null +++ b/ZjsComponent/zjs-component.txt @@ -0,0 +1,64 @@ +Development notes + +I've used a `` component for the last two years. +It replaces itself with the content at remote-src verbatim. Scripts within +that page are executed, style tags are respected, etc. + +It has proved very useful for client-side includes. This is a brainstorm for a +similar mechanism to do isolated components in a webpage. There are a plethora +of problems with trying to use zjs-include for reusable components, some of +which are: +1. No scoping - elements in the HTML fragment cannot have id attributes + specified, because the id might conflict with another element included by a + different zjs-include tag. +2. Code executed in that snippet's script tags is not scoped either - + declaring a variable and loading that fragment twice (for example, by + navigating away to another fragment, then navigating back, while staying on + the same actual page) causes errors because the variable is already + declared. +3. No scoping of DOM elements - I cannot write a function to perform a + querySelector only on that snippet. +4. No way to pass parameters to the fragment that is loaded, which is + necessary sometimes. +5. No way for a fragment to receive parameters. + +Some of these have crude and unscalable workarounds: +1. For #4 and #5 I have used URL parameters, but these are clunky and + require the script tag to decode the URL parameters each time it is run. + Sometimes I store values in a globalVar variable that the fragment will + check. This is also unworkable. +2. For #2, I ensure that everything in a script tag in the fragment is + wrapped in an anonymous function (I believe it's called a closure - confirm + if I am correct or not) that runs immediately (() => { ... }) ();. This is + also not good for many reasons, which include having to do window.clickfunc + = clickfunc for functions that are mentioned in an onclick attribute on a + button or similar. +3. For #1 and #3 I sometimes use attribute name= and use a known element + on the page with el.closest("name=...") to retrieve a fragment-level + element. + +The goal is a zjs-component element that works very similar to zjs-include: + +1. The element won't replace itself with the remote fragment, it will add + it as children elements. This solves the DOM scoping problems, because the + code in the fragment can do el.closest("zjs-component"). +2. By using an attribute for display, the element can be either inline, + block, block-inline, etc, which allows usage of the component as a block + display, or inline display, etc. +3. When scripts are executed on the fragment, they will be executed in a + closure that is stored. This allows the fragment to have "methods" like a + normal object, so that onCreate() defined in the fragment will result in an + .onCreate() method in the closure. +4. Executing scripts in this way also allows automatic calling of functions + named (for example) fragmentConstructor() and fragmentDestructor() to serve + as a constructor function (when the page is loaded) and a destructor + function (when the element is removed from the DOM). +5. Passing of parameters to the fragment's constructor can be done using + attributes. For example, any zjs-component attribute that is not display + can be used to set values in the closure. +6. I am also considering a pub/sub mechanism to allow logic in fragments to + execute when some other component generates a message. A simply pub/sub + implemented using customEvent is probably sufficient to do this. + +The long and short is: a fragment of HTML that can serve as an instance of an +object, recognising `this` in code snippets. diff --git a/ZjsComponent/zjsc/component-1.zjsc b/ZjsComponent/zjsc/component-1.zjsc new file mode 100644 index 0000000..9cc0709 --- /dev/null +++ b/ZjsComponent/zjsc/component-1.zjsc @@ -0,0 +1,40 @@ +
+ Show counter + +
+ +
+ Increment counter + +
+
+
+ + \ No newline at end of file