objs-core 2.0.3 → 2.1.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/EXAMPLES.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Objs v2.0 — Examples & Architecture Guide
2
2
 
3
- All examples work as-is with `<script src="objs.js"></script>`.
4
- Runnable paste-and-run code: [examples.js](examples.js).
3
+ All examples work as-is with `<script src="objs.js"></script>` or `import o from 'objs-core'`.
4
+ Runnable paste-and-run code: [examples.js](examples.js). Rules and conventions: [SKILL.md](SKILL.md).
5
+
6
+ **Conventions (from SKILL):** Use **refs** and **self.select(e)** in event handlers — not `e.target`, raw DOM, or class selectors. Use `.val()` for input/textarea/select; `attr(name, null)` to remove an attribute; `css(null)` to remove the `style` attribute. One state method per data slice; never call `.render()` to update.
5
7
 
6
8
  ---
7
9
 
@@ -758,14 +760,15 @@ const BadgeStates = {
758
760
  }),
759
761
  setCount: ({ self }, n) => {
760
762
  self.html(n);
761
- // Re-apply inline style transform skips unchanged values
762
- n === 0 ? self.css({ display: 'none' }) : self.css(null);
763
+ n === 0 ? self.css({ display: 'none' }) : self.css(null); // css(null) removes style attribute
763
764
  },
764
765
  };
765
766
  ```
766
767
 
767
768
  ### Input field atom
768
769
 
770
+ Use `ref="input"` and `ref="error"` so updates use refs (no class selectors). Use `.val()` for input value.
771
+
769
772
  ```js
