objs-core 2.0.1 → 2.0.2
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/EXAMPLES.md +80 -73
- package/package.json +1 -1
package/EXAMPLES.md
CHANGED
|
@@ -125,14 +125,14 @@ function Counter() {
|
|
|
125
125
|
|
|
126
126
|
**Objs**
|
|
127
127
|
```js
|
|
128
|
-
// State lives in the element
|
|
128
|
+
// State lives in the element; events in state — no separate .on() needed
|
|
129
|
+
let counterRef;
|
|
129
130
|
const counter = o.init({
|
|
130
131
|
name: 'Counter',
|
|
131
|
-
render: { tag: 'button', html: '0' },
|
|
132
|
+
render: { tag: 'button', html: '0', events: { click: () => counterRef.inc() } },
|
|
132
133
|
inc: ({ self }) => { self.html(+self.el.textContent + 1); },
|
|
133
134
|
}).render().appendInside('#app');
|
|
134
|
-
|
|
135
|
-
counter.on('click', () => counter.inc());
|
|
135
|
+
counterRef = counter;
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
> In React/Vue/Solid, the framework schedules a re-render when state changes.
|
|
@@ -315,33 +315,12 @@ function ProductList() {
|
|
|
315
315
|
|
|
316
316
|
**Objs**
|
|
317
317
|
```js
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
self.store.items = {};
|
|
325
|
-
products.forEach(p => {
|
|
326
|
-
const item = o.initState({ tag: 'li', html: p.name });
|
|
327
|
-
item.appendInside(self.el);
|
|
328
|
-
self.store.items[p.id] = item;
|
|
329
|
-
});
|
|
330
|
-
},
|
|
331
|
-
// Update one item — O(1), only that text node changes
|
|
332
|
-
updateName: ({ self }, { id, name }) => { self.store.items[id]?.html(name); },
|
|
333
|
-
// Remove one item — only that node is removed
|
|
334
|
-
remove: ({ self }, id) => { self.store.items[id]?.unmount(); delete self.store.items[id]; },
|
|
335
|
-
// Add one item — no list re-render
|
|
336
|
-
addItem: ({ self }, p) => {
|
|
337
|
-
const item = o.initState({ tag: 'li', html: p.name });
|
|
338
|
-
item.appendInside(self.el);
|
|
339
|
-
self.store.items[p.id] = item;
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
const list = o.init(listStates).render().appendInside('#app');
|
|
343
|
-
list.load(products);
|
|
344
|
-
list.updateName({ id: 42, name: 'New name' }); // one write
|
|
318
|
+
// One init, render(array) — one element per item (native)
|
|
319
|
+
const listStates = { name: 'ProductList', render: { tag: 'li', html: (p) => p.name } };
|
|
320
|
+
const ul = o.init({ render: { tag: 'ul' } }).render().appendInside('#app');
|
|
321
|
+
const list = o.init(listStates).render(products);
|
|
322
|
+
list.appendInside(ul.el);
|
|
323
|
+
// Re-render: list.render(products). Append/remove: list.select(i).el.remove(), then list.render(newProducts), etc.
|
|
345
324
|
```
|
|
346
325
|
|
|
347
326
|
---
|
|
@@ -405,18 +384,15 @@ function Form() {
|
|
|
405
384
|
```js
|
|
406
385
|
// The company field is a full atom — show/hide are state methods on it
|
|
407
386
|
const companyField = o.init(FieldStates).render({ name: 'company', label: 'Company name' });
|
|
408
|
-
companyField.hide();
|
|
387
|
+
companyField.hide();
|
|
409
388
|
|
|
389
|
+
// events in state — change bubbles from the input to the label root
|
|
410
390
|
const bizBox = o.initState({
|
|
411
391
|
tag: 'label', class: 'field',
|
|
412
392
|
html: '<input type="checkbox" class="biz-check"> Business account',
|
|
393
|
+
events: { change: (e) => { e.target.checked ? companyField.show() : companyField.hide(); } },
|
|
413
394
|
});
|
|
414
|
-
|
|
415
|
-
bizBox.first('.biz-check').on('change', e => {
|
|
416
|
-
e.target.checked ? companyField.show() : companyField.hide();
|
|
417
|
-
});
|
|
418
|
-
// DOM node is never removed — toggled with display:none/''
|
|
419
|
-
// To fully unmount and remount use .unmount() / .appendInside()
|
|
395
|
+
// DOM node is never removed — toggled with display:none/''. To unmount/remount use .unmount() / .appendInside()
|
|
420
396
|
```
|
|
421
397
|
|
|
422
398
|
---
|
|
@@ -549,19 +525,16 @@ function ProductList() {
|
|
|
549
525
|
const listStates = {
|
|
550
526
|
name: 'ProductList',
|
|
551
527
|
render: { tag: 'div', html: '<p class="loading">Loading…</p>' },
|
|
552
|
-
// Called by the loader when data arrives — success state
|
|
553
528
|
load: ({ self }, products) => {
|
|
554
529
|
self.el.innerHTML = '';
|
|
555
|
-
|
|
530
|
+
o.init({ render: { tag: 'p', html: (p) => p.name } }).render(products).appendInside(self.el);
|
|
556
531
|
},
|
|
557
|
-
|
|
558
|
-
loadFailed: ({ self }) => { self.first('.loading').html('Failed to load. Retry?'); },
|
|
532
|
+
loadFailed: ({ self }) => { self.el.innerHTML = '<p class="loading">Failed to load. Retry?</p>'; },
|
|
559
533
|
};
|
|
560
534
|
|
|
561
535
|
const list = o.init(listStates).render().appendInside('#app');
|
|
562
536
|
const loader = o.newLoader(o.get('/api/products'));
|
|
563
537
|
list.connect(loader, 'load', 'loadFailed');
|
|
564
|
-
// loader fires the request; .connect wires success → load, failure → loadFailed
|
|
565
538
|
```
|
|
566
539
|
|
|
567
540
|
---
|
|
@@ -576,14 +549,14 @@ list.connect(loader, 'load', 'loadFailed');
|
|
|
576
549
|
| Update a value | `setV(newVal)` → re-render | `v.value = newVal` → patch | `setV(newVal)` → fine patch | `comp.setState(newVal)` → direct write |
|
|
577
550
|
| Read in template | `{v}` | `{{ v }}` | `{v()}` | Not needed — state methods write directly |
|
|
578
551
|
| Props | function parameter | `defineProps()` | function parameter | `render(props)` argument |
|
|
579
|
-
| Events | `onClick={handler}` | `@click="handler"` | `onClick={handler}` | `.on('click', handler)` |
|
|
552
|
+
| Events | `onClick={handler}` | `@click="handler"` | `onClick={handler}` | `events: { click }` in state or `.on('click', handler)` |
|
|
580
553
|
| Child ref | `useRef()` | `ref="name"` | `let el` / `ref` | `self.store.child = childInstance` |
|
|
581
554
|
| Lifecycle: mount | `useEffect(fn, [])` | `onMounted(fn)` | `onMount(fn)` | `init` state method called after render |
|
|
582
555
|
| Lifecycle: unmount | `useEffect` return fn | `onBeforeUnmount(fn)` | `onCleanup(fn)` | `comp.unmount()` |
|
|
583
556
|
| Shared state | Context / Zustand | Pinia | Signals / store | Plain observer or `o.connectRedux` |
|
|
584
557
|
| Fetch on mount | `useEffect` + `useState` | `onMounted` + `ref` | `createResource` | `o.newLoader` + `.connect()` |
|
|
585
558
|
| Conditional render | `{flag && <El/>}` | `v-if="flag"` | `<Show when={flag}>` | `comp.show()` / `comp.hide()` |
|
|
586
|
-
| List render | `arr.map(x => <El key>)` | `v-for="x in arr"` | `<For each={arr}>` | `arr
|
|
559
|
+
| List render | `arr.map(x => <El key>)` | `v-for="x in arr"` | `<For each={arr}>` | `render(arr)` — one element per item; or forEach in a state method |
|
|
587
560
|
| CSS class toggle | `className={flag?'a':'b'}` | `:class="{a: flag}"` | `classList={{a: flag}}` | `comp.toggleClass('a', flag)` |
|
|
588
561
|
|
|
589
562
|
---
|
|
@@ -718,6 +691,7 @@ grid.reconcile(cards);
|
|
|
718
691
|
Add a `ref="name"` attribute to any element in an HTML-string render. After `init`, every such element is available on the component as `component.refs.name` — an ObjsInstance wrapper, not a raw DOM node.
|
|
719
692
|
|
|
720
693
|
```js
|
|
694
|
+
let cardRef;
|
|
721
695
|
const cardStates = {
|
|
722
696
|
name: 'ProductCard',
|
|
723
697
|
render: ({ title, price }) => ({
|
|
@@ -726,8 +700,8 @@ const cardStates = {
|
|
|
726
700
|
html: `<h3 ref="title">${title}</h3>
|
|
727
701
|
<p ref="price">$${price}</p>
|
|
728
702
|
<button ref="addBtn">Add to cart</button>`,
|
|
703
|
+
events: { click: (e) => { if (e.target.closest('button')) cardRef?.setAdded(); } },
|
|
729
704
|
}),
|
|
730
|
-
// Destructure refs for clean, selector-free access
|
|
731
705
|
setAdded: ({ self }) => {
|
|
732
706
|
const { addBtn } = self.refs;
|
|
733
707
|
addBtn.html('✓ Added').attr('disabled', '');
|
|
@@ -738,9 +712,8 @@ const cardStates = {
|
|
|
738
712
|
};
|
|
739
713
|
|
|
740
714
|
const card = o.init(cardStates).render({ title: 'Widget', price: 9.99 }).appendInside('#app');
|
|
741
|
-
|
|
742
|
-
// card.refs.title
|
|
743
|
-
card.refs.addBtn.on('click', () => card.setAdded());
|
|
715
|
+
cardRef = card;
|
|
716
|
+
// card.refs.addBtn, card.refs.title — refs for selector-free updates
|
|
744
717
|
```
|
|
745
718
|
|
|
746
719
|
> In React, `useRef` accesses a single DOM node. Objs `refs` auto-collects all named children at init time — no `useRef` call per element, no `ref={myRef}` on every JSX tag.
|
|
@@ -826,9 +799,41 @@ const FieldStates = {
|
|
|
826
799
|
|
|
827
800
|
## 3. Nesting & composition
|
|
828
801
|
|
|
829
|
-
|
|
802
|
+
Four patterns for building composite components. Choose based on how the parent and children relate.
|
|
803
|
+
|
|
804
|
+
### Pattern A — refs (`ref` attribute in innerHTML)
|
|
805
|
+
|
|
806
|
+
Best for: a single component whose render output has named regions you need to update or wire (buttons, labels, blocks). No child components — just one root element with marked descendants.
|
|
807
|
+
|
|
808
|
+
Add `ref="name"` to elements in the `html` string. After init, `self.refs.name` is an ObjsInstance for that element — use it in state methods for selector-free updates and events.
|
|
809
|
+
|
|
810
|
+
```js
|
|
811
|
+
let cardRef;
|
|
812
|
+
const cardStates = {
|
|
813
|
+
name: 'ProductCard',
|
|
814
|
+
render: ({ title, price }) => ({
|
|
815
|
+
tag: 'article',
|
|
816
|
+
className: 'card',
|
|
817
|
+
html: `<h3 ref="title">${title}</h3>
|
|
818
|
+
<p ref="price">$${price}</p>
|
|
819
|
+
<button ref="addBtn">Add to cart</button>`,
|
|
820
|
+
events: { click: (e) => { if (e.target.closest('button')) cardRef?.setAdded(); } },
|
|
821
|
+
}),
|
|
822
|
+
setAdded: ({ self }) => {
|
|
823
|
+
self.refs.addBtn.html('✓ Added').attr('disabled', '');
|
|
824
|
+
},
|
|
825
|
+
updatePrice: ({ self }, newPrice) => {
|
|
826
|
+
self.refs.price.html('$' + newPrice);
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const card = o.init(cardStates).render({ title: 'Widget', price: 9.99 }).appendInside('#app');
|
|
831
|
+
cardRef = card;
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
**When to update:** call `self.refs.name.html(...)`, `.attr()`, `.on()`, etc. No selectors, no child components to mount.
|
|
830
835
|
|
|
831
|
-
### Pattern
|
|
836
|
+
### Pattern B — Slot pattern
|
|
832
837
|
|
|
833
838
|
Best for: containers with named regions (cards, dialogs, panels, toolbars).
|
|
834
839
|
|
|
@@ -885,7 +890,7 @@ title.setLabel('New Product Name'); // only touches card__header's <button>
|
|
|
885
890
|
price.setCount(39); // only touches card__body's <span>
|
|
886
891
|
```
|
|
887
892
|
|
|
888
|
-
### Pattern
|
|
893
|
+
### Pattern C — append in render
|
|
889
894
|
|
|
890
895
|
Best for: molecules assembled from known atoms at creation time. Children are immutable after render.
|
|
891
896
|
|
|
@@ -927,9 +932,11 @@ const searchBar = createSearchBar().appendInside('.toolbar');
|
|
|
927
932
|
document.addEventListener('search', (e) => console.log('query:', e.detail));
|
|
928
933
|
```
|
|
929
934
|
|
|
930
|
-
### Pattern
|
|
935
|
+
### Pattern D — factory function with lazy child creation
|
|
936
|
+
|
|
937
|
+
Best for: dynamic lists with **complex per-item components** (e.g. cards with slots), O(1) per-item updates, or when children change at runtime.
|
|
931
938
|
|
|
932
|
-
|
|
939
|
+
For **simple lists** (one element per item, e.g. `<li>` text), use native `render(array)` and `.appendInside(ul.el)` — see Pattern 5.
|
|
933
940
|
|
|
934
941
|
Children are created inside a state method (not in render). The parent stores all child references and exposes methods to add, update, or remove individual items.
|
|
935
942
|
|
|
@@ -1221,7 +1228,8 @@ const productCardStates = {
|
|
|
1221
1228
|
},
|
|
1222
1229
|
};
|
|
1223
1230
|
|
|
1224
|
-
// ── Product list (
|
|
1231
|
+
// ── Product list (complex items: each row is a card with button; store refs for O(1) updates)
|
|
1232
|
+
// For a simple list (e.g. just names), use render(products) and appendInside(ul.el) — see Pattern 5.
|
|
1225
1233
|
const productListStates = {
|
|
1226
1234
|
name: 'ProductList',
|
|
1227
1235
|
render: { tag: 'div', class: 'card-list' },
|
|
@@ -1230,11 +1238,7 @@ const productListStates = {
|
|
|
1230
1238
|
products.forEach((product) => {
|
|
1231
1239
|
const card = o.init(productCardStates).render(product);
|
|
1232
1240
|
card.appendInside(self.el);
|
|
1233
|
-
|
|
1234
|
-
card.first('.card__btn').on('click', () => {
|
|
1235
|
-
cartAdd(product);
|
|
1236
|
-
card.setAdded(); // only this card's button changes
|
|
1237
|
-
});
|
|
1241
|
+
card.first('.card__btn').on('click', () => { cartAdd(product); card.setAdded(); });
|
|
1238
1242
|
self.store.cards[product.id] = card;
|
|
1239
1243
|
});
|
|
1240
1244
|
},
|
|
@@ -1251,6 +1255,7 @@ Promo dialog triggered by `?promo=CODE` URL parameter. sessionStorage prevents r
|
|
|
1251
1255
|
```js
|
|
1252
1256
|
const PROMO_KEY = 'oTest-promo-shown';
|
|
1253
1257
|
|
|
1258
|
+
let dialogRef;
|
|
1254
1259
|
const dialogStates = {
|
|
1255
1260
|
name: 'PromoDialog',
|
|
1256
1261
|
render: {
|
|
@@ -1261,6 +1266,9 @@ const dialogStates = {
|
|
|
1261
1266
|
<p class="dialog__body"></p>
|
|
1262
1267
|
<a class="dialog__cta" href="#">Get offer</a>
|
|
1263
1268
|
</div>`,
|
|
1269
|
+
events: {
|
|
1270
|
+
click: (e) => { if (e.target.closest('.dialog__close') || e.target === dialogRef?.el) dialogRef?.close(); },
|
|
1271
|
+
},
|
|
1264
1272
|
},
|
|
1265
1273
|
open: ({ self }, { title, body, cta, ctaUrl }) => {
|
|
1266
1274
|
self.first('.dialog__title').html(title);
|
|
@@ -1273,8 +1281,7 @@ const dialogStates = {
|
|
|
1273
1281
|
};
|
|
1274
1282
|
|
|
1275
1283
|
const dialog = o.init(dialogStates).render().appendInside('body');
|
|
1276
|
-
|
|
1277
|
-
dialog.on('click', (e) => { if (e.target === dialog.el) dialog.close(); });
|
|
1284
|
+
dialogRef = dialog;
|
|
1278
1285
|
|
|
1279
1286
|
const promoCode = o.getParams('promo');
|
|
1280
1287
|
if (promoCode && !sessionStorage.getItem(PROMO_KEY)) {
|
|
@@ -1299,6 +1306,7 @@ const applyFilters = (filters, productsLoader) => {
|
|
|
1299
1306
|
productsLoader.reload(o.get('/api/products?' + params.toString()));
|
|
1300
1307
|
};
|
|
1301
1308
|
|
|
1309
|
+
let drawerRef;
|
|
1302
1310
|
const drawerStates = {
|
|
1303
1311
|
name: 'FilterDrawer',
|
|
1304
1312
|
render: {
|
|
@@ -1310,6 +1318,15 @@ const drawerStates = {
|
|
|
1310
1318
|
<input class="drawer__max" type="number" placeholder="Max $">
|
|
1311
1319
|
<button class="drawer__apply">Apply</button>
|
|
1312
1320
|
<button class="drawer__reset">Reset</button>`,
|
|
1321
|
+
events: {
|
|
1322
|
+
click: (e) => {
|
|
1323
|
+
const d = drawerRef;
|
|
1324
|
+
if (!d) return;
|
|
1325
|
+
if (e.target.closest('.drawer__close')) d.close();
|
|
1326
|
+
else if (e.target.closest('.drawer__apply')) { applyFilters(d.getValues(), productsLoader); d.close(); }
|
|
1327
|
+
else if (e.target.closest('.drawer__reset')) { applyFilters({ category: '', minPrice: '', maxPrice: '' }, productsLoader); d.restore({ category: '', minPrice: '', maxPrice: '' }); }
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1313
1330
|
},
|
|
1314
1331
|
open: ({ self }) => { self.css({ transform: 'translateX(0)' }); },
|
|
1315
1332
|
close: ({ self }) => { self.css({ transform: 'translateX(-100%)' }); },
|
|
@@ -1326,19 +1343,9 @@ const drawerStates = {
|
|
|
1326
1343
|
};
|
|
1327
1344
|
|
|
1328
1345
|
const drawer = o.init(drawerStates).render().appendInside('body');
|
|
1346
|
+
drawerRef = drawer;
|
|
1329
1347
|
drawer.restore(getFilters());
|
|
1330
1348
|
|
|
1331
|
-
drawer.first('.drawer__close').on('click', () => drawer.close());
|
|
1332
|
-
drawer.first('.drawer__apply').on('click', () => {
|
|
1333
|
-
applyFilters(drawer.getValues(), productsLoader);
|
|
1334
|
-
drawer.close();
|
|
1335
|
-
});
|
|
1336
|
-
drawer.first('.drawer__reset').on('click', () => {
|
|
1337
|
-
const empty = { category: '', minPrice: '', maxPrice: '' };
|
|
1338
|
-
applyFilters(empty, productsLoader);
|
|
1339
|
-
drawer.restore(empty);
|
|
1340
|
-
});
|
|
1341
|
-
|
|
1342
1349
|
o.first('#open-filters').on('click', () => drawer.open());
|
|
1343
1350
|
```
|
|
1344
1351
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "objs-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightweight (~6 kB) library for fast, AI-friendly front-end development: samples and state control, built-in store (createStore), routing, caching, and recording → Playwright tests. No build step; split design into samples and give them data and actions. Works standalone or alongside React; SSR and hydrate from server-rendered DOM. v2.0: exportPlaywrightTest(), refs, TypeScript definitions, recording in all builds.",
|
|
6
6
|
"homepage": "https://en.fous.name/objs/",
|