@kaiserofthenight/human-js 1.0.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/README.md +545 -0
- package/examples/api-example/app.js +365 -0
- package/examples/counter/app.js +201 -0
- package/examples/todo-app/app.js +378 -0
- package/examples/user-dashboard/app.js +0 -0
- package/package.json +66 -0
- package/src/core/component.js +182 -0
- package/src/core/events.js +130 -0
- package/src/core/render.js +151 -0
- package/src/core/router.js +182 -0
- package/src/core/state.js +114 -0
- package/src/index.js +63 -0
- package/src/plugins/http.js +167 -0
- package/src/plugins/storage.js +181 -0
- package/src/plugins/validator.js +193 -0
- package/src/utils/dom.js +0 -0
- package/src/utils/helpers.js +209 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TODO APP - Complete Example
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Add, edit, delete todos
|
|
6
|
+
* - Mark as complete
|
|
7
|
+
* - Filter (all, active, completed)
|
|
8
|
+
* - LocalStorage persistence
|
|
9
|
+
* - Form validation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { app, html, each, when } from '../../src/index.js';
|
|
13
|
+
import { local } from '../../src/plugins/storage.js';
|
|
14
|
+
import { createValidator, rules, getFormData, displayErrors } from '../../src/plugins/validator.js';
|
|
15
|
+
import { uid } from '../../src/utils/helpers.js';
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// COMPONENTS
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
function TodoItem(todo, onToggle, onDelete, onEdit) {
|
|
22
|
+
return html`
|
|
23
|
+
<div
|
|
24
|
+
class="todo-item ${todo.completed ? 'completed' : ''}"
|
|
25
|
+
style="
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
padding: 15px;
|
|
29
|
+
border-bottom: 1px solid #eee;
|
|
30
|
+
gap: 10px;
|
|
31
|
+
"
|
|
32
|
+
>
|
|
33
|
+
<input
|
|
34
|
+
type="checkbox"
|
|
35
|
+
${todo.completed ? 'checked' : ''}
|
|
36
|
+
data-id="${todo.id}"
|
|
37
|
+
class="todo-checkbox"
|
|
38
|
+
style="width: 20px; height: 20px; cursor: pointer;"
|
|
39
|
+
/>
|
|
40
|
+
<span
|
|
41
|
+
style="
|
|
42
|
+
flex: 1;
|
|
43
|
+
text-decoration: ${todo.completed ? 'line-through' : 'none'};
|
|
44
|
+
color: ${todo.completed ? '#999' : '#333'};
|
|
45
|
+
"
|
|
46
|
+
>
|
|
47
|
+
${todo.text}
|
|
48
|
+
</span>
|
|
49
|
+
<button
|
|
50
|
+
data-id="${todo.id}"
|
|
51
|
+
class="edit-btn"
|
|
52
|
+
style="
|
|
53
|
+
padding: 5px 10px;
|
|
54
|
+
background: #4CAF50;
|
|
55
|
+
color: white;
|
|
56
|
+
border: none;
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
"
|
|
60
|
+
>
|
|
61
|
+
Edit
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
data-id="${todo.id}"
|
|
65
|
+
class="delete-btn"
|
|
66
|
+
style="
|
|
67
|
+
padding: 5px 10px;
|
|
68
|
+
background: #f44336;
|
|
69
|
+
color: white;
|
|
70
|
+
border: none;
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
"
|
|
74
|
+
>
|
|
75
|
+
Delete
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function TodoStats(todos) {
|
|
82
|
+
const total = todos.length;
|
|
83
|
+
const completed = todos.filter(t => t.completed).length;
|
|
84
|
+
const active = total - completed;
|
|
85
|
+
|
|
86
|
+
return html`
|
|
87
|
+
<div style="
|
|
88
|
+
display: flex;
|
|
89
|
+
justify-content: space-around;
|
|
90
|
+
padding: 20px;
|
|
91
|
+
background: #f5f5f5;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
margin: 20px 0;
|
|
94
|
+
">
|
|
95
|
+
<div style="text-align: center;">
|
|
96
|
+
<div style="font-size: 24px; font-weight: bold; color: #2196F3;">
|
|
97
|
+
${total}
|
|
98
|
+
</div>
|
|
99
|
+
<div style="color: #666;">Total</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div style="text-align: center;">
|
|
102
|
+
<div style="font-size: 24px; font-weight: bold; color: #FF9800;">
|
|
103
|
+
${active}
|
|
104
|
+
</div>
|
|
105
|
+
<div style="color: #666;">Active</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div style="text-align: center;">
|
|
108
|
+
<div style="font-size: 24px; font-weight: bold; color: #4CAF50;">
|
|
109
|
+
${completed}
|
|
110
|
+
</div>
|
|
111
|
+
<div style="color: #666;">Completed</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================
|
|
118
|
+
// VALIDATION
|
|
119
|
+
// ============================================
|
|
120
|
+
|
|
121
|
+
const todoValidator = createValidator({
|
|
122
|
+
todoText: [
|
|
123
|
+
rules.required,
|
|
124
|
+
rules.minLength(3),
|
|
125
|
+
rules.maxLength(100)
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ============================================
|
|
130
|
+
// MAIN APP
|
|
131
|
+
// ============================================
|
|
132
|
+
|
|
133
|
+
const todoApp = app.create({
|
|
134
|
+
state: {
|
|
135
|
+
todos: local.get('todos', []),
|
|
136
|
+
filter: 'all', // all, active, completed
|
|
137
|
+
editingId: null
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
render: (state) => {
|
|
141
|
+
// Filter todos based on current filter
|
|
142
|
+
const filteredTodos = state.todos.filter(todo => {
|
|
143
|
+
if (state.filter === 'active') return !todo.completed;
|
|
144
|
+
if (state.filter === 'completed') return todo.completed;
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const element = html`
|
|
149
|
+
<div style="
|
|
150
|
+
max-width: 600px;
|
|
151
|
+
margin: 50px auto;
|
|
152
|
+
padding: 20px;
|
|
153
|
+
font-family: system-ui, sans-serif;
|
|
154
|
+
">
|
|
155
|
+
<h1 style="text-align: center; color: #2196F3;">
|
|
156
|
+
📝 Todo App
|
|
157
|
+
</h1>
|
|
158
|
+
|
|
159
|
+
${TodoStats(state.todos)}
|
|
160
|
+
|
|
161
|
+
<!-- Add Todo Form -->
|
|
162
|
+
<form id="todo-form" style="margin: 20px 0;">
|
|
163
|
+
<div style="display: flex; gap: 10px;">
|
|
164
|
+
<input
|
|
165
|
+
type="text"
|
|
166
|
+
name="todoText"
|
|
167
|
+
placeholder="What needs to be done?"
|
|
168
|
+
style="
|
|
169
|
+
flex: 1;
|
|
170
|
+
padding: 12px;
|
|
171
|
+
border: 2px solid #ddd;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
font-size: 16px;
|
|
174
|
+
"
|
|
175
|
+
/>
|
|
176
|
+
<button
|
|
177
|
+
type="submit"
|
|
178
|
+
style="
|
|
179
|
+
padding: 12px 24px;
|
|
180
|
+
background: #2196F3;
|
|
181
|
+
color: white;
|
|
182
|
+
border: none;
|
|
183
|
+
border-radius: 4px;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
font-size: 16px;
|
|
186
|
+
font-weight: bold;
|
|
187
|
+
"
|
|
188
|
+
>
|
|
189
|
+
Add
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</form>
|
|
193
|
+
|
|
194
|
+
<!-- Filter Buttons -->
|
|
195
|
+
<div style="
|
|
196
|
+
display: flex;
|
|
197
|
+
gap: 10px;
|
|
198
|
+
margin: 20px 0;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
">
|
|
201
|
+
<button
|
|
202
|
+
class="filter-btn"
|
|
203
|
+
data-filter="all"
|
|
204
|
+
style="
|
|
205
|
+
padding: 8px 16px;
|
|
206
|
+
border: 2px solid ${state.filter === 'all' ? '#2196F3' : '#ddd'};
|
|
207
|
+
background: ${state.filter === 'all' ? '#2196F3' : 'white'};
|
|
208
|
+
color: ${state.filter === 'all' ? 'white' : '#666'};
|
|
209
|
+
border-radius: 4px;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
"
|
|
212
|
+
>
|
|
213
|
+
All
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
class="filter-btn"
|
|
217
|
+
data-filter="active"
|
|
218
|
+
style="
|
|
219
|
+
padding: 8px 16px;
|
|
220
|
+
border: 2px solid ${state.filter === 'active' ? '#FF9800' : '#ddd'};
|
|
221
|
+
background: ${state.filter === 'active' ? '#FF9800' : 'white'};
|
|
222
|
+
color: ${state.filter === 'active' ? 'white' : '#666'};
|
|
223
|
+
border-radius: 4px;
|
|
224
|
+
cursor: pointer;
|
|
225
|
+
"
|
|
226
|
+
>
|
|
227
|
+
Active
|
|
228
|
+
</button>
|
|
229
|
+
<button
|
|
230
|
+
class="filter-btn"
|
|
231
|
+
data-filter="completed"
|
|
232
|
+
style="
|
|
233
|
+
padding: 8px 16px;
|
|
234
|
+
border: 2px solid ${state.filter === 'completed' ? '#4CAF50' : '#ddd'};
|
|
235
|
+
background: ${state.filter === 'completed' ? '#4CAF50' : 'white'};
|
|
236
|
+
color: ${state.filter === 'completed' ? 'white' : '#666'};
|
|
237
|
+
border-radius: 4px;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
"
|
|
240
|
+
>
|
|
241
|
+
Completed
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Todo List -->
|
|
246
|
+
<div style="
|
|
247
|
+
border: 2px solid #ddd;
|
|
248
|
+
border-radius: 8px;
|
|
249
|
+
overflow: hidden;
|
|
250
|
+
">
|
|
251
|
+
${when(
|
|
252
|
+
filteredTodos.length > 0,
|
|
253
|
+
() => html`
|
|
254
|
+
<div>
|
|
255
|
+
${each(filteredTodos, (todo) => TodoItem(todo))}
|
|
256
|
+
</div>
|
|
257
|
+
`,
|
|
258
|
+
() => html`
|
|
259
|
+
<div style="
|
|
260
|
+
padding: 40px;
|
|
261
|
+
text-align: center;
|
|
262
|
+
color: #999;
|
|
263
|
+
">
|
|
264
|
+
${state.filter === 'all' ? 'No todos yet! Add one above.' : `No ${state.filter} todos.`}
|
|
265
|
+
</div>
|
|
266
|
+
`
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- Clear Completed -->
|
|
271
|
+
${when(
|
|
272
|
+
state.todos.some(t => t.completed),
|
|
273
|
+
() => html`
|
|
274
|
+
<button
|
|
275
|
+
id="clear-completed"
|
|
276
|
+
style="
|
|
277
|
+
margin-top: 20px;
|
|
278
|
+
padding: 10px 20px;
|
|
279
|
+
background: #f44336;
|
|
280
|
+
color: white;
|
|
281
|
+
border: none;
|
|
282
|
+
border-radius: 4px;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
width: 100%;
|
|
285
|
+
"
|
|
286
|
+
>
|
|
287
|
+
Clear Completed
|
|
288
|
+
</button>
|
|
289
|
+
`
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
const events = {
|
|
295
|
+
'#todo-form': {
|
|
296
|
+
submit: (e) => {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
|
|
299
|
+
const formData = getFormData(e.target);
|
|
300
|
+
const validation = todoValidator.validate(formData);
|
|
301
|
+
|
|
302
|
+
if (!validation.isValid) {
|
|
303
|
+
displayErrors(e.target, validation.errors);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Add todo
|
|
308
|
+
const newTodo = {
|
|
309
|
+
id: uid('todo'),
|
|
310
|
+
text: formData.todoText,
|
|
311
|
+
completed: false,
|
|
312
|
+
createdAt: Date.now()
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
state.todos = [...state.todos, newTodo];
|
|
316
|
+
local.set('todos', state.todos);
|
|
317
|
+
|
|
318
|
+
e.target.reset();
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
'.todo-checkbox': {
|
|
322
|
+
change: (e) => {
|
|
323
|
+
const id = e.target.dataset.id;
|
|
324
|
+
state.todos = state.todos.map(todo =>
|
|
325
|
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
326
|
+
);
|
|
327
|
+
local.set('todos', state.todos);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
'.delete-btn': {
|
|
331
|
+
click: (e) => {
|
|
332
|
+
const id = e.target.dataset.id;
|
|
333
|
+
if (confirm('Delete this todo?')) {
|
|
334
|
+
state.todos = state.todos.filter(todo => todo.id !== id);
|
|
335
|
+
local.set('todos', state.todos);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
'.edit-btn': {
|
|
340
|
+
click: (e) => {
|
|
341
|
+
const id = e.target.dataset.id;
|
|
342
|
+
const todo = state.todos.find(t => t.id === id);
|
|
343
|
+
const newText = prompt('Edit todo:', todo.text);
|
|
344
|
+
|
|
345
|
+
if (newText && newText.trim()) {
|
|
346
|
+
state.todos = state.todos.map(t =>
|
|
347
|
+
t.id === id ? { ...t, text: newText.trim() } : t
|
|
348
|
+
);
|
|
349
|
+
local.set('todos', state.todos);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
'.filter-btn': {
|
|
354
|
+
click: (e) => {
|
|
355
|
+
state.filter = e.target.dataset.filter;
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
'#clear-completed': {
|
|
359
|
+
click: () => {
|
|
360
|
+
if (confirm('Clear all completed todos?')) {
|
|
361
|
+
state.todos = state.todos.filter(todo => !todo.completed);
|
|
362
|
+
local.set('todos', state.todos);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return { element, events };
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
onMount: () => {
|
|
372
|
+
console.log('✅ Todo App mounted!');
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
onUpdate: (state) => {
|
|
376
|
+
console.log('🔄 Todos updated:', state.todos.length);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaiserofthenight/human-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A framework built for humans, not machines. Zero dependencies, zero build tools, 100% readable.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"human-js",
|
|
8
|
+
"humanjs",
|
|
9
|
+
"framework",
|
|
10
|
+
"frontend",
|
|
11
|
+
"javascript",
|
|
12
|
+
"reactive",
|
|
13
|
+
"spa",
|
|
14
|
+
"human-first",
|
|
15
|
+
"simple",
|
|
16
|
+
"minimalist",
|
|
17
|
+
"zero-build",
|
|
18
|
+
"no-dependencies",
|
|
19
|
+
"vanilla-js",
|
|
20
|
+
"lightweight",
|
|
21
|
+
"web-framework",
|
|
22
|
+
"ui-framework",
|
|
23
|
+
"reactive-programming",
|
|
24
|
+
"state-management",
|
|
25
|
+
"router",
|
|
26
|
+
"components",
|
|
27
|
+
"template-literals",
|
|
28
|
+
"proxy",
|
|
29
|
+
"human-readable"
|
|
30
|
+
],
|
|
31
|
+
"author": {
|
|
32
|
+
"name": "Abderrazzak Elouazghi",
|
|
33
|
+
"email": "abderrazzak.elouazghi@gmail.com",
|
|
34
|
+
"url": "https://github.com/kaiserofthenight"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/kaiserofthenight/human-js.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/kaiserofthenight/human-js/issues",
|
|
43
|
+
"email": "abderrazzak.elouazghi@gmail.com"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/kaiserofthenight/human-js#readme",
|
|
46
|
+
"files": [
|
|
47
|
+
"src/",
|
|
48
|
+
"examples/",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"CHANGELOG.md"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"dev": "python -m http.server 8000 || python3 -m http.server 8000 || npx serve .",
|
|
55
|
+
"demo": "open index.html || start index.html || xdg-open index.html",
|
|
56
|
+
"test": "echo 'Tests coming soon! Contributions welcome.' && exit 0",
|
|
57
|
+
"prepublishOnly": "echo '🚀 Publishing human-js v1.0.0...'"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=14.0.0"
|
|
61
|
+
},
|
|
62
|
+
"funding": {
|
|
63
|
+
"type": "github",
|
|
64
|
+
"url": "https://github.com/sponsors/kaiserofthenight"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPONENT SYSTEM
|
|
3
|
+
*
|
|
4
|
+
* Create reactive components with state, lifecycle hooks, and auto re-rendering.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createState } from './state.js';
|
|
8
|
+
import { attachEvents } from './events.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a reactive component
|
|
12
|
+
* @param {HTMLElement} rootElement - Where to mount the component
|
|
13
|
+
* @param {Function} renderFn - Function that returns { element, events }
|
|
14
|
+
* @param {Object} initialState - Initial state
|
|
15
|
+
* @param {Object} lifecycle - { onMount, onUpdate, onDestroy }
|
|
16
|
+
*/
|
|
17
|
+
export function createComponent(rootElement, renderFn, initialState = {}, initialLifecycle = {}) {
|
|
18
|
+
let currentElement = null;
|
|
19
|
+
let cleanupEvents = null;
|
|
20
|
+
let isDestroyed = false;
|
|
21
|
+
|
|
22
|
+
// Create reactive state FIRST
|
|
23
|
+
const componentState = createState(initialState);
|
|
24
|
+
|
|
25
|
+
// Render function
|
|
26
|
+
function render() {
|
|
27
|
+
if (isDestroyed) return;
|
|
28
|
+
|
|
29
|
+
// Call render function with state
|
|
30
|
+
const result = renderFn(componentState);
|
|
31
|
+
|
|
32
|
+
// Extract element and events
|
|
33
|
+
let newElement, events;
|
|
34
|
+
if (result && typeof result === 'object' && result.element) {
|
|
35
|
+
newElement = result.element;
|
|
36
|
+
events = result.events || {};
|
|
37
|
+
} else {
|
|
38
|
+
newElement = result;
|
|
39
|
+
events = {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// First render - mount
|
|
43
|
+
if (!currentElement) {
|
|
44
|
+
currentElement = newElement;
|
|
45
|
+
rootElement.appendChild(currentElement);
|
|
46
|
+
|
|
47
|
+
// Attach events
|
|
48
|
+
cleanupEvents = attachEvents(currentElement, events);
|
|
49
|
+
|
|
50
|
+
// Call onMount
|
|
51
|
+
if (initialLifecycle.onMount) {
|
|
52
|
+
initialLifecycle.onMount(componentState);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Subsequent renders - replace
|
|
56
|
+
else {
|
|
57
|
+
// Cleanup old events
|
|
58
|
+
if (cleanupEvents) cleanupEvents();
|
|
59
|
+
|
|
60
|
+
// Replace element
|
|
61
|
+
rootElement.replaceChild(newElement, currentElement);
|
|
62
|
+
currentElement = newElement;
|
|
63
|
+
|
|
64
|
+
// Attach new events
|
|
65
|
+
cleanupEvents = attachEvents(currentElement, events);
|
|
66
|
+
|
|
67
|
+
// Call onUpdate
|
|
68
|
+
if (initialLifecycle.onUpdate) {
|
|
69
|
+
initialLifecycle.onUpdate(componentState);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Watch for state changes and re-render
|
|
75
|
+
componentState.$watch = new Proxy(componentState.$watch || (() => {}), {
|
|
76
|
+
apply(target, thisArg, args) {
|
|
77
|
+
const result = target.apply(thisArg, args);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Override state setter to trigger re-render
|
|
83
|
+
const originalState = { ...componentState.$raw() };
|
|
84
|
+
Object.keys(originalState).forEach(key => {
|
|
85
|
+
let value = componentState[key];
|
|
86
|
+
Object.defineProperty(componentState, key, {
|
|
87
|
+
get() {
|
|
88
|
+
return value;
|
|
89
|
+
},
|
|
90
|
+
set(newValue) {
|
|
91
|
+
if (value !== newValue) {
|
|
92
|
+
value = newValue;
|
|
93
|
+
render();
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
enumerable: true,
|
|
97
|
+
configurable: true
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Destroy function
|
|
102
|
+
function destroy() {
|
|
103
|
+
if (isDestroyed) return;
|
|
104
|
+
|
|
105
|
+
isDestroyed = true;
|
|
106
|
+
|
|
107
|
+
// Cleanup events
|
|
108
|
+
if (cleanupEvents) cleanupEvents();
|
|
109
|
+
|
|
110
|
+
// Call onDestroy
|
|
111
|
+
if (initialLifecycle.onDestroy) {
|
|
112
|
+
initialLifecycle.onDestroy(componentState);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Remove from DOM
|
|
116
|
+
if (currentElement && currentElement.parentNode) {
|
|
117
|
+
currentElement.parentNode.removeChild(currentElement);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
currentElement = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Initial render
|
|
124
|
+
render();
|
|
125
|
+
|
|
126
|
+
// Return component API
|
|
127
|
+
return {
|
|
128
|
+
state: componentState,
|
|
129
|
+
destroy,
|
|
130
|
+
render,
|
|
131
|
+
get element() {
|
|
132
|
+
return currentElement;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Higher-level app API for cleaner component creation
|
|
139
|
+
*/
|
|
140
|
+
export const app = {
|
|
141
|
+
/**
|
|
142
|
+
* Create a component with options object
|
|
143
|
+
*/
|
|
144
|
+
create(options = {}) {
|
|
145
|
+
const {
|
|
146
|
+
root = document.getElementById('app'),
|
|
147
|
+
state: initialState = {},
|
|
148
|
+
render: renderFn,
|
|
149
|
+
onMount,
|
|
150
|
+
onUpdate,
|
|
151
|
+
onDestroy
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
if (!renderFn) {
|
|
155
|
+
throw new Error('[HumanJS] render function is required');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return createComponent(
|
|
159
|
+
root,
|
|
160
|
+
renderFn,
|
|
161
|
+
initialState,
|
|
162
|
+
{ onMount, onUpdate, onDestroy }
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Quick mount without state management
|
|
168
|
+
*/
|
|
169
|
+
mount(rootOrFn, renderFn) {
|
|
170
|
+
let root, render;
|
|
171
|
+
|
|
172
|
+
if (typeof rootOrFn === 'function') {
|
|
173
|
+
root = document.getElementById('app');
|
|
174
|
+
render = rootOrFn;
|
|
175
|
+
} else {
|
|
176
|
+
root = rootOrFn;
|
|
177
|
+
render = renderFn;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return createComponent(root, () => render(), {});
|
|
181
|
+
}
|
|
182
|
+
};
|