@vanilla-bean/components 1.0.1 → 1.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.
@@ -1,455 +0,0 @@
1
- # Component
2
-
3
- Reactive component class extending Elem with Oxject-backed reactive options, lifecycle management, and automatic cleanup.
4
-
5
- ## Key Features
6
-
7
- - **Reactive options system** - Property changes backed by Oxject emit events and update DOM automatically
8
- - **Automatic memory management** - Element-level listeners persist through DOM moves, cleaned up on destroy. External resources (timers, subscriptions) cleaned up on disconnect
9
- - **Flexible render timing** - Immediate, onload, or animationFrame rendering modes
10
- - **Enhanced event handling** - Input events include `.value` property, DOM connection detection
11
- - **Integrated styling** - Inline objects or scoped CSS with theme system integration
12
- - **Lifecycle hooks** - Built-in hover and press handlers with automatic cleanup
13
-
14
- ```js
15
- import { Component } from '@vanilla-bean/components';
16
- ```
17
-
18
- ## Basic Usage
19
-
20
- ### Simple Components
21
-
22
- Every Component extends EventTarget and wraps an HTMLElement with reactive options:
23
-
24
- ```js
25
- import { Component } from '@vanilla-bean/components';
26
-
27
- const button = new Component({
28
- tag: 'button',
29
- textContent: 'Click me',
30
- className: 'primary-btn',
31
- onPointerPress: () => alert('Hello!'),
32
- appendTo: document.body,
33
- });
34
- ```
35
-
36
- ### Reactive Property Updates
37
-
38
- Options are backed by Oxject. Property changes trigger DOM updates automatically:
39
-
40
- ```js
41
- const input = new Component({
42
- tag: 'input',
43
- value: 'Initial text',
44
- onChange: ({ value }) => {
45
- input.options.value = value; // Updates DOM reactively
46
- },
47
- appendTo: document.body,
48
- });
49
-
50
- // Display that updates automatically
51
- new Component({
52
- tag: 'p',
53
- textContent: input.options.subscriber('value', value => `Current: ${value}`),
54
- appendTo: document.body,
55
- });
56
- ```
57
-
58
- ### Extended Component Classes
59
-
60
- Create reusable component classes with custom behavior:
61
-
62
- ```js
63
- class Button extends Component {
64
- constructor(options = {}, ...children) {
65
- super(
66
- {
67
- tag: 'button',
68
- ...options,
69
- styles: ({ button }) => `
70
- ${button}
71
- ${options.styles?.({ button }) || ''}
72
- `,
73
- },
74
- ...children,
75
- );
76
- }
77
-
78
- _setOption(key, value) {
79
- if (key === 'mode') {
80
- this.removeClass(/\bmode-\S+/g).addClass(`mode-${value}`);
81
- } else {
82
- super._setOption(key, value);
83
- }
84
- }
85
- }
86
- ```
87
-
88
- ### With Styled Components
89
-
90
- Components integrate seamlessly with the styled system:
91
-
92
- ```js
93
- import { styled } from '@vanilla-bean/components';
94
-
95
- const StyledComponent = styled(
96
- Component,
97
- ({ colors }) => `
98
- background: ${colors.blue};
99
- color: ${colors.white};
100
- padding: 16px;
101
- border-radius: 8px;
102
- `,
103
- );
104
- ```
105
-
106
- ## Reactive Options System
107
-
108
- Component options are stored in an Oxject instance (not a plain object like Elem), enabling automatic reactivity:
109
-
110
- ```js
111
- const button = new Component({ textContent: 'Click me' });
112
-
113
- // Property changes trigger DOM updates
114
- button.options.textContent = 'Updated'; // Updates DOM reactively
115
- button.options.addClass = 'active'; // Adds CSS class reactively
116
-
117
- // Full Oxject features available
118
- const subscription = button.options.subscribe({
119
- key: 'textContent',
120
- callback: value => console.log('Text changed:', value),
121
- });
122
- ```
123
-
124
- ### Option Processing Pipeline
125
-
126
- Options are routed through `_setOption` based on key and value type:
127
-
128
- | Condition | Routing | Example |
129
- | ------------------------ | -------------------------- | ------------------------- |
130
- | Key starts with 'on' | Event handler registration | `onPointerPress: handler` |
131
- | Key in `knownAttributes` | `elem.setAttribute()` | `id: 'my-id'` |
132
- | Component has method | Method call with value | `addClass: 'active'` |
133
- | HTMLElement has property | Direct elem property | `textContent: 'hello'` |
134
- | Value is function | Component property | `customMethod: fn` |
135
- | Default | Elem property assignment | `customProp: value` |
136
-
137
- ## Event Handling
138
-
139
- ### Automatic Event Registration
140
-
141
- Event handlers are registered automatically when option keys start with 'on':
142
-
143
- ```js
144
- new Component({
145
- onPointerPress: event => console.log('pressed'),
146
- onChange: ({ value }) => console.log('value:', value), // Input events include .value
147
- onConnected: () => console.log('added to DOM'),
148
- onDisconnected: () => console.log('removed from DOM'),
149
- });
150
- ```
151
-
152
- > **Note:** `click` is not in the supported event set. Use `onPointerPress` for press interactions.
153
-
154
- ### Enhanced Input Events
155
-
156
- Input-related events (`keydown`, `keyup`, `change`, `blur`, `input`, `search`) receive enhanced event objects:
157
-
158
- ```js
159
- new Component({
160
- tag: 'input',
161
- type: 'text',
162
- onChange: ({ value, event }) => {
163
- console.log('New value:', value); // Extracted from input
164
- console.log('Original event:', event);
165
- },
166
- });
167
- ```
168
-
169
- ### Connection Events
170
-
171
- DOM connection/disconnection detected via MutationObserver:
172
-
173
- ```js
174
- new Component({
175
- onConnected: () => console.log('Component added to DOM'),
176
- onDisconnected: () => {
177
- console.log('Component removed - cleanup triggered');
178
- },
179
- });
180
- ```
181
-
182
- ### Specialized Event Handlers
183
-
184
- #### Hover with Movement Tracking
185
-
186
- ```js
187
- component.onHover(event => {
188
- console.log('Mouse position:', event.clientX, event.clientY);
189
- });
190
- ```
191
-
192
- #### Press Detection
193
-
194
- Fires on `pointerdown` for immediate, reliable response in all contexts including scroll containers and modal dialogs:
195
-
196
- ```js
197
- component.onPointerPress(event => {
198
- console.log('Pressed');
199
- });
200
- ```
201
-
202
- ## Styling
203
-
204
- ### Inline Style Objects
205
-
206
- Apply styles as JavaScript objects:
207
-
208
- ```js
209
- component.styles({
210
- color: 'red',
211
- padding: '10px',
212
- backgroundColor: '#f0f0f0',
213
- });
214
-
215
- // Or as option
216
- new Component({
217
- styles: { color: 'red', padding: '10px' },
218
- });
219
- ```
220
-
221
- ### Scoped CSS with Theme Integration
222
-
223
- Use theme functions for scoped, processed CSS:
224
-
225
- ```js
226
- // Theme function with automatic scoping
227
- component.styles(
228
- ({ colors, fonts }) => `
229
- background: ${colors.blue};
230
- ${fonts.kodeMono}
231
- padding: 16px;
232
- border-radius: 8px;
233
-
234
- &:hover {
235
- background: ${colors.darker(colors.blue)};
236
- }
237
-
238
- @media (max-width: 768px) {
239
- padding: 8px;
240
- }
241
- `,
242
- );
243
- ```
244
-
245
- CSS is scope-wrapped and injected as a scoped <style> tag. No preprocessing.
246
-
247
- ## Lifecycle Management
248
-
249
- ### Render Lifecycle
250
-
251
- `render()` orchestrates a fixed sequence every time it is called:
252
-
253
- ```
254
- render()
255
- ├─ if (this.rendered) ← re-render only: clears children, runs descendant cleanup
256
- │ empty()
257
- │ this.rendered = false
258
- ├─ build() ← subclass structural hook, runs before options are applied
259
- ├─ _processOptions() ← routes every option through _setOption()
260
- │ priority keys first (onConnected, textContent, content, appendTo, prependTo, value)
261
- │ then all remaining keys
262
- └─ this.rendered = true
263
- ```
264
-
265
- **`build()` is the subclass structural hook.** Override `build()`, never `render()`, to create child elements and internal structure. Because `build()` runs before `_processOptions()`, all structure exists by the time `_setOption` receives values.
266
-
267
- The preferred way to handle specific options is `static handlers` — a per-class dispatch map that composes across the constructor chain without requiring `super._setOption`:
268
-
269
- ```js
270
- class Card extends Component {
271
- build() {
272
- this.header = new Component({ tag: 'header', appendTo: this });
273
- this.body = new Component({ tag: 'section', appendTo: this });
274
- }
275
-
276
- static handlers = {
277
- title(value) {
278
- this.header.options.textContent = value;
279
- },
280
- variant(value) {
281
- this.removeClass(/variant-\S+/).addClass(`variant-${value}`);
282
- },
283
- };
284
- }
285
- ```
286
-
287
- Handlers that don't call `next(value)` fully own their key. Unhandled keys fall through to standard routing automatically — no `super._setOption` required. Use `_setOption` override only for enum validation or cases that need to intercept before the routing chain.
288
-
289
- **`empty()` only runs on re-render.** On the initial render it is skipped. On subsequent `render()` calls it removes all child elements and runs cleanup on all descendant components before the DOM is cleared.
290
-
291
- **`async build()` is not supported.** `render()` does not await `build()`'s return value. Asynchronous initialization belongs in `onConnected`.
292
-
293
- **Deep hierarchies must chain `super.build()` manually.** If both a parent class and its subclass define `build()`, the subclass must call `super.build()` explicitly. It is not called automatically.
294
-
295
- ### Rendering Process (summary)
296
-
297
- ```js
298
- component.render(); // Re-render: empty → build → _processOptions → rendered = true
299
- ```
300
-
301
- Rendering behavior:
302
-
303
- 1. **Content clearing** - `empty()` removes existing children on re-render only (skipped on first render)
304
- 2. **Structure creation** - `build()` creates sub-elements before options are applied
305
- 3. **Priority processing** - `_processOptions()` applies `priorityOptions` keys first
306
- 4. **Option routing** - Each option processed via `_setOption`
307
- 5. **Completion flag** - Sets `rendered = true`
308
-
309
- ### Automatic Cleanup System
310
-
311
- Components automatically clean up resources when removed from DOM:
312
-
313
- ```js
314
- // Manual cleanup registration
315
- component.addCleanup('unique-id', () => {
316
- console.log('Custom cleanup executed');
317
- });
318
-
319
- // Manual cleanup execution (automatic on disconnect)
320
- component.processCleanup();
321
- ```
322
-
323
- **Automatic cleanup includes:**
324
-
325
- - Event listener removal
326
- - Oxject subscription cleanup
327
- - Style element removal from DOM
328
- - Recursive child component cleanup
329
-
330
- ### Render Timing Control
331
-
332
- Control when components render with `autoRender` option:
333
-
334
- ```js
335
- // Immediate rendering (default)
336
- new Component({ autoRender: true });
337
-
338
- // Render on window load event
339
- new Component({ autoRender: 'onload' });
340
-
341
- // Render on next animation frame
342
- new Component({ autoRender: 'animationFrame' });
343
-
344
- // Manual rendering only
345
- new Component({ autoRender: false });
346
- ```
347
-
348
- ## API Reference
349
-
350
- ### Constructor
351
-
352
- ```js
353
- new Component(options?, ...children)
354
- ```
355
-
356
- **Parameters:**
357
-
358
- - `options` (Object) - Configuration options
359
- - `children` (...Component|Elem) - Child elements appended to `append` option
360
-
361
- ### Configuration Options
362
-
363
- #### Component-Specific Options
364
-
365
- | Option | Type | Description |
366
- | ------------------ | ------------------------------------- | ------------------------------------------- |
367
- | `tag` | `string` | HTML tag name (default: 'div') |
368
- | `autoRender` | `boolean\|'onload'\|'animationFrame'` | Render timing control |
369
- | `registeredEvents` | `Set<string>` | Custom event types for on() method |
370
- | `knownAttributes` | `Set<string>` | Attribute names for elem.setAttribute() |
371
- | `priorityOptions` | `Set<string>` | Option keys processed first during render |
372
- | `styles` | `string\|object\|Function` | CSS string, style object, or theme function |
373
- | `uniqueId` | `string` | Override auto-generated unique ID |
374
-
375
- #### Event Handler Options
376
-
377
- | Option | Type | Description |
378
- | ---------------- | ---------- | -------------------------------------------- |
379
- | `onHover` | `Function` | Hover handler with move tracking |
380
- | `onPointerPress` | `Function` | Fires on `pointerdown` |
381
- | ~~`onClick`~~ | — | Not supported. Use `onPointerPress` instead. |
382
- | `onChange` | `Function` | Change event handler (input .value included) |
383
- | `onConnected` | `Function` | DOM connection detection |
384
- | `onDisconnected` | `Function` | DOM disconnection detection |
385
-
386
- All standard Elem options work as reactive Component options.
387
-
388
- ### Methods
389
-
390
- #### Options-Compatible Methods
391
-
392
- Can be called directly or used as reactive options:
393
-
394
- ```js
395
- component.styles(styleFunction); // Direct call
396
- component.onHover(callback); // Direct call
397
- component.addClass('active'); // Direct call
398
- component.append(child); // Direct call
399
-
400
- // Or as reactive options:
401
- new Component({
402
- styles: styleFunction,
403
- onHover: callback,
404
- addClass: 'active',
405
- append: [child],
406
- });
407
- ```
408
-
409
- #### Lifecycle Methods
410
-
411
- ```js
412
- component.build(); // Subclass structural hook — override to create child elements
413
- component.render(); // Re-render: empty → build → _processOptions → rendered = true
414
- component.addCleanup(id, fn); // Register cleanup function (chains with existing for same id)
415
- component.replaceCleanup(id, fn); // Replace cleanup, running the previous one immediately
416
- component.replaceDestroyCleanup(id, fn); // Destroy-only cleanup: survives disconnect, runs on destroy()
417
- component.processCleanup(); // Execute all cleanup functions
418
- component.destroy(); // Disconnect observer, run all cleanup, remove from DOM
419
- ```
420
-
421
- #### Event Methods
422
-
423
- ```js
424
- component.on({ targetEvent, callback, id? }); // Event registration
425
- component.emit(eventType, detail); // Event dispatch
426
- ```
427
-
428
- #### Utility Methods
429
-
430
- ```js
431
- component.ancestry(); // Prototype chain inspection
432
- ```
433
-
434
- ### Properties
435
-
436
- ```js
437
- component.parent; // Parent Component/Elem instance
438
- component.children; // Child Component/Elem instances
439
- component.uniqueId; // Frozen unique identifier
440
- component.options; // Reactive Oxject instance
441
- component.elem; // Underlying HTMLElement
442
- component.rendered; // Boolean render status flag
443
- ```
444
-
445
- ## Memory Management
446
-
447
- Components provide comprehensive automatic memory management:
448
-
449
- - **Event listeners** - Automatically removed on DOM disconnect
450
- - **Oxject subscriptions** - Cleaned up when component destroyed
451
- - **Style elements** - Removed from DOM head on cleanup
452
- - **Child components** - Recursive cleanup propagation
453
- - **Custom cleanup** - Manual cleanup function registration
454
-
455
- No manual cleanup required for typical usage - components handle resource management automatically.