@zyrab/domo 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LICENSE → LICENSE.md} +21 -21
- package/README.md +88 -0
- package/package.json +7 -5
- package/src/client/base.client.js +37 -0
- package/src/client/builder.client.js +17 -0
- package/src/client/children.client.js +72 -0
- package/src/client/classes.client.js +36 -0
- package/src/client/domo.client.js +24 -0
- package/src/client/events.client.js +51 -0
- package/src/client/properties.client.js +78 -0
- package/src/index.js +14 -0
- package/src/server/base.server.js +121 -0
- package/src/server/builder.server.js +47 -0
- package/src/server/children.server.js +67 -0
- package/src/server/classes.server.js +42 -0
- package/src/server/domo.server.js +24 -0
- package/src/server/events.server.js +79 -0
- package/src/server/properties.server.js +81 -0
- package/src/base.js +0 -80
- package/src/builder.js +0 -43
- package/src/children.js +0 -188
- package/src/classes.js +0 -62
- package/src/domo.js +0 -77
- package/src/events.js +0 -166
- package/src/properties.js +0 -162
package/{LICENSE → LICENSE.md}
RENAMED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c)
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
all copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zyrab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @zyrab/domo
|
|
2
|
+
|
|
3
|
+
**Small, Fast, and Chainable.**
|
|
4
|
+
|
|
5
|
+
`domo` is a minimalist, industrial-strength DOM builder and UI micro-framework designed for developers who value performance, zero-boilerplate, and elegant APIs. It provides a fluent, chainable interface for building both dynamic browser applications and high-performance static sites.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why Domo?
|
|
10
|
+
|
|
11
|
+
In a world of heavy frameworks, `domo` returns to the fundamentals while providing modern features for the SSG era.
|
|
12
|
+
|
|
13
|
+
- **Fluent API**: Build complex structures with a natural, chainable syntax.
|
|
14
|
+
- **Dual-Entry Architecture**: Optimized builds for both Client and Server. The client bundle contains 0% string-building logic, while the server bundle acts as a high-speed metadata collector.
|
|
15
|
+
- **Zero-Branching**: No more `if (isServer)` in your components. Environment-aware modules handle the complexity for you.
|
|
16
|
+
- **SSG Native**: Deeply integrated with `@zyrab/domo-ssg`. Automatically collects events, refs, and state during the render pass to enable seamless hydration-free interactivity.
|
|
17
|
+
- **Microscopic Footprint**: Designed to be small enough to stay out of your way, yet powerful enough to build entire applications.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @zyrab/domo
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Basic Component
|
|
32
|
+
```javascript
|
|
33
|
+
import Domo from '@zyrab/domo';
|
|
34
|
+
|
|
35
|
+
const card = Domo('div').cls('card')
|
|
36
|
+
.child([
|
|
37
|
+
Domo('h1').txt('Product Name').css({ color: 'blue' }),
|
|
38
|
+
Domo('p').txt('This is a description.'),
|
|
39
|
+
Domo('button').txt('Add to Cart').on('click', () => alert('Added!'))
|
|
40
|
+
])
|
|
41
|
+
.build();
|
|
42
|
+
|
|
43
|
+
document.body.appendChild(card);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Advanced State & Interactivity
|
|
47
|
+
Domo handles state synchronization between Server and Client via the `.state()` method.
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
import Domo from '@zyrab/domo';
|
|
51
|
+
|
|
52
|
+
export default function Counter(initial = 0) {
|
|
53
|
+
return Domo('div').cls('counter')
|
|
54
|
+
.state({ count: initial })
|
|
55
|
+
.child([
|
|
56
|
+
Domo('span').txt(`Count: ${initial}`).id('display'),
|
|
57
|
+
Domo('button').txt('+').on('click', (e) => {
|
|
58
|
+
// Access state via Domo runtime...
|
|
59
|
+
})
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Server-Side Rendering (SSG/SSR)
|
|
65
|
+
On the server, `domo` generates clean HTML while capturing metadata for the hydration bridge.
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
import Domo from '@zyrab/domo';
|
|
69
|
+
|
|
70
|
+
// Identical code works on the server!
|
|
71
|
+
const html = Domo('div').txt('Hello from Server').build();
|
|
72
|
+
console.log(html); // "<div>Hello from Server</div>"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
Domo uses a **Layered Inheritance System** instead of dynamic mixins, ensuring maximum compatibility with modern bundlers and IDEs.
|
|
80
|
+
|
|
81
|
+
- **Client Pass**: Uses native `document.createElement`, `classList`, and `addEventListener`.
|
|
82
|
+
- **Server Pass**: Uses a lightweight "Virtual Element" (`vel`) schema and collects metadata (events, refs, state) for the SSG to generate optimal client-side bridge logic.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT © [Zyrab](https://github.com/zyrab) - see the [LICENSE.md](LICENSE.md) file for details.
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zyrab/domo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Minimalist DOM builder and chaining-friendly micro-framework with router support.",
|
|
5
|
-
"main": "./src/
|
|
5
|
+
"main": "./src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
|
-
"import": "./src/
|
|
9
|
-
"default": "./src/
|
|
8
|
+
"import": "./src/index.js",
|
|
9
|
+
"default": "./src/index.js"
|
|
10
10
|
},
|
|
11
11
|
"author": "Zyrab",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"files": [
|
|
14
|
-
"src/"
|
|
14
|
+
"src/",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE.md"
|
|
15
17
|
],
|
|
16
18
|
"keywords": [
|
|
17
19
|
"dom",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class BaseClient
|
|
3
|
+
* @description Foundational class for Client-side Domo elements.
|
|
4
|
+
*/
|
|
5
|
+
class BaseClient {
|
|
6
|
+
/**
|
|
7
|
+
* @property {HTMLElement} element - The actual DOM element.
|
|
8
|
+
*/
|
|
9
|
+
element;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @property {boolean} _isDomo - Flag to identify Domo instances.
|
|
13
|
+
*/
|
|
14
|
+
_isDomo = true;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates an instance of BaseClient.
|
|
18
|
+
* @param {string} [el="div"] - The HTML tag name.
|
|
19
|
+
*/
|
|
20
|
+
constructor(el = "div") {
|
|
21
|
+
this.element = document.createElement(String(el || "div").toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
island(enabled = false) {
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* In browser context, passes the actual DOM element to the callback.
|
|
28
|
+
* @param {function(HTMLElement): void} callback
|
|
29
|
+
* @returns {this}
|
|
30
|
+
*/
|
|
31
|
+
ref(callback) {
|
|
32
|
+
if (typeof callback === "function") callback(this.element);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default BaseClient;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ChildrenClient from "./children.client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class BuilderClient
|
|
5
|
+
* @extends ChildrenClient
|
|
6
|
+
*/
|
|
7
|
+
class BuilderClient extends ChildrenClient {
|
|
8
|
+
/**
|
|
9
|
+
* Finalizes the element construction and returns the native DOM element.
|
|
10
|
+
* @returns {HTMLElement}
|
|
11
|
+
*/
|
|
12
|
+
build() {
|
|
13
|
+
return this.element;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default BuilderClient;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import EventsClient from "./events.client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class ChildrenClient
|
|
5
|
+
* @extends EventsClient
|
|
6
|
+
*/
|
|
7
|
+
class ChildrenClient extends EventsClient {
|
|
8
|
+
_handleElementInstance(element) {
|
|
9
|
+
if (element && element._isDomo) return element.build();
|
|
10
|
+
if (element instanceof DocumentFragment || element instanceof Node) return element;
|
|
11
|
+
if (typeof element === "string" || typeof element === "number") {
|
|
12
|
+
return document.createTextNode(String(element));
|
|
13
|
+
}
|
|
14
|
+
return document.createTextNode(`⚠ Invalid child: ${String(element)}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
child(children = []) {
|
|
18
|
+
const flattenedChildren = Array.isArray(children) ? children.flat() : [children];
|
|
19
|
+
flattenedChildren.forEach((child) => {
|
|
20
|
+
this.element.appendChild(this._handleElementInstance(child));
|
|
21
|
+
});
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
append(children = []) {
|
|
26
|
+
return this.child(children);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
appendTo(target) {
|
|
30
|
+
const targetNode = target && target._isDomo ? target.element : target;
|
|
31
|
+
if (targetNode instanceof Node) {
|
|
32
|
+
targetNode.appendChild(this.element);
|
|
33
|
+
}
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parent(target) {
|
|
38
|
+
return this.appendTo(target);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear() {
|
|
42
|
+
this.element.replaceChildren();
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
replace(child, newChild) {
|
|
47
|
+
const resolvedNew = this._handleElementInstance(newChild);
|
|
48
|
+
if (child === this.element) {
|
|
49
|
+
this.element.replaceWith(resolvedNew);
|
|
50
|
+
this.element = resolvedNew;
|
|
51
|
+
} else if (this.element.contains(child)) {
|
|
52
|
+
child.replaceWith(resolvedNew);
|
|
53
|
+
}
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
show(visible = true, displayValue = "block") {
|
|
58
|
+
this.element.style.display = visible ? displayValue : "none";
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if(condition) {
|
|
63
|
+
if (!condition) {
|
|
64
|
+
return new this.constructor("if")
|
|
65
|
+
.attr({ hidden: true })
|
|
66
|
+
.data({ if: this.element.tagName.toLowerCase() });
|
|
67
|
+
}
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default ChildrenClient;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import PropertiesClient from "./properties.client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class ClassesClient
|
|
5
|
+
* @extends PropertiesClient
|
|
6
|
+
*/
|
|
7
|
+
class ClassesClient extends PropertiesClient {
|
|
8
|
+
/**
|
|
9
|
+
* Normalizes various inputs into a clean array of class names.
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
_parseClassList(input) {
|
|
13
|
+
return Array.isArray(input) ? input.filter(Boolean) : String(input).split(" ").filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
cls(classes) {
|
|
17
|
+
if (!classes) return this;
|
|
18
|
+
const clsList = this._parseClassList(classes);
|
|
19
|
+
this.element.classList.add(...clsList);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
rmvCls(classes) {
|
|
24
|
+
if (classes) {
|
|
25
|
+
this.element.classList.remove(...this._parseClassList(classes));
|
|
26
|
+
}
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
tgglCls(className, force) {
|
|
31
|
+
this.element.classList.toggle(className, force);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default ClassesClient;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import BuilderClient from "./builder.client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class DomoClient
|
|
5
|
+
* @extends BuilderClient
|
|
6
|
+
* @description Main Domo class for the Browser environment.
|
|
7
|
+
*/
|
|
8
|
+
class DomoClient extends BuilderClient {
|
|
9
|
+
constructor(el = "div") {
|
|
10
|
+
super(el);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Factory function for Browser Domo elements.
|
|
16
|
+
* @param {string} [el="div"]
|
|
17
|
+
* @returns {DomoClient}
|
|
18
|
+
*/
|
|
19
|
+
function Domo(el = "div") {
|
|
20
|
+
return new DomoClient(el);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { DomoClient };
|
|
24
|
+
export default Domo;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import ClassesClient from "./classes.client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class EventsClient
|
|
5
|
+
* @extends ClassesClient
|
|
6
|
+
*/
|
|
7
|
+
class EventsClient extends ClassesClient {
|
|
8
|
+
/**
|
|
9
|
+
* Adds event listeners to the element.
|
|
10
|
+
*/
|
|
11
|
+
on(eventMapOrName, callback, options = {}) {
|
|
12
|
+
if (!eventMapOrName) return this;
|
|
13
|
+
|
|
14
|
+
if (typeof eventMapOrName === "object" && eventMapOrName !== null) {
|
|
15
|
+
for (const [event, value] of Object.entries(eventMapOrName)) {
|
|
16
|
+
if (typeof value === "function") {
|
|
17
|
+
this.element.addEventListener(event, value);
|
|
18
|
+
} else if (Array.isArray(value)) {
|
|
19
|
+
const [cb, opts] = value;
|
|
20
|
+
this.element.addEventListener(event, cb, opts);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} else if (typeof callback === "function") {
|
|
24
|
+
this.element.addEventListener(eventMapOrName, callback, options);
|
|
25
|
+
}
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_handleClosest(e, map) {
|
|
30
|
+
for (const [selector, handler] of Object.entries(map)) {
|
|
31
|
+
const match = e.target.closest(selector);
|
|
32
|
+
if (match) handler(e, match);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onClosest(event, selectors = {}, options = {}) {
|
|
37
|
+
return this.on(event, (e) => this._handleClosest(e, selectors), options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_handleMatches(e, map) {
|
|
41
|
+
for (const [selector, handler] of Object.entries(map)) {
|
|
42
|
+
if (e.target.matches(selector)) handler(e, e.target);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onMatch(event, selectors = {}, options = {}) {
|
|
47
|
+
return this.on(event, (e) => this._handleMatches(e, selectors), options);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default EventsClient;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import BaseClient from "./base.client.js";
|
|
2
|
+
|
|
3
|
+
const _stateMap = new WeakMap();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @class PropertiesClient
|
|
7
|
+
* @extends BaseClient
|
|
8
|
+
*/
|
|
9
|
+
class PropertiesClient extends BaseClient {
|
|
10
|
+
/**
|
|
11
|
+
* Sets a property on the element.
|
|
12
|
+
* @protected
|
|
13
|
+
* @param {string} key
|
|
14
|
+
* @param {*} val
|
|
15
|
+
* @returns {this}
|
|
16
|
+
*/
|
|
17
|
+
_set(key, val) {
|
|
18
|
+
if (val === undefined) return this;
|
|
19
|
+
this.element[key] = val;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
id(id) {
|
|
24
|
+
return this._set("id", id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
val(value) {
|
|
28
|
+
return this._set("value", value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
txt(text) {
|
|
32
|
+
return this._set("textContent", text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
attr(attributes = {}) {
|
|
36
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
37
|
+
if (typeof value === "boolean") {
|
|
38
|
+
value ? this.element.setAttribute(key, "") : this.element.removeAttribute(key);
|
|
39
|
+
} else if (value != null) {
|
|
40
|
+
this.element.setAttribute(key, value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
tgglAttr(attrName, force) {
|
|
47
|
+
if (typeof force === "boolean") {
|
|
48
|
+
force ? this.element.setAttribute(attrName, "") : this.element.removeAttribute(attrName);
|
|
49
|
+
} else {
|
|
50
|
+
this.element.hasAttribute(attrName) ? this.element.removeAttribute(attrName) : this.element.setAttribute(attrName, "");
|
|
51
|
+
}
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
data(data = {}) {
|
|
56
|
+
Object.entries(data).forEach(([key, val]) => {
|
|
57
|
+
this.element.dataset[key] = val;
|
|
58
|
+
});
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
css(styles = {}) {
|
|
63
|
+
Object.assign(this.element.style, styles);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Stores state object in a WeakMap for the client runtime.
|
|
69
|
+
* @param {object} obj
|
|
70
|
+
* @returns {this}
|
|
71
|
+
*/
|
|
72
|
+
state(obj) {
|
|
73
|
+
_stateMap.set(this.element, obj);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default PropertiesClient;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import DomoClientFactory, { DomoClient } from "./client/domo.client.js";
|
|
2
|
+
import DomoServerFactory, { DomoServer } from "./server/domo.server.js";
|
|
3
|
+
|
|
4
|
+
const isServer = typeof document === "undefined";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The main Domo factory function.
|
|
8
|
+
* Automatically resolves to DomoClient in the browser and DomoServer in Node.js/SSG environments.
|
|
9
|
+
* @type {typeof DomoClientFactory}
|
|
10
|
+
*/
|
|
11
|
+
const Domo = isServer ? DomoServerFactory : DomoClientFactory;
|
|
12
|
+
|
|
13
|
+
export default Domo;
|
|
14
|
+
export { DomoClient, DomoServer };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { fileURLToPath } from "url";
|
|
2
|
+
/**
|
|
3
|
+
* @class BaseServer
|
|
4
|
+
* @description Foundational class for Server-side (Virtual) Domo elements.
|
|
5
|
+
* Acts as a Metadata Collector for the SSG.
|
|
6
|
+
*/
|
|
7
|
+
class BaseServer {
|
|
8
|
+
/**
|
|
9
|
+
* @property {object} element - The virtual representation of the element.
|
|
10
|
+
*/
|
|
11
|
+
element;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @property {boolean} _isDomo - Flag to identify Domo instances.
|
|
15
|
+
*/
|
|
16
|
+
_isDomo = true;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates an instance of BaseServer.
|
|
20
|
+
* @param {string} [el="div"] - The HTML tag name.
|
|
21
|
+
*/
|
|
22
|
+
constructor(el = "div") {
|
|
23
|
+
this.element = {
|
|
24
|
+
_tag: el,
|
|
25
|
+
_attr: {},
|
|
26
|
+
_cls: [],
|
|
27
|
+
_data: {},
|
|
28
|
+
_css: {},
|
|
29
|
+
_child: [],
|
|
30
|
+
_events: [],
|
|
31
|
+
_refs: [],
|
|
32
|
+
_island: false,
|
|
33
|
+
_state: {},
|
|
34
|
+
__file: null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Manually mark this element/component as an island.
|
|
40
|
+
* This signals the SSG to bundle the Domo client runtime.
|
|
41
|
+
*/
|
|
42
|
+
island(enabled = true) {
|
|
43
|
+
this.element._island = enabled;
|
|
44
|
+
this._getOrSetId();
|
|
45
|
+
if (enabled) {
|
|
46
|
+
this.element.__file = this._getCallerFile(2); // Captures the component file path
|
|
47
|
+
}
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* On the server, captures the reference handler's name for ESM extraction.
|
|
53
|
+
* @param {Function} callback
|
|
54
|
+
* @returns {this}
|
|
55
|
+
*/
|
|
56
|
+
ref(callback) {
|
|
57
|
+
if (typeof callback === "function") {
|
|
58
|
+
this.element._refs.push({
|
|
59
|
+
name: meta._name || callback.name || "anonymous",
|
|
60
|
+
path: meta._source || null, // The absolute path s
|
|
61
|
+
handler: callback,
|
|
62
|
+
});
|
|
63
|
+
// Ensure element has a stable ID if it has a ref
|
|
64
|
+
this._getOrSetId();
|
|
65
|
+
}
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generates or retrieves a stable, hash-based ID for metadata association.
|
|
71
|
+
* @protected
|
|
72
|
+
* @returns {string} The data-domo-id.
|
|
73
|
+
*/
|
|
74
|
+
_getOrSetId() {
|
|
75
|
+
const existing = this.element._attr["data-domo-id"] || this.element._attr["id"];
|
|
76
|
+
if (existing) {
|
|
77
|
+
return existing;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Simple stable hash based on tag and current metadata state
|
|
81
|
+
const hash = Math.random().toString(36).substring(2, 7);
|
|
82
|
+
const id = `d-${hash}`;
|
|
83
|
+
this.element._attr["data-domo-id"] = id;
|
|
84
|
+
return id;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Gets the file path of the caller in the stack trace.
|
|
88
|
+
* @param {number} depth - How many steps back to look (default 2: the caller of the caller)
|
|
89
|
+
*/
|
|
90
|
+
_getCallerFile(depth = 2) {
|
|
91
|
+
const originalPrepare = Error.prepareStackTrace;
|
|
92
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
93
|
+
const stack = new Error().stack;
|
|
94
|
+
Error.prepareStackTrace = originalPrepare;
|
|
95
|
+
|
|
96
|
+
if (!stack || !stack[depth]) return null;
|
|
97
|
+
|
|
98
|
+
const filename = stack[depth].getFileName();
|
|
99
|
+
|
|
100
|
+
// Clean up "file://" prefixes for Windows/ESM compatibility
|
|
101
|
+
if (filename && filename.startsWith("file://")) {
|
|
102
|
+
return fileURLToPath(filename);
|
|
103
|
+
}
|
|
104
|
+
return filename;
|
|
105
|
+
}
|
|
106
|
+
_getExternalPath(fn, currentFile) {
|
|
107
|
+
// If the function has no name and is very short, it's likely an inline arrow fn
|
|
108
|
+
// However, the most reliable way in Node is checking the stack
|
|
109
|
+
const caller = getCallerFile(3); // Adjust depth based on where this is called
|
|
110
|
+
|
|
111
|
+
// Logic: If we can't find a source, or the source is the same as the
|
|
112
|
+
// component currently being built, we treat it as "inline".
|
|
113
|
+
if (!caller || caller === currentFile) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return caller;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default BaseServer;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ChildrenServer from "./children.server.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class BuilderServer
|
|
5
|
+
* @extends ChildrenServer
|
|
6
|
+
*/
|
|
7
|
+
class BuilderServer extends ChildrenServer {
|
|
8
|
+
/**
|
|
9
|
+
* Serializes the virtual element and its metadata into an HTML string.
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
build() {
|
|
13
|
+
const tag = this.element._tag;
|
|
14
|
+
const cls = this.element._cls.join(" ");
|
|
15
|
+
|
|
16
|
+
const toKebab = (s) => s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
17
|
+
|
|
18
|
+
const css = Object.entries(this.element._css)
|
|
19
|
+
.map(([k, v]) => `${toKebab(k)}:${v}`)
|
|
20
|
+
.join(";");
|
|
21
|
+
|
|
22
|
+
const attrs = Object.entries(this.element._attr).map(([k, v]) =>
|
|
23
|
+
v === true ? k : `${k}="${String(v).replace(/"/g, """)}"`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const data = Object.entries(this.element._data).map(([k, v]) => `data-${k}="${String(v).replace(/"/g, """)}"`);
|
|
27
|
+
|
|
28
|
+
if (cls) attrs.push(`class="${cls}"`);
|
|
29
|
+
if (css) attrs.push(`style="${css}"`);
|
|
30
|
+
|
|
31
|
+
const attrStr = attrs.concat(data).join(" ");
|
|
32
|
+
|
|
33
|
+
const escapeHTML = (str) => String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
34
|
+
|
|
35
|
+
const children = this.element._child
|
|
36
|
+
.map((c) => {
|
|
37
|
+
if (typeof c === "string" || typeof c === "number") return escapeHTML(c);
|
|
38
|
+
if (c && typeof c.build === "function") return c.build();
|
|
39
|
+
return "";
|
|
40
|
+
})
|
|
41
|
+
.join("");
|
|
42
|
+
|
|
43
|
+
return `<${tag}${attrStr ? " " + attrStr : ""}>${children}</${tag}>`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default BuilderServer;
|