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.
Files changed (2) hide show
  1. package/EXAMPLES.md +80 -73
  2. 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, not in a reactive variable
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
- const listStates = {
319
- name: 'ProductList',
320
- render: { tag: 'ul' },
321
- // Initial load — creates all items, stores by ID
322
- load: ({ self }, products) => {
323
- self.el.innerHTML = '';
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(); // display:none on the atom's root element
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
- products.forEach(p => o.initState({ tag: 'p', html: p.name }).appendInside(self.el));
530
+ o.init({ render: { tag: 'p', html: (p) => p.name } }).render(products).appendInside(self.el);
556
531
  },
557
- // Called by the loader on network error fail state
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.forEach` in a state method |
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
- // card.refs.addBtn — ObjsInstance wrapping the <button ref="addBtn">
742
- // card.refs.title ObjsInstance wrapping the <h3 ref="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
- Three patterns for building composite components. Choose based on how the parent and children relate.
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 A — Slot 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 B — append in render
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 C — factory function with lazy child creation
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
- Best for: dynamic lists, data-driven components, components where children change at runtime.
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 (factory pattern children in self.store) ───────────────
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
- // Wire button no DOM query needed, card has .first()
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
- dialog.first('.dialog__close').on('click', () => dialog.close());
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.1",
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/",