enigmatic 0.33.0 → 0.35.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/{CLIENT_JS_DOCS.md → README.md} +163 -36
- package/bin/enigmatic.js +3 -0
- package/client/public/AGENTS.md +314 -0
- package/client/public/client.js +125 -0
- package/client/public/custom.js +34 -0
- package/client/public/index.html +197 -0
- package/clientserver.png +0 -0
- package/package.json +7 -9
- package/server/bun-server.js +119 -0
- package/__tests__/e2.test.js +0 -310
- package/__tests__/jest.config.js +0 -7
- package/__tests__/jest.setup.js +0 -9
- package/bun-server.js +0 -130
- package/public/client.css +0 -286
- package/public/client.js +0 -80
- package/public/custom.js +0 -29
- package/public/index.html +0 -45
- package/public/index2.html +0 -9
- package/public/theme.css +0 -9
|
@@ -1,4 +1,108 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Enigmatic
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
A lightweight client-side JavaScript library for DOM manipulation, reactive state management, and API interactions, with an optional Bun server for backend functionality.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
The diagram above shows the interaction between the client (browser), Bun server, and external services (Auth0 and Cloudflare R2/S3).
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Using client.js via CDN
|
|
16
|
+
|
|
17
|
+
Include `client.js` in any HTML file using the unpkg CDN:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<!DOCTYPE html>
|
|
21
|
+
<html>
|
|
22
|
+
<head>
|
|
23
|
+
<script src="https://unpkg.com/enigmatic"></script>
|
|
24
|
+
<script src="https://unpkg.com/enigmatic/client/public/custom.js"></script>
|
|
25
|
+
<script>
|
|
26
|
+
window.api_url = 'https://your-server.com';
|
|
27
|
+
window.state.message = 'Hello World';
|
|
28
|
+
</script>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<hello-world data="message"></hello-world>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Note:** Use `enigmatic@0.35.0` (or latest) in the URL to pin a version.
|
|
37
|
+
|
|
38
|
+
### Using the Bun Server
|
|
39
|
+
|
|
40
|
+
The Bun server provides a complete backend with:
|
|
41
|
+
- **Key-value storage** – Per-user KV persisted as append-only JSONL (`server/kv/{user}.jsonl`) with `update`/`delete` actions and timestamps
|
|
42
|
+
- **File storage** – Per-user files via Cloudflare R2 (or S3-compatible API)
|
|
43
|
+
- **Authentication** – Auth0 OAuth2 login/logout
|
|
44
|
+
- **Static files** – Served from `client/public/`
|
|
45
|
+
|
|
46
|
+
#### Installation
|
|
47
|
+
|
|
48
|
+
1. Install [Bun](https://bun.sh):
|
|
49
|
+
```bash
|
|
50
|
+
curl -fsSL https://bun.sh/install | bash
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
2. Install dependencies (if any):
|
|
54
|
+
```bash
|
|
55
|
+
bun install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
3. TLS certificates: place `cert.pem` and `key.pem` in `server/certs/` for HTTPS (required for Auth0 in production).
|
|
59
|
+
|
|
60
|
+
#### Environment Variables
|
|
61
|
+
|
|
62
|
+
Create a `.env` file in the project root (or set env vars):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Auth0
|
|
66
|
+
AUTH0_DOMAIN=your-tenant.auth0.com
|
|
67
|
+
AUTH0_CLIENT_ID=your-client-id
|
|
68
|
+
AUTH0_CLIENT_SECRET=your-client-secret
|
|
69
|
+
|
|
70
|
+
# Cloudflare R2 (optional, for file storage)
|
|
71
|
+
CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id
|
|
72
|
+
CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key
|
|
73
|
+
CLOUDFLARE_BUCKET_NAME=your-bucket-name
|
|
74
|
+
CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Running the Server
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm start
|
|
81
|
+
# or
|
|
82
|
+
npx enigmatic
|
|
83
|
+
# or with hot reload
|
|
84
|
+
npm run hot
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Server runs at **https://localhost:3000** (HTTPS is required for Auth0 cookies).
|
|
88
|
+
|
|
89
|
+
#### Server Endpoints
|
|
90
|
+
|
|
91
|
+
| Method | Path | Description |
|
|
92
|
+
|----------|------------|-------------|
|
|
93
|
+
| GET | `/` | Serves `client/public/index.html` |
|
|
94
|
+
| GET | `/index.html`, `/*.js`, etc. | Static files from `client/public/` |
|
|
95
|
+
| GET | `/login` | Redirects to Auth0 login |
|
|
96
|
+
| GET | `/callback`| Auth0 OAuth callback |
|
|
97
|
+
| GET | `/logout` | Logs out and clears session |
|
|
98
|
+
| GET | `/me` | Current user or 401 (no auth) |
|
|
99
|
+
| GET | `/{key}` | KV get (auth required) |
|
|
100
|
+
| POST | `/{key}` | KV set (auth required) |
|
|
101
|
+
| DELETE | `/{key}` | KV delete (auth required) |
|
|
102
|
+
| PUT | `/{key}` | Upload file to R2 (auth required) |
|
|
103
|
+
| PURGE | `/{key}` | Delete file from R2 (auth required) |
|
|
104
|
+
| PROPFIND | `/` | List R2 files (auth required) |
|
|
105
|
+
| PATCH | `/{key}` | Download file from R2 (auth required) |
|
|
2
106
|
|
|
3
107
|
## Overview
|
|
4
108
|
|
|
@@ -36,13 +140,15 @@ Configures the base URL for all API requests. Modify this to point to your serve
|
|
|
36
140
|
- Set a property: `window.state.myKey = 'value'`
|
|
37
141
|
- Elements with `data="myKey"` attribute are automatically updated
|
|
38
142
|
- The system looks for custom element handlers in `window.custom[tagName]`
|
|
143
|
+
- Only elements with matching custom element handlers are updated
|
|
39
144
|
- Supports both function and object-based custom elements
|
|
40
145
|
|
|
41
146
|
**Example:**
|
|
42
147
|
```html
|
|
43
|
-
<
|
|
148
|
+
<my-element data="message">Initial</my-element>
|
|
44
149
|
<script>
|
|
45
|
-
window.
|
|
150
|
+
window.custom['my-element'] = (data) => `<div>${data}</div>`;
|
|
151
|
+
window.state.message = "Updated!"; // Automatically updates the element
|
|
46
152
|
</script>
|
|
47
153
|
```
|
|
48
154
|
|
|
@@ -167,9 +273,19 @@ window.logout();
|
|
|
167
273
|
|
|
168
274
|
**Behavior:** Sets `window.location.href` to `{api_url}/logout`
|
|
169
275
|
|
|
276
|
+
#### `window.me()`
|
|
277
|
+
Returns the current user if authenticated, or `null` if not (e.g. 401).
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
const user = await window.me();
|
|
281
|
+
// user is { sub, email, ... } or null
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Endpoint:** `GET {api_url}/me` (with credentials)
|
|
285
|
+
|
|
170
286
|
## Custom Elements System
|
|
171
287
|
|
|
172
|
-
Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads.
|
|
288
|
+
Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads or when elements are added dynamically.
|
|
173
289
|
|
|
174
290
|
### Initialization
|
|
175
291
|
|
|
@@ -178,6 +294,17 @@ The library automatically:
|
|
|
178
294
|
2. Iterates through all keys in `window.custom`
|
|
179
295
|
3. Finds all matching HTML elements by tag name
|
|
180
296
|
4. Calls the custom element handler and sets `innerHTML`
|
|
297
|
+
5. Watches for new elements added to the DOM via MutationObserver and initializes them automatically
|
|
298
|
+
|
|
299
|
+
### Proxy Behavior
|
|
300
|
+
|
|
301
|
+
`window.custom` is a Proxy that automatically initializes matching elements when you add a new custom element definition:
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
// Adding a new custom element automatically initializes all matching elements in the DOM
|
|
305
|
+
window.custom['my-element'] = (data) => `<div>${data}</div>`;
|
|
306
|
+
// All <my-element> tags are immediately initialized
|
|
307
|
+
```
|
|
181
308
|
|
|
182
309
|
### Defining Custom Elements
|
|
183
310
|
|
|
@@ -196,12 +323,15 @@ window.custom = {
|
|
|
196
323
|
<my-element></my-element>
|
|
197
324
|
```
|
|
198
325
|
|
|
199
|
-
When
|
|
326
|
+
When used with reactive state, the function receives the state value:
|
|
200
327
|
```html
|
|
201
328
|
<my-element data="myKey"></my-element>
|
|
329
|
+
<script>
|
|
330
|
+
window.state.myKey = 'value'; // Function is called with 'value'
|
|
331
|
+
</script>
|
|
202
332
|
```
|
|
203
333
|
|
|
204
|
-
The function receives the state value as the first parameter.
|
|
334
|
+
The function receives the state value as the first parameter. If no state value is set, it receives `undefined`.
|
|
205
335
|
|
|
206
336
|
#### Object-based Custom Element
|
|
207
337
|
|
|
@@ -267,37 +397,18 @@ window.get('key').catch(err => console.error(err));
|
|
|
267
397
|
<!DOCTYPE html>
|
|
268
398
|
<html>
|
|
269
399
|
<head>
|
|
270
|
-
<script src="
|
|
271
|
-
<script src="client.js"></script>
|
|
400
|
+
<script src="https://unpkg.com/enigmatic"></script>
|
|
401
|
+
<script src="https://unpkg.com/enigmatic/client/public/custom.js"></script>
|
|
402
|
+
<script>
|
|
403
|
+
window.api_url = window.api_url || window.location.origin;
|
|
404
|
+
window.state.message = 'World';
|
|
405
|
+
</script>
|
|
272
406
|
</head>
|
|
273
407
|
<body>
|
|
274
|
-
|
|
408
|
+
<hello-world data="message"></hello-world>
|
|
275
409
|
<file-widget></file-widget>
|
|
276
|
-
|
|
277
|
-
<!-- Reactive state element -->
|
|
278
|
-
<div data="message">Initial</div>
|
|
279
|
-
|
|
280
410
|
<script>
|
|
281
|
-
|
|
282
|
-
window.state.message = "Hello World";
|
|
283
|
-
|
|
284
|
-
// Use API functions
|
|
285
|
-
(async () => {
|
|
286
|
-
await window.set('test', 'value');
|
|
287
|
-
const value = await window.get('test');
|
|
288
|
-
console.log(value);
|
|
289
|
-
|
|
290
|
-
// Upload file
|
|
291
|
-
const fileInput = document.querySelector('input[type="file"]');
|
|
292
|
-
fileInput.onchange = async (e) => {
|
|
293
|
-
const file = e.target.files[0];
|
|
294
|
-
await window.put(file.name, file);
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
// List files
|
|
298
|
-
const files = await window.list();
|
|
299
|
-
console.log(files);
|
|
300
|
-
})();
|
|
411
|
+
window.me().then(u => console.log(u ? 'Logged in as ' + u.email : 'Not logged in'));
|
|
301
412
|
</script>
|
|
302
413
|
</body>
|
|
303
414
|
</html>
|
|
@@ -305,18 +416,34 @@ window.get('key').catch(err => console.error(err));
|
|
|
305
416
|
|
|
306
417
|
## Dependencies
|
|
307
418
|
|
|
308
|
-
- Requires
|
|
309
|
-
- Requires a backend server that implements the API endpoints
|
|
419
|
+
- Requires a backend server that implements the API endpoints (or use the included Bun server)
|
|
310
420
|
- Requires browser support for:
|
|
311
421
|
- `fetch` API
|
|
312
422
|
- `Proxy` API
|
|
313
423
|
- `Blob` API
|
|
314
424
|
- `URL.createObjectURL`
|
|
425
|
+
- `MutationObserver` API
|
|
426
|
+
|
|
427
|
+
**Note:** Load `client.js` first, then your custom element definitions (e.g. `custom.js`); the Proxy initializes elements when definitions are assigned.
|
|
315
428
|
|
|
316
429
|
## Notes
|
|
317
430
|
|
|
318
431
|
- All API functions automatically encode keys using `encodeURIComponent`
|
|
319
432
|
- The `window.download()` function uses PATCH method internally (browsers don't support custom HTTP methods)
|
|
320
|
-
- Custom elements are initialized
|
|
433
|
+
- Custom elements are automatically initialized:
|
|
434
|
+
- On page load (when DOM is ready)
|
|
435
|
+
- When new custom element definitions are added to `window.custom`
|
|
436
|
+
- When new matching elements are added to the DOM (via MutationObserver)
|
|
321
437
|
- The reactive state system only updates elements with matching `data` attributes
|
|
322
438
|
- Custom element handlers can be async functions
|
|
439
|
+
- When a custom element has a `data` attribute, it automatically reads from `window.state[dataValue]` if no explicit value is provided
|
|
440
|
+
|
|
441
|
+
## Development
|
|
442
|
+
|
|
443
|
+
- **Start server:** `npm start` or `npx enigmatic`
|
|
444
|
+
- **Hot reload:** `npm run hot`
|
|
445
|
+
- **Client:** Load `client.js` from unpkg or from `client/public/client.js` when serving locally. Load `custom.js` (or your definitions) after `client.js`; set `window.api_url` before making API calls.
|
|
446
|
+
|
|
447
|
+
## License
|
|
448
|
+
|
|
449
|
+
MIT
|
package/bin/enigmatic.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Custom Web Components Instructions for LLMs
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
When users request web components to be created, you MUST use the `window.custom` system. You can see examples at https://unpkg.com/enigmatic/client/public/custom.js or in the repo at `client/public/custom.js`. This system allows you to define custom HTML elements that automatically render and can be reactive to state changes.
|
|
6
|
+
|
|
7
|
+
## System Architecture
|
|
8
|
+
|
|
9
|
+
The custom component system works by:
|
|
10
|
+
1. Defining components in `window.custom` object
|
|
11
|
+
2. Using custom HTML tags in the DOM (e.g., `<my-component></my-component>`)
|
|
12
|
+
3. Components automatically initialize when the DOM loads or when elements are added
|
|
13
|
+
4. Components can be reactive to `window.state` changes via `data` attributes
|
|
14
|
+
|
|
15
|
+
## Component Definition Format
|
|
16
|
+
|
|
17
|
+
### Format 1: Function-Based Component (Simple)
|
|
18
|
+
|
|
19
|
+
**Use this format for simple components that just render HTML:**
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
window.custom = {
|
|
23
|
+
"mycomponent": (data) => {
|
|
24
|
+
// data is the value from window.state if element has data="key" attribute
|
|
25
|
+
// If no data attribute, data will be undefined
|
|
26
|
+
return `<div>Your HTML here: ${data || 'default'}</div>`;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Example from custom.js:**
|
|
32
|
+
```javascript
|
|
33
|
+
window.custom = {
|
|
34
|
+
"hello-world": (data) => `Hello ${data}`,
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**HTML Usage:**
|
|
39
|
+
```html
|
|
40
|
+
<hello-world></hello-world>
|
|
41
|
+
<!-- or with reactive state -->
|
|
42
|
+
<hello-world data="message"></hello-world>
|
|
43
|
+
<script>
|
|
44
|
+
window.state.message = "World"; // Component updates automatically
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Format 2: Object-Based Component (Advanced)
|
|
49
|
+
|
|
50
|
+
**Use this format when you need methods, properties, or more complex logic:**
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
window.custom = {
|
|
54
|
+
"component-name": {
|
|
55
|
+
// Optional helper methods
|
|
56
|
+
prop: (data) => `Processed: ${data}`,
|
|
57
|
+
|
|
58
|
+
// Required: render method that returns HTML string
|
|
59
|
+
render: function(data) {
|
|
60
|
+
// Use 'this' to access other methods/properties
|
|
61
|
+
return `<div>${this.prop(data)}</div>`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Example from custom.js:**
|
|
68
|
+
```javascript
|
|
69
|
+
window.custom = {
|
|
70
|
+
"hello-world-2": {
|
|
71
|
+
prop: (data) => `${data} World`,
|
|
72
|
+
render: function(data) {
|
|
73
|
+
return this.prop(data);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**HTML Usage:**
|
|
80
|
+
```html
|
|
81
|
+
<hello-world-2 data="greeting"></hello-world-2>
|
|
82
|
+
<script>
|
|
83
|
+
window.state.greeting = "Hello"; // Component updates automatically
|
|
84
|
+
</script>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Format 3: Async Component
|
|
88
|
+
|
|
89
|
+
**Use this format when you need to fetch data or perform async operations:**
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
window.custom = {
|
|
93
|
+
"async-component": async () => {
|
|
94
|
+
try {
|
|
95
|
+
const data = await window.get('some-key');
|
|
96
|
+
// Or use other API functions: window.list(), window.set(), etc.
|
|
97
|
+
return `<div>${JSON.stringify(data)}</div>`;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return `<div>Error: ${err.message}</div>`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Example from custom.js:**
|
|
106
|
+
```javascript
|
|
107
|
+
window.custom = {
|
|
108
|
+
"file-widget": async () => {
|
|
109
|
+
try {
|
|
110
|
+
const list = await window.list();
|
|
111
|
+
// Render file list with styles and interactive elements
|
|
112
|
+
return `<div>...</div>`;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return `<div>Error occurred</div>`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Key Rules and Patterns
|
|
121
|
+
|
|
122
|
+
### 1. Component Names
|
|
123
|
+
- Use kebab-case (e.g., `"my-component"`, `"file-widget"`)
|
|
124
|
+
- Must match the HTML tag name exactly (case-insensitive)
|
|
125
|
+
- Example: `window.custom["my-component"]` → `<my-component></my-component>`
|
|
126
|
+
|
|
127
|
+
### 2. Return Value
|
|
128
|
+
- Components MUST return a string containing HTML
|
|
129
|
+
- Can include inline `<style>` tags for component-specific styles
|
|
130
|
+
- Can include inline event handlers using `onclick`, `onchange`, etc.
|
|
131
|
+
|
|
132
|
+
### 3. Reactive State Integration
|
|
133
|
+
- Components automatically receive state values when element has `data="key"` attribute
|
|
134
|
+
- When `window.state.key = value` is set, all elements with `data="key"` are re-rendered
|
|
135
|
+
- Access state value as the first parameter: `(data) => ...` or `render: function(data) { ... }`
|
|
136
|
+
|
|
137
|
+
### 4. Async Operations
|
|
138
|
+
- Use `async` functions when you need to fetch data
|
|
139
|
+
- Use `window.get()`, `window.set()`, `window.list()`, `window.put()`, `window.delete()`, `window.purge()`, `window.me()`, `window.download()`, etc. for API calls (all use `window.api_url`)
|
|
140
|
+
- Always wrap async operations in try-catch for error handling
|
|
141
|
+
|
|
142
|
+
### 5. Styling
|
|
143
|
+
- Include styles inline using `<style>` tags within the returned HTML
|
|
144
|
+
- Use scoped class names (e.g., `.w-c`, `.w-i`) to avoid conflicts
|
|
145
|
+
- Keep styles minimal and component-specific
|
|
146
|
+
|
|
147
|
+
### 6. Event Handlers
|
|
148
|
+
- Use inline event handlers: `onclick="..."`, `onchange="..."`
|
|
149
|
+
- Can call `window` API functions directly: `onclick="window.download('file.txt')"`
|
|
150
|
+
- For async operations, wrap in IIFE: `onclick="(async()=>{await window.purge('file');location.reload()})()"`
|
|
151
|
+
|
|
152
|
+
## Complete Examples
|
|
153
|
+
|
|
154
|
+
### Example 1: Simple Counter Component
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
window.custom = {
|
|
158
|
+
"counter": (data) => {
|
|
159
|
+
const count = data || 0;
|
|
160
|
+
return `
|
|
161
|
+
<div style="padding: 20px; text-align: center;">
|
|
162
|
+
<h2>Count: ${count}</h2>
|
|
163
|
+
<button onclick="window.state.count = (window.state.count || 0) + 1">+</button>
|
|
164
|
+
<button onclick="window.state.count = (window.state.count || 0) - 1">-</button>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**HTML:**
|
|
172
|
+
```html
|
|
173
|
+
<counter data="count"></counter>
|
|
174
|
+
<script>
|
|
175
|
+
window.state.count = 0;
|
|
176
|
+
</script>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Example 2: Data Display Component
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
window.custom = {
|
|
183
|
+
"data-display": async () => {
|
|
184
|
+
try {
|
|
185
|
+
const data = await window.get('user-data');
|
|
186
|
+
return `
|
|
187
|
+
<div style="padding: 15px; border: 1px solid #ddd; border-radius: 5px;">
|
|
188
|
+
<h3>User Data</h3>
|
|
189
|
+
<pre>${JSON.stringify(data, null, 2)}</pre>
|
|
190
|
+
</div>
|
|
191
|
+
`;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return `<div style="color: red;">Error loading data: ${err.message}</div>`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**HTML:**
|
|
200
|
+
```html
|
|
201
|
+
<data-display></data-display>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Example 3: Form Component with Object Methods
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
window.custom = {
|
|
208
|
+
"contact-form": {
|
|
209
|
+
validate: (email) => email && email.includes('@'),
|
|
210
|
+
|
|
211
|
+
render: function(data) {
|
|
212
|
+
const email = data?.email || '';
|
|
213
|
+
const isValid = this.validate(email);
|
|
214
|
+
|
|
215
|
+
return `
|
|
216
|
+
<style>
|
|
217
|
+
.form-container { padding: 20px; max-width: 400px; }
|
|
218
|
+
.form-input { width: 100%; padding: 8px; margin: 5px 0; }
|
|
219
|
+
.form-submit { background: #007bff; color: white; padding: 10px 20px; border: none; cursor: pointer; }
|
|
220
|
+
</style>
|
|
221
|
+
<div class="form-container">
|
|
222
|
+
<input type="email" class="form-input"
|
|
223
|
+
placeholder="Email"
|
|
224
|
+
value="${email}"
|
|
225
|
+
onchange="window.state.formData = {...(window.state.formData || {}), email: this.value}">
|
|
226
|
+
<button class="form-submit"
|
|
227
|
+
onclick="alert('Form submitted!')"
|
|
228
|
+
${!isValid ? 'disabled' : ''}>
|
|
229
|
+
Submit
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**HTML:**
|
|
239
|
+
```html
|
|
240
|
+
<contact-form data="formData"></contact-form>
|
|
241
|
+
<script>
|
|
242
|
+
window.state.formData = { email: '' };
|
|
243
|
+
</script>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## When to Use Each Format
|
|
247
|
+
|
|
248
|
+
- **Function format**: Simple components, static content, or when you only need the data parameter
|
|
249
|
+
- **Object format**: When you need helper methods, want to organize code better, or need `this` context
|
|
250
|
+
- **Async format**: When you need to fetch data from APIs, perform async operations, or load external resources
|
|
251
|
+
|
|
252
|
+
## Important Notes
|
|
253
|
+
|
|
254
|
+
1. **Always return HTML strings** - Components must return valid HTML strings
|
|
255
|
+
2. **Handle errors** - Wrap async operations in try-catch blocks
|
|
256
|
+
3. **Use kebab-case** - Component names must be in kebab-case to match HTML tag names
|
|
257
|
+
4. **State reactivity** - Use `data="key"` attribute to make components reactive to `window.state[key]`
|
|
258
|
+
5. **Auto-initialization** - Components automatically initialize when:
|
|
259
|
+
- DOM loads
|
|
260
|
+
- Component is added to `window.custom`
|
|
261
|
+
- New matching elements are added to the DOM
|
|
262
|
+
|
|
263
|
+
## Common Patterns
|
|
264
|
+
|
|
265
|
+
### Pattern: Loading State
|
|
266
|
+
```javascript
|
|
267
|
+
window.custom = {
|
|
268
|
+
"loading-component": async () => {
|
|
269
|
+
try {
|
|
270
|
+
const data = await window.get('data');
|
|
271
|
+
return data ? `<div>${data}</div>` : '<div>No data</div>';
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return `<div>Error: ${err.message}</div>`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Pattern: Error Handling
|
|
280
|
+
```javascript
|
|
281
|
+
window.custom = {
|
|
282
|
+
"safe-component": async () => {
|
|
283
|
+
try {
|
|
284
|
+
const result = await window.list();
|
|
285
|
+
return `<div>Success: ${result.length} items</div>`;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return `<div style="color: red;">Error: ${err.message}</div>`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Pattern: Conditional Rendering
|
|
294
|
+
```javascript
|
|
295
|
+
window.custom = {
|
|
296
|
+
"conditional": (data) => {
|
|
297
|
+
if (!data) return '<div>No data</div>';
|
|
298
|
+
if (data.error) return `<div>Error: ${data.error}</div>`;
|
|
299
|
+
return `<div>Data: ${data.value}</div>`;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Summary
|
|
305
|
+
|
|
306
|
+
When creating web components:
|
|
307
|
+
1. Define them in `window.custom` object
|
|
308
|
+
2. Use kebab-case names matching HTML tag names
|
|
309
|
+
3. Return HTML strings (can include styles and event handlers)
|
|
310
|
+
4. Use function format for simple components
|
|
311
|
+
5. Use object format with `render` method for complex components
|
|
312
|
+
6. Use async functions when fetching data
|
|
313
|
+
7. Make components reactive by using `data="key"` attributes
|
|
314
|
+
8. Always handle errors in async components
|