770
773
  const FieldStates = {
771
774
  name: 'FormField',
@@ -773,23 +776,22 @@ const FieldStates = {
773
776
  tag: 'div',
774
777
  class: 'field',
775
778
  html: `<label class="field__label">${label}</label>
776
- <input class="field__input" type="${type}" name="${name}" placeholder="${placeholder}">
777
- <span class="field__error"></span>`,
779
+ <input ref="input" type="${type}" name="${name}" placeholder="${placeholder}">
780
+ <span ref="error" class="field__error"></span>`,
778
781
  }),
779
- // Each state only touches its specific sub-element
780
782
  setError: ({ self }, msg) => {
781
- self.first('.field__input').addClass('field__input--error');
782
- self.first('.field__error').html(msg || '');
783
+ self.refs.input.addClass('field__input--error');
784
+ self.refs.error.html(msg || '');
783
785
  },
784
786
  setSuccess: ({ self }) => {
785
- self.first('.field__input').removeClass('field__input--error').addClass('field__input--ok');
786
- self.first('.field__error').html('');
787
+ self.refs.input.removeClass('field__input--error').addClass('field__input--ok');
788
+ self.refs.error.html('');
787
789
  },
788
790
  setIdle: ({ self }) => {
789
- self.first('.field__input').removeClass('field__input--error').removeClass('field__input--ok');
790
- self.first('.field__error').html('');
791
+ self.refs.input.removeClass('field__input--error').removeClass('field__input--ok');
792
+ self.refs.error.html('');
791
793
  },
792
- getValue: ({ self }) => self.first('.field__input').val(),
794
+ getValue: ({ self }) => self.refs.input.val(),
793
795
  show: ({ self }) => { self.css(null); },
794
796
  hide: ({ self }) => { self.css({ display: 'none' }); },
795
797
  };
@@ -811,16 +813,16 @@ Add `ref="name"` to elements in the `html` string. After init, `self.refs.name`
811
813
  let cardRef;
812
814
  const cardStates = {
813
815
  name: 'ProductCard',
814
- render: ({ title, price }) => ({
816
+ render: ({ self, title, price }) => ({
815
817
  tag: 'article',
816
818
  className: 'card',
817
819
  html: `<h3 ref="title">${title}</h3>
818
820
  <p ref="price">$${price}</p>
819
821
  <button ref="addBtn">Add to cart</button>`,
820
- events: { click: (e) => { if (e.target.closest('button')) cardRef?.setAdded(); } },
822
+ events: { click: (e) => { if (e.target === self.refs?.addBtn?.el) cardRef?.setAdded(); } },
821
823
  }),
822
824
  setAdded: ({ self }) => {
823
- self.refs.addBtn.html('✓ Added').attr('disabled', '');
825
+ self.refs.addBtn.html('✓ Added').attr('disabled', 'true'); // use attr('disabled', null) to remove
824
826
  },
825
827
  updatePrice: ({ self }, newPrice) => {
826
828
  self.refs.price.html('$' + newPrice);
@@ -1218,13 +1220,12 @@ const productCardStates = {
1218
1220
  render: ({ title, price }) => ({
1219
1221
  tag: 'article',
1220
1222
  class: 'card',
1221
- html: `<h3 class="card__title">${title}</h3>
1222
- <p class="card__price">$${price}</p>
1223
- <button class="card__btn">Add to cart</button>`,
1223
+ html: `<h3 ref="title">${title}</h3>
1224
+ <p ref="price">$${price}</p>
1225
+ <button ref="addBtn">Add to cart</button>`,
1224
1226
  }),
1225
- // Granular: only the button changes, card root never touched
1226
1227
  setAdded: ({ self }) => {
1227
- self.first('.card__btn').html('✓ Added').attr('disabled', '');
1228
+ self.refs.addBtn.html('✓ Added').attr('disabled', 'true');
1228
1229
  },
1229
1230
  };
1230
1231
 
@@ -1238,7 +1239,7 @@ const productListStates = {
1238
1239
  products.forEach((product) => {
1239
1240
  const card = o.init(productCardStates).render(product);
1240
1241
  card.appendInside(self.el);
1241
- card.first('.card__btn').on('click', () => { cartAdd(product); card.setAdded(); });
1242
+ card.refs.addBtn.on('click', () => { cartAdd(product); card.setAdded(); });
1242
1243
  self.store.cards[product.id] = card;
1243
1244
  });
1244
1245
  },
@@ -1261,19 +1262,19 @@ const dialogStates = {
1261
1262
  render: {
1262
1263
  tag: 'div', class: 'dialog-overlay', style: 'display:none',
1263
1264
  html: `<div class="dialog">
1264
- <button class="dialog__close">✕</button>
1265
- <h2 class="dialog__title"></h2>
1266
- <p class="dialog__body"></p>
1267
- <a class="dialog__cta" href="#">Get offer</a>
1265
+ <button ref="closeBtn">✕</button>
1266
+ <h2 ref="title"></h2>
1267
+ <p ref="body"></p>
1268
+ <a ref="cta" href="#">Get offer</a>
1268
1269
  </div>`,
1269
1270
  events: {
1270
- click: (e) => { if (e.target.closest('.dialog__close') || e.target === dialogRef?.el) dialogRef?.close(); },
1271
+ click: (e) => { if (e.target === dialogRef?.refs?.closeBtn?.el || e.target === dialogRef?.el) dialogRef?.close(); },
1271
1272
  },
1272
1273
  },
1273
1274
  open: ({ self }, { title, body, cta, ctaUrl }) => {
1274
- self.first('.dialog__title').html(title);
1275
- self.first('.dialog__body').html(body);
1276
- self.first('.dialog__cta').html(cta).attr('href', ctaUrl);
1275
+ self.refs.title.html(title);
1276
+ self.refs.body.html(body);
1277
+ self.refs.cta.html(cta).attr('href', ctaUrl);
1277
1278
  self.css({ display: 'flex' });
1278
1279
  sessionStorage.setItem(PROMO_KEY, '1');
1279
1280
  },
@@ -1311,34 +1312,34 @@ const drawerStates = {
1311
1312
  name: 'FilterDrawer',
1312
1313
  render: {
1313
1314
  tag: 'aside', class: 'drawer', style: 'transform:translateX(-100%)',
1314
- html: `<button class="drawer__close">✕</button>
1315
+ html: `<button ref="closeBtn">✕</button>
1315
1316
  <h3>Filters</h3>
1316
- <select class="drawer__cat"><option value="">All</option><option value="electronics">Electronics</option></select>
1317
- <input class="drawer__min" type="number" placeholder="Min $">
1318
- <input class="drawer__max" type="number" placeholder="Max $">
1319
- <button class="drawer__apply">Apply</button>
1320
- <button class="drawer__reset">Reset</button>`,
1317
+ <select ref="cat"><option value="">All</option><option value="electronics">Electronics</option></select>
1318
+ <input ref="min" type="number" placeholder="Min $">
1319
+ <input ref="max" type="number" placeholder="Max $">
1320
+ <button ref="applyBtn">Apply</button>
1321
+ <button ref="resetBtn">Reset</button>`,
1321
1322
  events: {
1322
1323
  click: (e) => {
1323
1324
  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: '' }); }
1325
+ if (!d?.refs) return;
1326
+ if (e.target === d.refs.closeBtn.el) d.close();
1327
+ else if (e.target === d.refs.applyBtn.el) { applyFilters(d.getValues(), productsLoader); d.close(); }
1328
+ else if (e.target === d.refs.resetBtn.el) { applyFilters({ category: '', minPrice: '', maxPrice: '' }, productsLoader); d.restore({ category: '', minPrice: '', maxPrice: '' }); }
1328
1329
  },
1329
1330
  },
1330
1331
  },
1331
1332
  open: ({ self }) => { self.css({ transform: 'translateX(0)' }); },
1332
1333
  close: ({ self }) => { self.css({ transform: 'translateX(-100%)' }); },
1333
1334
  restore: ({ self }, { category, minPrice, maxPrice }) => {
1334
- self.first('.drawer__cat').val(category);
1335
- self.first('.drawer__min').val(minPrice);
1336
- self.first('.drawer__max').val(maxPrice);
1335
+ self.refs.cat.val(category);
1336
+ self.refs.min.val(minPrice);
1337
+ self.refs.max.val(maxPrice);
1337
1338
  },
1338
1339
  getValues: ({ self }) => ({
1339
- category: self.first('.drawer__cat').val(),
1340
- minPrice: self.first('.drawer__min').val(),
1341
- maxPrice: self.first('.drawer__max').val(),
1340
+ category: self.refs.cat.val(),
1341
+ minPrice: self.refs.min.val(),
1342
+ maxPrice: self.refs.max.val(),
1342
1343
  }),
1343
1344
  };
1344
1345
 
@@ -1387,7 +1388,7 @@ const formStates = {
1387
1388
  name: 'RegistrationForm',
1388
1389
  render: {
1389
1390
  tag: 'form', class: 'form',
1390
- html: '<div class="form__fields"></div><div class="form__preview"></div><button type="submit" class="btn btn--primary" disabled>Submit</button>',
1391
+ html: '<div class="form__fields"></div><div class="form__preview"></div><button ref="submitBtn" type="submit" class="btn btn--primary" disabled>Submit</button>',
1391
1392
  },
1392
1393
  init: ({ self }) => {
1393
1394
  const fieldsRoot = self.first('.form__fields').el;
@@ -1408,7 +1409,7 @@ const formStates = {
1408
1409
  checkSubmit: ({ self }) => {
1409
1410
  const v = self.store.valid;
1410
1411
  const ok = v.email && v.name && (!v.isBiz || v.company);
1411
- self.first('button[type="submit"]').el.disabled = !ok;
1412
+ self.refs.submitBtn.attr('disabled', ok ? null : 'true');
1412
1413
  },
1413
1414
  };
1414
1415
 
@@ -1422,13 +1423,13 @@ const validate = (fieldComp, rule, value) => {
1422
1423
  return res === true;
1423
1424
  };
1424
1425
 
1425
- emailField.first('input')
1426
- .on('blur', (e) => { form.store.valid.email = validate(emailField, 'email', e.target.value); form.checkSubmit(); })
1427
- .on('input', (e) => { preview.update({ name: nameField.getValue(), email: e.target.value }); });
1426
+ emailField.refs.input
1427
+ .on('blur', () => { form.store.valid.email = validate(emailField, 'email', emailField.getValue()); form.checkSubmit(); })
1428
+ .on('input', () => { preview.update({ name: nameField.getValue(), email: emailField.getValue() }); });
1428
1429
 
1429
- nameField.first('input')
1430
- .on('blur', (e) => { form.store.valid.name = validate(nameField, 'name', e.target.value); form.checkSubmit(); })
1431
- .on('input', (e) => { preview.update({ name: e.target.value, email: emailField.getValue() }); });
1430
+ nameField.refs.input
1431
+ .on('blur', () => { form.store.valid.name = validate(nameField, 'name', nameField.getValue()); form.checkSubmit(); })
1432
+ .on('input', () => { preview.update({ name: nameField.getValue(), email: emailField.getValue() }); });
1432
1433
 
1433
1434
  bizBox.first('.biz-check').on('change', (e) => {
1434
1435
  form.store.valid.isBiz = e.target.checked;
@@ -1442,12 +1443,12 @@ bizBox.first('.biz-check').on('change', (e) => {
1442
1443
  form.checkSubmit();
1443
1444
  });
1444
1445
 
1445
- companyField.first('input')
1446
- .on('blur', (e) => { form.store.valid.company = validate(companyField, 'company', e.target.value); form.checkSubmit(); });
1446
+ companyField.refs.input
1447
+ .on('blur', () => { form.store.valid.company = validate(companyField, 'company', companyField.getValue()); form.checkSubmit(); });
1447
1448
 
1448
1449
  form.on('submit', (e) => {
1449
1450
  e.preventDefault();
1450
- const submitBtn = form.first('button[type="submit"]');
1451
+ const submitBtn = form.refs.submitBtn;
1451
1452
  submitBtn.setLoading?.(true);
1452
1453
  o.post('/api/register', { data: Object.fromEntries(new FormData(e.target)) })
1453
1454
  .then(r => r.json())
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **React-developer-friendly** — familiar `className`, `ref`/`refs`, `o.createStore`. Add one script tag to an existing React app and get Playwright test generation without touching any components.
7
7
 
8
- **Live examples** — real patterns in [`examples/`](https://foggysq.github.io/objs/examples/), narrative walkthroughs in [`EXAMPLES.md`](EXAMPLES.md).
8
+ **Live examples** — real patterns in [`examples/`](https://foggysq.github.io/objs/examples/), narrative walkthroughs in EXAMPLES.md. For AI assistants: use SKILL.md as `@SKILL.md` or system prompt.
9
9
 
10
10
  ---
11
11
 
@@ -25,6 +25,12 @@
25
25
 
26
26
  ---
27
27
 
28
+ ### Update v2.1: New features
29
+ - **`refs` on ObjsInstance** — array data support for auto-collect `ref="name"` child elements. Use `.select(i)` to choose not only element but also its refs. The default `.refs` contains the first element children.
30
+ - **self.select(e).refs...** — use `.select(e)` in render and other actions in event handlers to get Objs instance with the e.target from self and controll refs.
31
+ - **Auto HTML hydration** — when render sets `innerHTML` with markup built from inited children (e.g. `html: header.html() + field.html()` in the parent’s render), Objs automatically binds those same instances to the new DOM nodes inside the container. The parent’s stored references (e.g. `self.store.field`) then point at the real elements, and events/refs work. It makes JSX-like code from native HTML sample strings.
32
+
33
+
28
34
  ### Update v2.0: New features for micro-service architecture and AI development
29
35
 
30
36
  #### Breaking changes (migrate first)
@@ -71,12 +77,19 @@
71
77
 
72
78
 
73
79
  ## Get started
74
- Just import in your project or include script on the page.
80
+
81
+ **Browser** — source with test tools:
82
+ ```html
83
+ <script src="objs.js" type="text/javascript"></script>
75
84
  ```
76
- npm i objs-core
85
+
86
+ **npm / bundler** — correct file chosen automatically via `package.json` exports:
87
+ ```js
88
+ import o from 'objs-core'; // resolves to objs.built.js
77
89
  ```
90
+
78
91
  ```
79
- <script src="objs.built.min.js" type="text/javascript"></script>
92
+ npm i objs-core
80
93
  ```
81
94
 
82
95
 
@@ -113,9 +126,8 @@ To control elements Objs uses states. State - it's an information how to create
113
126
  // state called render for timer example
114
127
  const timerStates = {
115
128
  render: {
116
- tag: 'div',
117
129
  class: 'timer',
118
- html: 'Seconds: <span>0</span>',
130
+ html: 'Seconds: <span ref="n">0</span>',
119
131
  }
120
132
  }
121
133
  ```
@@ -133,15 +145,13 @@ Then add a new state that will start and finish counting. Number will be stored
133
145
  const timerStates = {
134
146
  render: {
135
147
  class: 'timer',
136
- html: 'Seconds: <span>0</span> ',
148
+ html: 'Seconds: <span ref="n">0</span>',
137
149
  },
138
150
  start: ({self}) => {
139
- // save number or create
140
151
  self.n = self.n || 0;
141
- // start interval
142
152
  self.interval = setInterval(() => {
143
153
  self.n++;
144
- o(self).first('span').html(self.n);
154
+ self.refs.n.html(self.n);
145
155
  }, 1000);
146
156
  },
147
157
  stop: ({self}) => {
@@ -201,7 +211,7 @@ Almost all functions return control object with methods, let's call it **Objs**.
201
211
 
202
212
  `o.inits[initID]` – an array of all inited objects. Available by index **initID** or **o.take()**.
203
213
 
204
- Instance: `o().init()` – equal to **o.init()** but with elements to control. `o().initState()` – equal to **o.initState()** but with elements to control. `o().sample()` – returns states object with render state for creation such elements. `o().getSSR(initId)` – hydrate from existing DOM. `o().saveState([id])`, `o().revertState([id])`, `o().loseState(id)` – save/restore DOM state. `o().unmount()` – remove from DOM and **o.inits**. `o().connect(loader, state, fail)` – connect a loader to this instance (state/fail method names). `o().initID` – undefined or number in **o.inits[]**. `o().html([html])` – returns html string of all elements or sets innerHTML as **html**.
214
+ Instance: `o().init()` – equal to **o.init()** but with elements to control. `o().initState()` – equal to **o.initState()** but with elements to control. `o().sample()` – returns states object with render state for creation such elements. `o().getSSR(initId, [fromEls])` – bind this instance to DOM nodes by initId; optional **fromEls** (e.g. from a container) skips document query; used by auto-hydration when parent sets innerHTML. `o().saveState([id])`, `o().revertState([id])`, `o().loseState(id)` – save/restore DOM state. `o().unmount()` – remove from DOM and **o.inits**. `o().connect(loader, state, fail)` – connect a loader to this instance (state/fail method names). `o().initID` – undefined or number in **o.inits[]**. `o().html([html])` – returns html string of all elements or sets innerHTML as **html**; when **html** is set, any `[data-o-init]` nodes inside are auto-hydrated (inited instances bound to those nodes).
205
215
 
206
216
  ### DOM manipulation
207
217
  `o().reset(q)` – clears **Objs** and get new elements by **q**, works as **o()**.
@@ -481,7 +491,7 @@ No Playwright config to set up manually. No test IDs to maintain. The entire pip
481
491
 
482
492
  ### Dev/prod build split
483
493
 
484
- `objs.js` is the source for development. `objs.built.js` and `objs.built.min.js` are produced by `node build.js` (ESM + window.o). Only the debug flag is behind `__DEV__`.
494
+ `objs.js` is the source for development or script tag. `objs.built.js` and `objs.built.min.js` are produced by `node build.js` (ESM + window.o). Only the debug flag is behind `__DEV__`.
485
495
 
486
496
  The **recording pipeline** (`startRecording`, `stopRecording`, `exportTest`, `exportPlaywrightTest`, `reactQA`) ships in all builds so QA assessors can use it on staging.
487
497
 
@@ -491,9 +501,8 @@ Bundlers pick the right file automatically via `package.json` exports conditions
491
501
  // Vite, webpack, esbuild — no config needed
492
502
  import o from 'objs-core'; // dev server → objs.js, build → objs.built.js
493
503
 
494
- // Script tag — explicit choice
495
- <script src="objs.js"></script> // dev/staging
496
- <script src="objs.built.js"></script> // or objs.built.min.js
504
+ // Script tag
505
+ <script src="objs.js"></script>
497
506
  ```
498
507
 
499
508
  ### States as AI-natural data structures
@@ -502,14 +511,27 @@ Every Objs component is a plain JS object. An LLM can generate correct component
502
511
 
503
512
  ```js
504
513
  // AI prompt: "create a counter with increment and reset"
514
+ // Hack: render as function sets the entity store and returns the init object (no globals, no post-init wiring)
505
515
  const counterStates = {
506
516
  name: 'Counter',
507
- render: { tag: 'div', html: '<span class="n">0</span> <button class="inc">+</button> <button class="rst">Reset</button>' },
508
- updateCount: ({ self }, n) => { o(self).first('.n').html(n); },
517
+ render: ({ self }) => {
518
+ self.store.n = self.store.n ?? 0;
519
+ return {
520
+ html: '<span ref="n">0</span> <button ref="inc">+</button> <button ref="rst">Reset</button>',
521
+ events: {
522
+ click: (e) => {
523
+ if (e.target === self.refs?.inc?.el) self.updateCount(++self.store.n);
524
+ else if (e.target === self.refs?.rst?.el) self.updateCount(self.store.n = 0);
525
+ },
526
+ },
527
+ };
528
+ },
529
+ updateCount: ({ self }, num) => {
530
+ self.store.n = num;
531
+ self.refs.n.html(num);
532
+ },
509
533
  };
510
- const counter = o.init(counterStates).render().appendInside('#app');
511
- o(counter).first('.inc').on('click', () => counter.updateCount(++n));
512
- o(counter).first('.rst').on('click', () => counter.updateCount(n = 0));
534
+ o.init(counterStates).render().appendInside('#app');
513
535
  ```
514
536
 
515
537
  No compiler. No build step to try the above. No framework knowledge needed to generate it.
@@ -543,13 +565,15 @@ Similar philosophy to Solid.js signals — but the update logic is a plain funct
543
565
 
544
566
  ### Real-world patterns
545
567
 
546
- See [EXAMPLES.md](EXAMPLES.md) for complete runnable examples:
547
- - Site menu with active route state
548
- - Product card list + cart with shared store (granular update pattern)
549
- - Overlay dialog with UTM auto-open and session persistence
550
- - Drawer with filters and two-way URL sync
551
- - Complex form with per-field validation and live preview
552
- - React coexistence with shared context bridge
568
+ See [EXAMPLES.md](EXAMPLES.md) for the architecture guide and runnable examples (aligned with [SKILL.md](SKILL.md)):
569
+ 1. **How render works** plain object, function, HTML string, multi-instance, `append`, `children`, `ref`/`refs`
570
+ 2. **Single components (atoms)** Button, Badge, Field with `val()`, `css(null)`, `addClass` spread
571
+ 3. **Nesting & composition** slot pattern, `append` in render, factory with dynamic children
572
+ 4. **Design system** Atoms Molecules → Organisms, `self.store`, update efficiency
573
+ 5. **Real-world** menu, cart+cards, dialog, drawer+URL, complex form
574
+ 6. **React integration** four modes including bolt-on Playwright recording with `o.reactQA`
575
+
576
+ **Rule (from SKILL):** Define one state method per data slice; never call `.render()` to update — use targeted state methods. In event handlers use **self.select(e)** and **refs** (e.g. `row.refs.input.val()`), not `e.target`/class selectors or raw DOM.
553
577
 
554
578
 
555
579
 
package/SKILL.md CHANGED
@@ -9,11 +9,8 @@ Use this file as a `.cursorrules` attachment, system prompt, or `@SKILL.md` refe
9
9
  ### Loading
10
10
 
11
11
  ```html
12
- <!-- Browser — dev source (includes test tools) -->
12
+ <!-- Browser -->
13
13
  <script src="objs.js"></script>
14
-
15
- <!-- Browser — distribution (node build.js → objs.built.js, objs.built.min.js) -->
16
- <script src="objs.built.js"></script>
17
14
  ```
18
15
 
19
16
  ```js
@@ -31,6 +28,7 @@ o([el1, el2]) // → ObjsInstance wrapping element array
31
28
  o(2) // → ObjsInstance from o.inits[2] (previously inited component)
32
29
  o() // → empty ObjsInstance, used to start init chains
33
30
  o.first('#id') // → ObjsInstance, single element, same as querySelector
31
+ self.select(e) // → select the element in state action or render with self in the parameters, returns Objs instance with e.target (e.g. the row); then .refs, .el apply to that row
34
32
  ```
35
33
 
36
34
  ---
@@ -481,9 +479,60 @@ Use **o.verify(pairs, safe?)** to check types at runtime—useful for function a
481
479
  | Use `states.name` for QA autotag | Manually add `data-qa` attributes — autotag keeps them in sync |
482
480
  | Use `.val()` to get/set input value: `field.first('input').val('new')` | Access raw DOM: `field.first('input').el.value = 'new'` |
483
481
  | Use `self.refs.name` to access named child elements | Use `self.first('[ref="name"]')` — refs gives ObjsInstance directly |
482
+ | Use `ref="name"` for elements the component needs to access (then `self.refs.name`) | Use `id` for component-owned elements — prefer ref so the instance owns the reference and avoids global id collisions |
484
483
  | Use `attr('disabled', null)` to remove an attribute | Use `attr('disabled', '')` — empty string now _sets_ the attribute to `""` |
485
484
  | Use `css(null)` to remove the `style` attribute entirely | Use `css({})` or `style('')` — those no longer remove the style |
486
485
  | Use `o.reactQA('ComponentName')` for stable React test selectors | Write `data-testid` manually — `reactQA` converts CamelCase to kebab automatically |
486
+ | Use **self.select(e)** and **refs** in event handlers (close over `self` from render) | Use CSS classes (e.g. `e.target.closest('.field')`, `wrap.querySelector('.error')`) — brittle when classes are hashed (Nano CSS etc.) |
487
+ | Use Objs instances in handlers: **self.select(e).refs.input.val()**, **row.refs.error.html()** | Use global selectors, **e.target** and native DOM (`.value`, `.textContent`, `.classList`) — stay in Objs API for consistency and refs |
488
+
489
+ ---
490
+
491
+ ## Common mistakes
492
+
493
+ ### Using global selectors, e.target and native DOM in event handlers
494
+
495
+ **Antipattern:** In event handlers, using `e.target` and raw DOM (e.g. `e.target.value`, `el.textContent`, `el.classList.add`) or global/document selectors to find and update elements. This bypasses Objs refs and the component instance.
496
+
497
+ ```js
498
+ // BAD — raw DOM and e.target
499
+ handler: (e) => {
500
+ const wrap = e.target.closest('.form-atom__field');
501
+ const errEl = wrap?.querySelector('.form-atom__field-error');
502
+ const valid = emailValid(e.target.value);
503
+ if (!valid) errEl.textContent = 'Invalid email';
504
+ wrap.classList.add('form-atom__field--error');
505
+ }
506
+ ```
507
+
508
+ **Fix:** Close over **self** from render, use **self.select(e)** to get the row/component that contains the event, then use **refs** and Objs API (`.val()`, `.html()`, `.css()`, `.el` only when needed for e.g. classList).
509
+
510
+ ```js
511
+ // GOOD — Objs instances and refs
512
+ render: ({ self, ...p }) => ({
513
+ html: `<input ref="input" ...><span ref="error"></span>`,
514
+ events: {
515
+ blur: { targetRef: 'input', handler: (e) => {
516
+ const row = self.select(e);
517
+ if (!row.refs?.error) return;
518
+ const value = row.refs.input.val();
519
+ const valid = emailValid(value);
520
+ if (!valid && value.trim()) {
521
+ row.refs.error.html('Invalid email');
522
+ row.el?.classList.add('form-atom__field--error');
523
+ } else {
524
+ row.refs.error.html(''); row.el?.classList.remove('form-atom__field--error');
525
+ }
526
+ } },
527
+ },
528
+ }),
529
+ ```
530
+
531
+ ### Using CSS classes to find elements from an event target
532
+
533
+ **Antipattern:** Using `e.target.closest('.some-class')` or `wrap.querySelector('.child-class')` to find the component root or siblings. Class names are often generated (e.g. Nano CSS) and are brittle for structure.
534
+
535
+ **Fix:** Use **self.select(e)** (with `self` from render) so refs are updated for the row that contains the event; then use **row.refs.name** — no class-based lookups. Add `ref="name"` to elements you need to access.
487
536
 
488
537
  ---
489
538