Adds ZjsComponent files and docs.
This commit is contained in:
parent
d334984902
commit
0438bb1952
13
LICENSE
13
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue