Adds ZjsComponent files and docs.

This commit is contained in:
lelanthran 2025-04-06 08:56:03 +02:00
parent d334984902
commit 0438bb1952
6 changed files with 466 additions and 4 deletions

13
LICENSE
View File

@ -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

196
ZjsComponent/README.md Normal file
View File

@ -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 `<script>` elements.
The component can have any number of methods, all defined within `<script>`
elements. Methods can be called the usual way using a reference to the JS
element, or via the `ZjsComponent.send()` static method that will find the
closest **ZjsComponent** ancestor and execute the method on that instance.
At it's minimal usage, **ZjsComponent** can simply be used for client-side
includes of HTML. With full leverage of all it's features, **ZjsComponent**
can be used to create reusable HTML web components in the simplest way
possible.
The example below shows how this can be used within plain HTML to let a
`button` element call a method on the containing **ZjsComponent** instance.
---
## 🔧 Installation
Add the script to your webpage before using any `<zjs-component>` tags:
```html
<script src="/path/to/zjs-component.js"></script>
```
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
<zjs-component remote-src="components/hello.zjsc"
greeting="Hello"
name="World">
</zjs-component>
```
> **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 `<script>` elements defining component methods and lifecycle hooks
- The scripts executes in isolation and may export functions to bind to the
component
Example `hello.zjsc`:
```html
<div>
<input name="name-input" placeholder="Enter your name here">
<button onclick="ZjsComponent.send(this, 'updateGreeting')">Greet</button>
<p name="greeting-display"></p>
</div>
<script>
// Method automatically called when the component is connected to the DOM
function onConnected() {
this.greeting = this.getAttribute("greeting") || "Hi";
this.name = this.getAttribute("name") || "there";
}
// Normal method; the `this` keyword works here too.
function updateGreeting() {
const el = this.querySelector("[name='name-input']");
this.name = el.innerText;
// No special reason for deferring the call. I just wanted to
// demonstrate that the private methods can be called at any time
// even outside of the stack frame that has the called public
// method.
setTimeout(() => displayGreeting());
}
// Another method, this one won't be exported.
function displayGreeting() {
const el = this.querySelector("[name='greeting-display']");
el.innerText = this.name;
}
// All public methods *must* be exported like this. If they are
// not exported, they are private to this instance and cannot
// be called from outside of this instance.
exports.onConnected = onConnected;
exports.updateGreeting = updateGreeting;
</script>
```
---
## 🧠 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
<button onclick="ZjsComponent.send(this, 'someMethod')">Click</button>
```
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
<zjs-component remote-src="components/debug.zjsc" debug></zjs-component>
```
---
## 🔐 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)

View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang=en-US>
<head>
<title>JSON-HTML Test</title>
<script src='/quicklee/webroot/js/fluent.js'></script>
<script src='/quicklee/webroot/js/util.js'></script>
<script src='/quicklee/webroot/js/rpc.js'></script>
<script src='/quicklee/webroot/js/crud.js'></script>
<script src='/quicklee/webroot/js/zjs/zjs-component.js'></script>
<link rel="stylesheet"
href="../css/themes/colors/muted.css">
<link rel='stylesheet'
href='../css/themes/classless/vanilla.css' />
<link rel='stylesheet'
href='../css/themes/brand/olive.css' />
<link rel='stylesheet'
href='../css/layouts.css' />
<meta name='viewport'
content='width=device-width, initial-scale=1'>
</head>
<body>
<div class=grid-2>
<div>
LHS Menu stuff
<hr>
<button onclick='removeElement("#first")'>Remove First</button>
<button onclick='removeElement("#second")'>Remove Second</button>
</div>
<div>
<zjs-component id=first
display=inline
remote-src=zjsc/component-1.zjsc
counter-2=100>
</zjs-component>
<hr>
<zjs-component id=second
remote-src=zjsc/component-1.zjsc
counter-2=200>
</zjs-component>
</div>
</div>
<script>
function removeElement(qselector) {
document.querySelector(qselector).remove();
}
</script>
</body>
</html>

View File

@ -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);

View File

@ -0,0 +1,64 @@
Development notes
I've used a `<zjs-include remote-src=...>` 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.

View File

@ -0,0 +1,40 @@
<fieldset>
<legend>Show counter</legend>
<button onclick="ZjsComponent.send(this, 'showCounter');">Click Me</button>
</fieldset>
<fieldset>
<legend>Increment counter</legend>
<button onclick="ZjsComponent.send(this, 'increment', 11);">Increment</button>
<div name=counter-display>
</div>
</fieldset>
<script>
function onConnected() {
this.counter1 = 0;
this.counter2 = parseInt(this.getAttribute("counter-2"));
console.log("Connected");
}
function onDisconnected() {
console.log("Disconnected");
}
function showCounter(el) {
this.querySelector("[name=counter-display]").innerText =
`${this.counter1}: ${this.counter2}`;
}
function increment(amount) {
this.counter1 += amount;
this.counter2 += amount;
console.log(`${this.counter1}: ${this.counter2}`);
}
exports.showCounter = showCounter;
exports.increment = increment;
exports.onConnected = onConnected;
exports.onDisconnected = onDisconnected;
</script>