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/README.md ADDED
@@ -0,0 +1,437 @@
1
+ # Ultra Light Framework
2
+
3
+ [![CI](https://github.com/EvilPrime98/ultra-light-js/actions/workflows/ci.yml/badge.svg)](https://github.com/EvilPrime98/ultra-light-js/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/ultra-light.js.svg)](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