objs-core 1.1.1 → 2.0.1

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/SKILL.md ADDED
@@ -0,0 +1,500 @@
1
+ # Objs v2.0 — AI Skill File
2
+
3
+ Use this file as a `.cursorrules` attachment, system prompt, or `@SKILL.md` reference to teach an AI assistant how to work with the Objs library.
4
+
5
+ ---
6
+
7
+ ## Library basics
8
+
9
+ ### Loading
10
+
11
+ ```html
12
+ <!-- Browser — dev source (includes test tools) -->
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
+ ```
18
+
19
+ ```js
20
+ // npm / bundler — correct file chosen automatically by package.json exports
21
+ import o from 'objs-core'; // resolves to objs.built.js
22
+ ```
23
+
24
+ ### The `o()` function
25
+
26
+ ```js
27
+ o('#id') // → ObjsInstance wrapping all matching elements
28
+ o('.class') // → ObjsInstance wrapping all matching elements
29
+ o(domElement) // → ObjsInstance wrapping one DOM element
30
+ o([el1, el2]) // → ObjsInstance wrapping element array
31
+ o(2) // → ObjsInstance from o.inits[2] (previously inited component)
32
+ o() // → empty ObjsInstance, used to start init chains
33
+ o.first('#id') // → ObjsInstance, single element, same as querySelector
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Running Objs in Node (SSR)
39
+
40
+ In Node, **o.D** is **o.DocumentMVP** — there is no real `document` or `window`. You can run Objs in Node to render components to HTML (e.g. for SSR or for verification): use `o.init(states).render()`; the result is a tree of plain objects; serialize with the same SSR path the app uses (e.g. `o.D.parseElement`). To self-check generated Objs code, you can run a Node script that requires Objs, calls `o.init(...).render()`, and inspects the output or HTML string — no browser or user review required for structure verification. Call **`.html()`** (no arguments) on the rendered ObjsInstance to get that HTML string (in Node it uses `o.D.parseElement` under the hood).
41
+
42
+ ---
43
+
44
+ ## Component model
45
+
46
+ A component is a **states object** passed to `o.init()`. Every key becomes a method on the component instance.
47
+
48
+ ### State object keys
49
+
50
+ | Key | Meaning |
51
+ |---|---|
52
+ | `name` | Component name string — used for `o.autotag` data attribute |
53
+ | `render` | Reserved: defines the DOM element to create (tag, attributes, html) |
54
+ | `tag` / `tagName` | HTML element type (default: `div`) |
55
+ | `html` / `innerHTML` | Inner HTML of the element |
56
+ | `class` / `className` | CSS class (`className` is a React-familiar alias) |
57
+ | `style` | Inline style string or object |
58
+ | `dataset` | Object of `data-*` attributes |
59
+ | `events` | Object of `{eventName: handler}` added on creation |
60
+ | any other key | HTML attribute set via `setAttribute` |
61
+
62
+ **ObjsInstance properties after init:**
63
+
64
+ | Property | Description |
65
+ |---|---|
66
+ | `refs` | Object of `{ name: ObjsInstance }` — auto-populated from `ref="name"` child elements on `init` |
67
+ | `store` | Plain object for storing child components and other per-instance data |
68
+
69
+ ### State function signature
70
+
71
+ Every non-render state is a function:
72
+
73
+ ```js
74
+ stateName: ({ self, o, i, parent }, data) => {
75
+ // self — the ObjsInstance (has all methods: .first(), .html(), .attr(), .on(), etc.)
76
+ // o — the o() function for creating new instances or querying the global DOM
77
+ // i — index of current element in multi-element instances
78
+ // parent — the ObjsInstance this component was appendInside() into, or null
79
+ // data — argument passed when the state is called: component.stateName(data)
80
+ }
81
+ ```
82
+
83
+ Inside a state function, `self` is the ObjsInstance. Use `self.first()`, `self.html()`, `self.attr()` etc. directly — no need to re-wrap it with `o(self)`.
84
+
85
+ ### Creating a component
86
+
87
+ ```js
88
+ const buttonStates = {
89
+ name: 'SubmitButton', // sets data-qa="submit-button" if o.autotag = "qa"
90
+ render: { tag: 'button', class: 'btn', html: 'Submit' },
91
+ disable: ({ self }) => { self.attr('disabled', 'true'); },
92
+ enable: ({ self }) => { self.attr('disabled', null); }, // null removes the attribute
93
+ setLabel:({ self }, text) => { self.html(text); },
94
+ };
95
+
96
+ const btn = o.init(buttonStates).render(); // creates the DOM element
97
+ btn.appendInside('#form'); // inserts it
98
+ btn.disable(); // calls the disable state
99
+ btn.setLabel('Saving...'); // calls with data
100
+ ```
101
+
102
+ ### Shorthand for simple elements
103
+
104
+ ```js
105
+ // Render a single element without explicit states object
106
+ o.initState({ tag: 'span', class: 'badge', html: '3' }).appendInside('.nav');
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Selecting and querying elements
112
+
113
+ ```js
114
+ o('.card').first('h3').html('New title'); // find first h3 inside each .card, set text
115
+ o('.card').find('button').on('click', fn); // find all buttons inside all .cards
116
+ o('.card').select(0).addClass('featured'); // operate only on first .card
117
+
118
+ const el = o.first('.card').el; // raw DOM element
119
+ const els = o('.card').els; // raw DOM array
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Events
125
+
126
+ ```js
127
+ o(component).on('click', handler);
128
+ o(component).on('click, mouseover', handler); // multiple events
129
+ o(component).off('click', handler);
130
+ o(component).offAll(); // remove all listeners
131
+ o(component).offAll('click'); // remove all click listeners
132
+
133
+ // Event delegation (listen on parent, match children)
134
+ o('#list').onDelegate('click', '.item', handler);
135
+ o('#list').offDelegate('click'); // removes all delegated listeners for event type
136
+
137
+ // Parent listener (one parent element; listener runs when event.target is inside Objs elements)
138
+ o('#list').onParent('click', '.parentBlock', handler);
139
+ o('#list').offParent('click', '.parentBlock');
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Granular store updates — the key pattern
145
+
146
+ **Rule: define one state method per data slice. Never call `.render()` to update — it recreates the element. Use targeted state methods for updates.**
147
+
148
+ ```js
149
+ // WRONG — recreates the entire DOM element on every store change
150
+ o.connectRedux(store, s => s, card, 'render');
151
+
152
+ // CORRECT — each update only touches the affected part
153
+ const cardStates = {
154
+ render: { tag: 'div', html: '<span class="name"></span> <span class="score"></span>' },
155
+ updateName: ({ self }, data) => { self.first('.name').html(data); },
156
+ updateScore: ({ self }, data) => { self.first('.score').html(data); },
157
+ };
158
+ const card = o.init(cardStates).render();
159
+ o.connectRedux(store, s => s.userName, card, 'updateName'); // only .name updates
160
+ o.connectRedux(store, s => s.userScore, card, 'updateScore'); // only .score updates
161
+ ```
162
+
163
+ The `transform()` function inside the library already skips attributes whose value hasn't changed. Targeted state methods give you direct writes — O(1) per update, no diff.
164
+
165
+ ---
166
+
167
+ ## Store adapters
168
+
169
+ ### Redux
170
+
171
+ ```js
172
+ // Returns an unsubscribe function
173
+ const unsub = o.connectRedux(
174
+ store, // Redux store
175
+ state => state.cartItems, // selector
176
+ menuCart, // Objs component
177
+ 'updateCount' // state method to call
178
+ );
179
+ unsub(); // disconnect
180
+ ```
181
+
182
+ ### MobX
183
+
184
+ ```js
185
+ // Returns a disposer function
186
+ const dispose = o.connectMobX(
187
+ mobx, // MobX instance
188
+ appStore, // observable
189
+ obs => obs.cartItems, // accessor
190
+ menuCart,
191
+ 'updateCount'
192
+ );
193
+ dispose();
194
+ ```
195
+
196
+ ### o.newLoader (built-in, for fetch/promise)
197
+
198
+ ```js
199
+ const loader = o.newLoader(o.get('/api/data')); // fires the request
200
+ component.connect(loader, 'render'); // calls component.render(data) when ready
201
+ component.connect(loader, 'render', 'showError'); // optional fail state
202
+ loader.reload(o.get('/api/data?page=2')); // refetch with new request
203
+ ```
204
+
205
+ ### o.createStore (built-in reactive store)
206
+
207
+ ```js
208
+ const cartStore = o.createStore({ items: [], total: 0 });
209
+
210
+ // Subscribe components — they receive store props merged into their state context
211
+ cartStore.subscribe(cartBadge, 'sync'); // cartBadge.sync({ items, total, self, o, i }) on each notify
212
+ cartStore.subscribe(cartDrawer, 'sync');
213
+
214
+ // Update and notify all subscribers
215
+ cartStore.items.push(product);
216
+ cartStore.total += product.price;
217
+ cartStore.notify(); // fires both cartBadge.sync() and cartDrawer.sync()
218
+
219
+ // Reset to original defaults
220
+ cartStore.reset();
221
+ ```
222
+
223
+ ### Plain callback (no adapter needed)
224
+
225
+ ```js
226
+ const store = { value: 0, listeners: [] };
227
+ const notify = () => store.listeners.forEach(fn => fn(store.value));
228
+
229
+ store.listeners.push((v) => counter.updateCount(v));
230
+ store.value++;
231
+ notify();
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Component composition — nesting
237
+
238
+ A parent component stores child instances in its `.store` object. Children append inside the parent's element.
239
+
240
+ ```js
241
+ const parentStates = {
242
+ name: 'ParentCard',
243
+ render: { tag: 'div', class: 'card', html: '<div class="header"></div><div class="body"></div>' },
244
+ init: ({ self }) => {
245
+ // Create children and store references
246
+ self.store.header = o.init(headerStates).render().appendInside(self.first('.header').el);
247
+ self.store.body = o.init(bodyStates).render().appendInside(self.first('.body').el);
248
+ },
249
+ // Update only the relevant child — no rerender of parent
250
+ updateTitle: ({ self }, title) => { self.store.header.setTitle(title); },
251
+ };
252
+
253
+ const card = o.init(parentStates).render().appendInside('#app');
254
+ card.init(); // sets up children
255
+ card.updateTitle('New title');
256
+ ```
257
+
258
+ To access all components:
259
+ ```js
260
+ o.inits // array of all inited components
261
+ o.getStores() // array of all .store objects
262
+ o.getListeners() // array of all .ie (event listener maps)
263
+ ```
264
+
265
+ ---
266
+
267
+ ## React integration — three modes
268
+
269
+ ### Mode 1: Objs inside a React ref (most common)
270
+
271
+ ```jsx
272
+ function ProductSection() {
273
+ const ref = React.useRef(null);
274
+
275
+ React.useEffect(() => {
276
+ const grid = o.init(gridStates).render().appendInside(ref.current);
277
+ return () => grid.unmount(); // cleanup on unmount
278
+ }, []);
279
+
280
+ return <div ref={ref} />;
281
+ }
282
+ ```
283
+
284
+ ### Mode 2: Objs element as a React element
285
+
286
+ ```jsx
287
+ // In a React component:
288
+ const badge = o.init(badgeStates).render();
289
+ const reactEl = badge.prepareFor(React.createElement); // returns React element
290
+ return <div>{reactEl}</div>;
291
+ ```
292
+
293
+ ### Mode 3: Context bridge (shared state)
294
+
295
+ ```jsx
296
+ const CartContext = React.createContext(null);
297
+
298
+ // In the React tree:
299
+ const CartBridge = o.withReactContext(React, CartContext, ctx => ctx.items, menuCart, 'updateCount');
300
+ // Mount anywhere inside <CartContext.Provider>:
301
+ return <CartContext.Provider value={cartState}><CartBridge /><OtherComponents /></CartContext.Provider>;
302
+ ```
303
+
304
+ `CartBridge` renders nothing (`return null`). It calls `menuCart.updateCount(ctx.items)` on every context change — no React rerender of anything.
305
+
306
+ ---
307
+
308
+ ## QA autotag & React integration
309
+
310
+ ```js
311
+ o.autotag = 'qa'; // set once, globally, before init() calls
312
+
313
+ // Each component with states.name gets data-qa="kebab-name" automatically
314
+ const btn = o.init({ name: 'SubmitButton', render: { tag: 'button' } }).render();
315
+ // Result: <button data-qa="submit-button" data-o-state="render" ...>
316
+
317
+ // Use in Playwright / Cypress:
318
+ // page.getByTestId('submit-button') (with testIdAttribute: 'data-qa')
319
+ // cy.get('[data-qa="submit-button"]')
320
+ ```
321
+
322
+ `o.autotag` is present in all builds — QA teams need stable selectors in staging/production.
323
+
324
+ ### o.reactQA — bolt-on for React projects
325
+
326
+ ```jsx
327
+ // Returns { 'data-qa': 'kebab-name' } for spreading onto React JSX elements
328
+ // Converts CamelCase to kebab-case. Respects o.autotag value.
329
+ <button {...o.reactQA('CheckoutButton')} onClick={fn}>Checkout</button>
330
+ // → <button data-qa="checkout-button">
331
+
332
+ // Works even when o.autotag is undefined (defaults to 'data-qa')
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Dev/prod split
338
+
339
+ `objs.js` is the source; `node build.js` produces `objs.built.js` and `objs.built.min.js`. Only the debug flag and debug logging are behind `__DEV__`; all other API is present in every build.
340
+
341
+ **Behind `__DEV__` (stripped when `__DEV__` is false):**
342
+ - `o.debug` flag and debug logging in `returner()`, `result.debug()`
343
+
344
+ **Always present (all builds — objs.js, objs.built.js, objs.built.min.js):**
345
+ - All DOM manipulation methods, states, events (including `onDelegate`, `offDelegate`, `onParent`, `offParent`)
346
+ - `o.autotag`, `o.reactQA`
347
+ - `o.startRecording`, `o.stopRecording`, `o.exportTest`, `o.exportPlaywrightTest`, `o.clearRecording`, `o.playRecording`
348
+ - `o.test`, `o.addTest`, `o.runTest`, `o.testUpdate`, `o.testOverlay`, `o.testConfirm` (assessors on staging can replay and see auto + manual results)
349
+ - `o.measure`, `o.assertVisible`, `o.assertSize` (layout assertions for tests)
350
+ - `o.newLoader`, `o.connectRedux`, `o.connectMobX`, `o.withReactContext`
351
+ - `o.route`, `o.router`, `o.inc`, `o.ajax`, `o.get`, `o.post`
352
+ - `o.setCookie`, `o.getCookie`, storage helpers
353
+ - `o.verify`, `o.safeVerify`, `o.specialTypes`
354
+
355
+ > **Security note:** `o.startRecording()` intercepts `window.fetch` and captures request/response bodies. Appropriate for staging; review before enabling on production.
356
+
357
+ ---
358
+
359
+ ## Testing patterns
360
+
361
+ ### Unit test
362
+
363
+ ```js
364
+ o.addTest('Button states',
365
+ ['renders correctly', () => {
366
+ const btn = o.init(buttonStates).render();
367
+ return btn.el?.tagName === 'BUTTON';
368
+ }],
369
+ ['disable works', () => {
370
+ btn.disable();
371
+ return btn.el?.disabled === true;
372
+ }],
373
+ );
374
+ ```
375
+
376
+ ### With lifecycle hooks
377
+
378
+ ```js
379
+ o.addTest('Cart updates',
380
+ ['adds item', () => { cartAdd({ id: 1 }); return cartStore.items.length === 1; }],
381
+ { before: () => { cartStore.items = []; }, after: () => { cartStore.items = []; } }
382
+ );
383
+ ```
384
+
385
+ ### Measurement assertions
386
+
387
+ ```js
388
+ o.addTest('Layout',
389
+ ['menu is visible', () => o.assertVisible(menu.el)],
390
+ ['button has correct width', () => o.assertSize(btn.el, { w: 120 })],
391
+ );
392
+ ```
393
+
394
+ ### Recording → Objs test
395
+
396
+ ```js
397
+ // Available in all builds — record a session
398
+ o.startRecording();
399
+ // ... user interacts ...
400
+ const recording = o.stopRecording();
401
+ console.log(o.exportTest(recording)); // paste into your codebase as o.addTest()
402
+ ```
403
+
404
+ ### Recording → Playwright CI test
405
+
406
+ ```js
407
+ // Available in all builds — works on any DOM including React
408
+ o.startRecording('#app'); // optional: scope MutationObserver to selector; returns observeRoot in recording
409
+ // ... QA tester uses the app ...
410
+ const recording = o.stopRecording();
411
+ // recording.assertions, recording.observeRoot when scoped
412
+ console.log(o.exportPlaywrightTest(recording, { testName: 'Checkout flow' }));
413
+ // → paste into tests/checkout.spec.ts → npx playwright test
414
+
415
+ // Options:
416
+ // { testName: 'My flow' } — sets test() name
417
+ // { baseUrl: '/app' } — overrides page.goto() path
418
+ ```
419
+
420
+ Generated output includes:
421
+ - `page.route()` mocks for every intercepted `fetch` call
422
+ - `page.goto(relativePath)` — needs `baseURL` in playwright.config.ts
423
+ - Typed locator steps: `.fill()`, `.click()`, `.check()`, `.selectOption()`, `.hover()`
424
+ - Auto-inserted `expect()` from `recording.assertions` (visible, toContainText, class comments)
425
+
426
+ ### Manual check overlay
427
+
428
+ `o.testConfirm(label, items?, opts?)` — All builds. Shows a draggable overlay "Label: Paused" with an optional checklist; returns `Promise<{ ok, errors? }>`. Use after replay for items that can't be asserted automatically (e.g. hover).
429
+
430
+ ```js
431
+ const r = await o.testConfirm('Manual check', ['Hover effect exists']);
432
+ if (!r.ok) console.warn(r.errors);
433
+ ```
434
+
435
+ ### Reload-based e2e
436
+
437
+ ```js
438
+ // Survives page reloads — useful for testing navigation
439
+ const { autorun } = o.addTest('Page flow',
440
+ ['page title is correct', () => document.title === 'Home'],
441
+ ['nav link works', (info) => {
442
+ // mark as async, navigate, check on next load
443
+ o.testUpdate(info, document.title === 'Products');
444
+ }],
445
+ );
446
+ autorun(); // runs all tests in sequence, reloading between steps
447
+ ```
448
+
449
+ ### Test overlay (auto tests + manual results)
450
+
451
+ `o.testOverlay()` — All builds. Call once per page; shows a fixed 🧪 Tests button. For assessors: after replay, open the overlay to see if all auto tests passed and which manual checks failed.
452
+
453
+ ```js
454
+ o.testOverlay(); // call once — shows a fixed button with live results (o.tLog / o.tRes)
455
+ ```
456
+
457
+ ---
458
+
459
+ ## Runtime verification (o.verify, o.specialTypes)
460
+
461
+ Use **o.verify(pairs, safe?)** to check types at runtime—useful for function arguments, config, or API responses. **pairs** is an array of `[value, expectedTypes]` (e.g. `[[id, ['number']], [opts, ['object','undefined']]]`). Built-in: `typeof` names plus **o.specialTypes**: `notEmptyString`, `array`, `promise`. On failure: throws (or returns Error if **safe** true). **o.safeVerify(pairs)** returns boolean.
462
+
463
+ **Add global validators** by extending **o.specialTypes**: `o.specialTypes.myType = (val, type) => ...`. They are then available everywhere (your code and Objs internals), so you can use `o.verify([x, ['myType']])` consistently.
464
+
465
+ ---
466
+
467
+ ## Do / Don't
468
+
469
+ | Do | Don't |
470
+ |---|---|
471
+ | Use `self.first()`, `self.html()` etc. directly inside state functions | Re-wrap self: `o(self).first()` — works but is redundant |
472
+ | Call `.render()` once to create the element | Call `.render()` again to update — re-evaluates the render function, recreating any child components built inside it |
473
+ | Define one state method per data slice | Put all update logic in a single `update` state |
474
+ | Build child components in an `init` state or factory, store in `self.store` | Build child components inside the `render` function — they duplicate on every re-render |
475
+ | Set `o.autotag` before any `o.init()` calls | Set `o.autotag` after components have already rendered |
476
+ | Use `self.store` to hold child component references | Store children in external variables that may close over stale refs |
477
+ | Call `grid.unmount()` in React `useEffect` cleanup | Leave Objs components running after their React parent unmounts |
478
+ | Create Objs components inside `useEffect`, not in the React component body | Create Objs components in the React component body — they are recreated on every React render |
479
+ | Use `o.connectRedux` / `o.connectMobX` for live store connections | Manually subscribe and call `component.render()` on each change |
480
+ | Use `objs.built.js` or `objs.built.min.js` for distribution | Rely on `objs.prod.js` / `objs.dev.js` (no longer generated) |
481
+ | Use `states.name` for QA autotag | Manually add `data-qa` attributes — autotag keeps them in sync |
482
+ | Use `.val()` to get/set input value: `field.first('input').val('new')` | Access raw DOM: `field.first('input').el.value = 'new'` |
483
+ | Use `self.refs.name` to access named child elements | Use `self.first('[ref="name"]')` — refs gives ObjsInstance directly |
484
+ | Use `attr('disabled', null)` to remove an attribute | Use `attr('disabled', '')` — empty string now _sets_ the attribute to `""` |
485
+ | Use `css(null)` to remove the `style` attribute entirely | Use `css({})` or `style('')` — those no longer remove the style |
486
+ | Use `o.reactQA('ComponentName')` for stable React test selectors | Write `data-testid` manually — `reactQA` converts CamelCase to kebab automatically |
487
+
488
+ ---
489
+
490
+ ## Full examples
491
+
492
+ See [EXAMPLES.md](EXAMPLES.md) for architecture guide and complete walkthroughs, and [examples.js](examples.js) for paste-and-run code.
493
+
494
+ **EXAMPLES.md sections:**
495
+ 1. How render works — plain object, function, HTML string, multi-instance, `append`, `children`, `ref`/`refs`
496
+ 2. Single components — atoms (Button, Badge, Field) with `val()`, `css(null)`, `addClass` spread
497
+ 3. Nesting & composition — slot pattern, `append` in render, factory with dynamic children
498
+ 4. Design system architecture — Atoms → Molecules → Organisms, `self.store`, update efficiency table
499
+ 5. Real-world examples — menu, cart+cards, dialog, drawer+URL, complex form
500
+ 6. React integration — four modes including bolt-on Playwright recording with `o.reactQA`