objs-core 2.0.2 → 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 +59 -58
- package/README.md +51 -27
- package/SKILL.md +53 -4
- package/objs.built.js +154 -37
- package/objs.built.min.js +26 -26
- package/objs.d.ts +3 -2
- package/objs.js +171 -47
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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.
|
|
782
|
-
self.
|
|
783
|
+
self.refs.input.addClass('field__input--error');
|
|
784
|
+
self.refs.error.html(msg || '');
|
|
783
785
|
},
|
|
784
786
|
setSuccess: ({ self }) => {
|
|
785
|
-
self.
|
|
786
|
-
self.
|
|
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.
|
|
790
|
-
self.
|
|
791
|
+
self.refs.input.removeClass('field__input--error').removeClass('field__input--ok');
|
|
792
|
+
self.refs.error.html('');
|
|
791
793
|
},
|
|
792
|
-
getValue: ({ self }) => self.
|
|
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.
|
|
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
|
|
1222
|
-
<p
|
|
1223
|
-
<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.
|
|
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.
|
|
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
|
|
1265
|
-
<h2
|
|
1266
|
-
<p
|
|
1267
|
-
<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
|
|
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.
|
|
1275
|
-
self.
|
|
1276
|
-
self.
|
|
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
|
|
1315
|
+
html: `<button ref="closeBtn">✕</button>
|
|
1315
1316
|
<h3>Filters</h3>
|
|
1316
|
-
<select
|
|
1317
|
-
<input
|
|
1318
|
-
<input
|
|
1319
|
-
<button
|
|
1320
|
-
<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.
|
|
1326
|
-
else if (e.target.
|
|
1327
|
-
else if (e.target.
|
|
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.
|
|
1335
|
-
self.
|
|
1336
|
-
self.
|
|
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.
|
|
1340
|
-
minPrice: self.
|
|
1341
|
-
maxPrice: self.
|
|
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.
|
|
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.
|
|
1426
|
-
.on('blur', (
|
|
1427
|
-
.on('input', (
|
|
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.
|
|
1430
|
-
.on('blur', (
|
|
1431
|
-
.on('input', (
|
|
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.
|
|
1446
|
-
.on('blur', (
|
|
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.
|
|
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
|
|
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
|
-
|
|
80
|
+
|
|
81
|
+
**Browser** — source with test tools:
|
|
82
|
+
```html
|
|
83
|
+
<script src="objs.js" type="text/javascript"></script>
|
|
75
84
|
```
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)` –
|
|
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
|
|
495
|
-
<script src="objs.js"></script>
|
|
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: {
|
|
508
|
-
|
|
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
|
-
|
|
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
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
-
|
|
552
|
-
|
|
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
|
|
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
|
|