ultra-light-js 1.0.18
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.md +674 -0
- package/README.md +437 -0
- package/dist/jsx.d.ts +684 -0
- package/dist/jsx.d.ts.map +1 -0
- package/dist/jsx.js +109 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/ultra-light.d.ts +71 -0
- package/dist/ultra-light.d.ts.map +1 -0
- package/dist/ultra-light.js +816 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Ultra Light Framework
|
|
2
|
+
|
|
3
|
+
[](https://github.com/EvilPrime98/ultra-light-js/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/ultra-light.js)
|
|
5
|
+
|
|
6
|
+
An ultra-lightweight and reactive mini framework for modern web development, with zero dependencies.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Reactive State Management**
|
|
11
|
+
- **SPA Router**
|
|
12
|
+
- **Component System**
|
|
13
|
+
- **Scoped CSS**
|
|
14
|
+
- **Context API**
|
|
15
|
+
- **Zero dependencies**
|
|
16
|
+
- **Ultra lightweight** - Less than 5KB minified
|
|
17
|
+
- **TypeScript** - Fully typed
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install ultra-light.js
|
|
23
|
+
# or
|
|
24
|
+
pnpm add ultra-light.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
```javascript
|
|
29
|
+
import { ultraState, UltraComponent } from 'ultra-light.js';
|
|
30
|
+
|
|
31
|
+
function Counter(){
|
|
32
|
+
|
|
33
|
+
//create a state
|
|
34
|
+
const [getCount, setCount, subscribe] = ultraState(0);
|
|
35
|
+
|
|
36
|
+
//this function will execute when the state changes, and by default
|
|
37
|
+
//it receives a reference to the trigger owner as the first argument.
|
|
38
|
+
//this allows for granular DOM manipulation
|
|
39
|
+
function onIncrement(
|
|
40
|
+
$h1
|
|
41
|
+
){
|
|
42
|
+
$h1.innerHTML = `Counter:${getCount()}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return UltraComponent({
|
|
46
|
+
|
|
47
|
+
component: '<div></div>', //this is the root node. It accepts children in plain HTML
|
|
48
|
+
|
|
49
|
+
children: [
|
|
50
|
+
|
|
51
|
+
UltraComponent({
|
|
52
|
+
component: `<h1>Counter:${getCount()}</h1>`,
|
|
53
|
+
trigger: [ //this is where the magic happens! when the subscriber state changes, the trigger function is executed
|
|
54
|
+
{ subscriber: subscribe, triggerFunction: onIncrement}
|
|
55
|
+
]
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
UltraComponent({
|
|
59
|
+
component: '<button>Increment</button>',
|
|
60
|
+
eventHandler: { click: () => setCount(getCount() + 1) }
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
This is a work in progress. The API will change frequently. Contributions are welcome! Please open an issue or PR.
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### ultraState(initialValue)
|
|
78
|
+
|
|
79
|
+
Creates a reactive state.
|
|
80
|
+
```javascript
|
|
81
|
+
const [getValue, setValue, subscribe] = ultraState(0);
|
|
82
|
+
|
|
83
|
+
// Get value
|
|
84
|
+
console.log(getValue()); // 0
|
|
85
|
+
|
|
86
|
+
// Set value
|
|
87
|
+
setValue(5);
|
|
88
|
+
|
|
89
|
+
// Subscribe to changes
|
|
90
|
+
const unsubscribe = subscribe((newValue) => {
|
|
91
|
+
console.log('New value:', newValue);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Unsubscribe
|
|
95
|
+
unsubscribe();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### ultraScope(fn)
|
|
99
|
+
|
|
100
|
+
Runs `fn` inside an implicit owner scope: any `ultraState`/`ultraCompState` subscription made synchronously during `fn`'s execution is auto-registered for disposal, so you don't have to manually collect and thread unsubscribe functions through a `cleanup` array. `UltraRouter` uses this internally to dispose route-scoped subscriptions on navigation, but it's also exported for apps that mount components outside the router and want the same guarantee.
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
const [items, setItems, subscribeItems] = ultraState([]);
|
|
104
|
+
|
|
105
|
+
const [result, disposeScope] = ultraScope(() => {
|
|
106
|
+
// any subscription made synchronously here is auto-registered
|
|
107
|
+
subscribeItems((value) => console.log('items changed:', value));
|
|
108
|
+
return 'some result';
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// later, tear down every subscription made inside the scope at once
|
|
112
|
+
disposeScope();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Only subscriptions made **synchronously** inside `fn` are captured — anything subscribed after an `await`, inside an event handler, or in a `setTimeout` runs with no active scope and still needs explicit `cleanup`/`trigger` wiring.
|
|
116
|
+
|
|
117
|
+
### ultraCompState(initialComp)
|
|
118
|
+
|
|
119
|
+
Creates a composite stateful object. Each key becomes a reactive state with `get`, `set`, and `subscribe`. Functions receive the composite state object as the first argument, allowing methods to access other state values.
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
const store = ultraCompState({
|
|
123
|
+
count: 0,
|
|
124
|
+
name: 'John',
|
|
125
|
+
// functions become methods on the store
|
|
126
|
+
increment: (state) => state.count.set(state.count.get() + 1),
|
|
127
|
+
reset: (state) => {
|
|
128
|
+
state.count.set(0);
|
|
129
|
+
state.name.set('John');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Access state
|
|
134
|
+
store.count.get(); // 0
|
|
135
|
+
store.count.set(5);
|
|
136
|
+
store.count.subscribe((v) => console.log(v));
|
|
137
|
+
|
|
138
|
+
// Call methods
|
|
139
|
+
store.increment();
|
|
140
|
+
store.reset();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### UltraContext(initialValue, displayName?)
|
|
144
|
+
|
|
145
|
+
Creates a scoped context that can be owned by a specific DOM node. All methods require the calling node as the first argument. If the node is not a descendant of the owner, the operation is blocked.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const ThemeContext = UltraContext('light', 'ThemeContext');
|
|
149
|
+
|
|
150
|
+
// Assign an owner node (can only be set once)
|
|
151
|
+
ThemeContext.own(myRootNode);
|
|
152
|
+
|
|
153
|
+
// Set value (requires caller node to be inside owner)
|
|
154
|
+
ThemeContext.set(myNode, 'dark');
|
|
155
|
+
|
|
156
|
+
// Get value
|
|
157
|
+
console.log(ThemeContext.get(myNode)); // 'dark'
|
|
158
|
+
|
|
159
|
+
// Subscribe
|
|
160
|
+
const unsub = ThemeContext.subscribe(myNode, (theme) => {
|
|
161
|
+
console.log('Theme changed:', theme);
|
|
162
|
+
});
|
|
163
|
+
unsub(); // unsubscribe
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### ultraStyles(cssString)
|
|
167
|
+
|
|
168
|
+
Creates automatically scoped styles. Returns a map of original class names to hashed class names.
|
|
169
|
+
```javascript
|
|
170
|
+
const styles = ultraStyles(`
|
|
171
|
+
.container {
|
|
172
|
+
padding: 20px;
|
|
173
|
+
background: white;
|
|
174
|
+
}
|
|
175
|
+
.title {
|
|
176
|
+
color: blue;
|
|
177
|
+
font-size: 24px;
|
|
178
|
+
}
|
|
179
|
+
`);
|
|
180
|
+
|
|
181
|
+
// Use scoped classes
|
|
182
|
+
const Component = `<div class="${styles.container}">
|
|
183
|
+
<h1 class="${styles.title}">Title</h1>
|
|
184
|
+
</div>`;
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### ultraStyles2(cssObject, document?)
|
|
188
|
+
|
|
189
|
+
Like `ultraStyles`, but takes a CSS-in-JS object (camelCase properties) instead of a CSS string. Returns a map of original keys to hashed class names.
|
|
190
|
+
```javascript
|
|
191
|
+
const styles = ultraStyles2({
|
|
192
|
+
container: { display: 'flex' },
|
|
193
|
+
title: { fontSize: '1rem', color: 'blue' }
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const Component = `<div class="${styles.container}">
|
|
197
|
+
<h1 class="${styles.title}">Title</h1>
|
|
198
|
+
</div>`;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### ultraQueryParams()
|
|
202
|
+
|
|
203
|
+
Gets URL search parameters as a plain object.
|
|
204
|
+
```javascript
|
|
205
|
+
// URL: ?name=John&age=30
|
|
206
|
+
const params = ultraQueryParams();
|
|
207
|
+
console.log(params); // { name: 'John', age: '30' }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### ultraQuery()
|
|
211
|
+
|
|
212
|
+
Creates a fetcher with built-in caching, request de-duplication, and stale-time invalidation.
|
|
213
|
+
```javascript
|
|
214
|
+
const { fetch, isFetching, hasError, cache, invalidateCache, subscribeToCache } = ultraQuery();
|
|
215
|
+
|
|
216
|
+
const result = await fetch('user-1', () => window.fetch(`/api/users/1`).then(r => r.json()), 60 * 1000);
|
|
217
|
+
// result.data, result.isFetching(), result.hasError()
|
|
218
|
+
|
|
219
|
+
// Concurrent calls with the same key are de-duplicated, and results are cached
|
|
220
|
+
// until manually invalidated or the staleTime (ms, default 5 minutes) elapses.
|
|
221
|
+
invalidateCache('user-1');
|
|
222
|
+
|
|
223
|
+
subscribeToCache(() => console.log('cache changed:', cache()));
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### UltraRouter(...routes)
|
|
227
|
+
|
|
228
|
+
Creates a router for SPA navigation.
|
|
229
|
+
```javascript
|
|
230
|
+
import { UltraRouter, UltraLink } from 'ultra-light.js';
|
|
231
|
+
|
|
232
|
+
const Home = () => '<div><h1>Home</h1></div>';
|
|
233
|
+
const About = () => '<div><h1>About</h1></div>';
|
|
234
|
+
const User = (params) => `<div><h1>User: ${params.id}</h1></div>`;
|
|
235
|
+
|
|
236
|
+
const router = UltraRouter(
|
|
237
|
+
{ path: '/', component: Home },
|
|
238
|
+
{ path: '/about', component: About },
|
|
239
|
+
{ path: '/user/:id', component: User },
|
|
240
|
+
{ path: '/*', component: () => '<h1>404 Not Found</h1>' }
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
document.body.appendChild(router);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Each route's `component` function is run inside its own [`ultraScope`](#ultrascopefn), so any `ultraState`/`ultraCompState` subscription made synchronously inside it is automatically disposed on navigation or when the router is cleaned up — no need to manually collect and pass those subscriptions into a `cleanup` array.
|
|
247
|
+
|
|
248
|
+
### UltraLink({ href, child })
|
|
249
|
+
|
|
250
|
+
Creates SPA navigation links. Ctrl/Meta+click opens in a new tab normally.
|
|
251
|
+
```javascript
|
|
252
|
+
const link = UltraLink({
|
|
253
|
+
href: '/about',
|
|
254
|
+
child: '<span>Go to About</span>'
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### ultraNavigate({ href, viewTransition? })
|
|
259
|
+
|
|
260
|
+
Navigates programmatically within a `UltraRouter` context (pushes history state, scrolls to top, dispatches `popstate`). Use this when you need to navigate outside of a click handler, e.g. `UltraLink` uses it internally.
|
|
261
|
+
```javascript
|
|
262
|
+
ultraNavigate({ href: '/dashboard' });
|
|
263
|
+
|
|
264
|
+
// Use the View Transition API when available, falling back to a direct
|
|
265
|
+
// navigation otherwise.
|
|
266
|
+
ultraNavigate({ href: '/dashboard', viewTransition: true });
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### UltraComponent(props)
|
|
270
|
+
|
|
271
|
+
Creates a component with event handlers, styles, class names, children, reactive triggers, lifecycle hooks, and cleanup.
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
const Button = UltraComponent({
|
|
275
|
+
component: '<button>Click me</button>',
|
|
276
|
+
|
|
277
|
+
// Event handlers - object with event names as keys
|
|
278
|
+
eventHandler: {
|
|
279
|
+
click: () => alert('Clicked!'),
|
|
280
|
+
mouseenter: () => console.log('hovered')
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
// Inline styles
|
|
284
|
+
styles: {
|
|
285
|
+
backgroundColor: 'blue',
|
|
286
|
+
color: 'white'
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// CSS class names to add
|
|
290
|
+
className: ['btn', 'btn-primary'],
|
|
291
|
+
|
|
292
|
+
// Child elements (strings, HTMLElements, or null for conditional rendering)
|
|
293
|
+
children: ['<span>Icon</span>', null],
|
|
294
|
+
|
|
295
|
+
// Reactive triggers - run when state changes
|
|
296
|
+
trigger: [{
|
|
297
|
+
subscriber: subscribe,
|
|
298
|
+
triggerFunction: (node) => {
|
|
299
|
+
node.querySelector('#count').textContent = getCount();
|
|
300
|
+
},
|
|
301
|
+
defer: false // set true to defer to next animation frame
|
|
302
|
+
}],
|
|
303
|
+
|
|
304
|
+
// Called immediately after mount (next animation frame)
|
|
305
|
+
onMount: [(node) => node.focus()],
|
|
306
|
+
|
|
307
|
+
// Cleanup functions called on component teardown
|
|
308
|
+
cleanup: [() => clearInterval(myInterval)]
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### UltraActivity(props)
|
|
313
|
+
|
|
314
|
+
Shows or hides an element based on state. Shares the same props as `UltraComponent`, plus `mode` and `type`.
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
const [isVisible, setVisible, subscribeVisible] = ultraState(true);
|
|
318
|
+
|
|
319
|
+
const ConditionalDiv = UltraActivity({
|
|
320
|
+
component: '<div>Only visible when isVisible is true</div>',
|
|
321
|
+
|
|
322
|
+
// mode controls visibility
|
|
323
|
+
mode: {
|
|
324
|
+
state: isVisible, // getter function returning boolean
|
|
325
|
+
subscriber: subscribeVisible // or an array of subscribers
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// 'display' (default) toggles display:none | ''
|
|
329
|
+
// 'visibility' toggles visibility:hidden | visible
|
|
330
|
+
type: 'display'
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### UltraFragment(...children)
|
|
335
|
+
|
|
336
|
+
Groups multiple elements into a `DocumentFragment` without a wrapper node. Accepts `null` values for conditional rendering.
|
|
337
|
+
```javascript
|
|
338
|
+
const fragment = UltraFragment(
|
|
339
|
+
'<div>Element 1</div>',
|
|
340
|
+
someCondition ? '<div>Element 2</div>' : null,
|
|
341
|
+
'<div>Element 3</div>'
|
|
342
|
+
);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### ultraPortal(app, portal)
|
|
346
|
+
|
|
347
|
+
Inserts a component directly after a given application root element, outside of the normal component tree. Useful for modals, tooltips, or anything that needs to escape a parent's `overflow`/`z-index` stacking context. Throws if the app element or the portal content can't be resolved.
|
|
348
|
+
```javascript
|
|
349
|
+
// app: a CSS selector or an HTMLElement identifying the mount point
|
|
350
|
+
ultraPortal('#app', '<div class="modal">Hello from a portal</div>');
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Examples
|
|
354
|
+
|
|
355
|
+
### Complete Todo App
|
|
356
|
+
```javascript
|
|
357
|
+
import { ultraState, UltraComponent, ultraStyles } from 'ultra-light.js';
|
|
358
|
+
|
|
359
|
+
const styles = ultraStyles(`
|
|
360
|
+
|
|
361
|
+
.todo-app {
|
|
362
|
+
max-width: 600px;
|
|
363
|
+
margin: 0 auto;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.todo-item {
|
|
367
|
+
padding: 10px;
|
|
368
|
+
border: 1px solid #ddd;
|
|
369
|
+
margin: 5px 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
`);
|
|
373
|
+
|
|
374
|
+
function TodoApp(){
|
|
375
|
+
|
|
376
|
+
const [getTodos, setTodos, subscribeTodos] = ultraState([]);
|
|
377
|
+
const [getInput, setInput, subscribeInput] = ultraState('');
|
|
378
|
+
|
|
379
|
+
function onListChange($list){
|
|
380
|
+
$list.innerHTML = getTodos()
|
|
381
|
+
.map(todo => `<div class="${styles['todo-item']}">${todo.text}</div>`)
|
|
382
|
+
.join('');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function onInputChange($input){
|
|
386
|
+
$input.value = getInput();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return UltraComponent({
|
|
390
|
+
|
|
391
|
+
component: '<div></div>',
|
|
392
|
+
|
|
393
|
+
className: [styles['todo-app']],
|
|
394
|
+
|
|
395
|
+
children: [
|
|
396
|
+
|
|
397
|
+
UltraComponent({
|
|
398
|
+
component: '<input type="text" placeholder="New task..." />',
|
|
399
|
+
eventHandler: { input: (e) => setInput(e.target.value) },
|
|
400
|
+
trigger: [{ subscriber: subscribeInput, triggerFunction: onInputChange }]
|
|
401
|
+
}),
|
|
402
|
+
|
|
403
|
+
UltraComponent({
|
|
404
|
+
component: '<button>Add</button>',
|
|
405
|
+
eventHandler: {
|
|
406
|
+
click: () => {
|
|
407
|
+
if (getInput().trim()) {
|
|
408
|
+
setTodos([...getTodos(), { id: Date.now(), text: getInput() }]);
|
|
409
|
+
setInput('');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}),
|
|
414
|
+
|
|
415
|
+
UltraComponent({
|
|
416
|
+
component: '<div></div>',
|
|
417
|
+
trigger: [{ subscriber: subscribeTodos, triggerFunction: onListChange }]
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
]
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
document.body.appendChild(
|
|
426
|
+
TodoApp()
|
|
427
|
+
);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
GPL-3.0
|
|
434
|
+
|
|
435
|
+
## Author
|
|
436
|
+
|
|
437
|
+
Amin Perez Alconchel
|