@hypen-space/web 0.4.3 → 0.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypen-space/web",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Hypen web renderers - DOM and Canvas rendering for browsers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "clean": "rm -rf dist"
57
57
  },
58
58
  "dependencies": {
59
- "@hypen-space/core": "^0.4.3"
59
+ "@hypen-space/core": "^0.4.5"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/bun": "latest",
@@ -7,6 +7,11 @@
7
7
  import type { VirtualNode, Rectangle } from "./types.js";
8
8
  import { getAbsoluteBounds } from "./utils.js";
9
9
 
10
+ /** Minimal engine interface for dispatching bind actions */
11
+ interface BindEngine {
12
+ dispatchAction(name: string, payload?: unknown): void;
13
+ }
14
+
10
15
  /**
11
16
  * Input Overlay Manager
12
17
  */
@@ -15,6 +20,8 @@ export class InputOverlay {
15
20
  private overlay: HTMLInputElement | HTMLTextAreaElement | null = null;
16
21
  private focusedNode: VirtualNode | null = null;
17
22
  private onChangeCallback: ((value: string) => void) | null = null;
23
+ private bindPath: string | null = null;
24
+ private engine: BindEngine | null = null;
18
25
 
19
26
  constructor(container: HTMLElement | null) {
20
27
  this.container = container || ({} as HTMLElement);
@@ -26,13 +33,18 @@ export class InputOverlay {
26
33
  showInput(
27
34
  node: VirtualNode,
28
35
  canvasBounds: DOMRect,
29
- onChange: (value: string) => void
36
+ onChange: (value: string) => void,
37
+ engine?: BindEngine | null
30
38
  ): void {
31
39
  // Skip if document is not available (non-browser environment)
32
40
  if (typeof document === "undefined") return;
33
41
 
34
42
  this.hideInput();
35
43
 
44
+ // Store bind path and engine for two-way binding
45
+ this.bindPath = (node.props.bind as string) || null;
46
+ this.engine = engine || null;
47
+
36
48
  const bounds = getAbsoluteBounds(node);
37
49
  if (!bounds) return;
38
50
 
@@ -71,6 +83,8 @@ export class InputOverlay {
71
83
  }
72
84
  this.focusedNode = null;
73
85
  this.onChangeCallback = null;
86
+ this.bindPath = null;
87
+ this.engine = null;
74
88
  }
75
89
 
76
90
  /**
@@ -164,6 +178,14 @@ export class InputOverlay {
164
178
 
165
179
  const value = this.overlay.value;
166
180
  this.onChangeCallback(value);
181
+
182
+ // Dispatch __hypen_bind for two-way binding
183
+ if (this.bindPath && this.engine) {
184
+ this.engine.dispatchAction("__hypen_bind", {
185
+ path: this.bindPath,
186
+ value,
187
+ });
188
+ }
167
189
  }
168
190
 
169
191
  /**
@@ -492,4 +492,75 @@ export const eventHandlers: Record<string, ApplicatorHandler> = {
492
492
  // Mouse hover events
493
493
  onMouseEnter: createEventHandler("mouseenter", { extractPayload: mousePayload }),
494
494
  onMouseLeave: createEventHandler("mouseleave", { extractPayload: mousePayload }),
495
+
496
+ // Two-way binding for .bind(@state.x)
497
+ bind: ((element: HTMLElement, value: unknown) => {
498
+ const bindPath = typeof value === "string" ? value : null;
499
+ if (!bindPath) return;
500
+
501
+ const disposables = getElementDisposables(element);
502
+ const eventKey = `bind:${bindPath}`;
503
+ if (getRegisteredEvents(element).has(eventKey)) return;
504
+ registerEvent(element, eventKey);
505
+
506
+ // Determine the target element, event type, and value extractor based on component type
507
+ const hypenType = element.dataset?.hypenType;
508
+
509
+ if (hypenType === "checkbox" || hypenType === "switch") {
510
+ // Checkbox/Switch: wrapper <label> containing <input type="checkbox">
511
+ const input = element.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
512
+ if (!input) return;
513
+
514
+ const listener = () => {
515
+ const engine = getEngine(element);
516
+ if (engine) {
517
+ engine.dispatchAction("__hypen_bind", {
518
+ path: bindPath,
519
+ value: input.checked,
520
+ });
521
+ }
522
+ };
523
+
524
+ disposables.add(
525
+ disposableListener(input, "change", listener, { passive: true })
526
+ );
527
+ } else if (element instanceof HTMLSelectElement) {
528
+ // Select: listen to change event, read .value
529
+ const listener = () => {
530
+ const engine = getEngine(element);
531
+ if (engine) {
532
+ engine.dispatchAction("__hypen_bind", {
533
+ path: bindPath,
534
+ value: element.value,
535
+ });
536
+ }
537
+ };
538
+
539
+ disposables.add(
540
+ disposableListener(element, "change", listener, { passive: true })
541
+ );
542
+ } else if (
543
+ element instanceof HTMLInputElement ||
544
+ element instanceof HTMLTextAreaElement
545
+ ) {
546
+ // Input/Textarea: listen to input event, read .value
547
+ const listener = () => {
548
+ const engine = getEngine(element);
549
+ if (engine) {
550
+ engine.dispatchAction("__hypen_bind", {
551
+ path: bindPath,
552
+ value: element.value,
553
+ });
554
+ }
555
+ };
556
+
557
+ disposables.add(
558
+ disposableListener(element, "input", listener, { passive: true })
559
+ );
560
+ } else {
561
+ return;
562
+ }
563
+
564
+ disposables.addCallback(() => unregisterEvent(element, eventKey));
565
+ }) as ApplicatorHandler,
495
566
  };
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Patch } from "@hypen-space/core/types";
8
- import type { HypenModuleInstance, RouterContext } from "@hypen-space/core/app";
8
+ import type { HypenModuleInstance } from "@hypen-space/core/app";
9
9
  import type { HypenRouter } from "@hypen-space/core/router";
10
10
  import type { HypenGlobalContext } from "@hypen-space/core/context";
11
11
  import { frameworkLoggers } from "@hypen-space/core/logger";
@@ -30,7 +30,7 @@ export class DOMRenderer {
30
30
  private applicators: ApplicatorRegistry;
31
31
  private engine: IEngine;
32
32
  private currentState: Record<string, any> = {};
33
- private routerContext: RouterContext | null = null;
33
+ private router: HypenRouter | null = null;
34
34
  private globalContext: HypenGlobalContext | null = null;
35
35
  private componentInstances = new Map<string, HypenModuleInstance>();
36
36
  private debugTracker: RerenderTracker;
@@ -52,8 +52,8 @@ export class DOMRenderer {
52
52
  /**
53
53
  * Set router and global context for component composition
54
54
  */
55
- setContext(routerContext: RouterContext, globalContext: HypenGlobalContext): void {
56
- this.routerContext = routerContext;
55
+ setContext(router: HypenRouter | null, globalContext: HypenGlobalContext): void {
56
+ this.router = router;
57
57
  this.globalContext = globalContext;
58
58
  }
59
59
 
package/src/hypen.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { Engine } from "@hypen-space/core/engine/browser";
8
8
  import { HypenModuleInstance } from "@hypen-space/core/app";
9
- import type { RouterContext, HypenModuleDefinition } from "@hypen-space/core/app";
9
+ import type { HypenModuleDefinition } from "@hypen-space/core/app";
10
10
  import { HypenRouter } from "@hypen-space/core/router";
11
11
  import { HypenGlobalContext } from "@hypen-space/core/context";
12
12
  import { componentLoader } from "@hypen-space/core/loader";
@@ -65,8 +65,7 @@ export class Hypen {
65
65
  componentLoader.register("Route", Route, "");
66
66
  componentLoader.register("Link", Link, "");
67
67
 
68
- // Store router and hypen instance in global context for access in components
69
- (this.globalContext as any).__router = this.router;
68
+ // Store hypen engine instance in global context for built-in component access
70
69
  (this.globalContext as any).__hypenEngine = this;
71
70
  }
72
71
 
@@ -150,14 +149,8 @@ export class Hypen {
150
149
  this.renderer!.applyPatches(patches);
151
150
  });
152
151
 
153
- // Create router context
154
- const routerContext: RouterContext = {
155
- root: this.router,
156
- current: this.router,
157
- };
158
-
159
152
  // Set context on renderer for component composition
160
- this.renderer.setContext(routerContext, this.globalContext);
153
+ this.renderer.setContext(this.router, this.globalContext);
161
154
 
162
155
  // Extract module ID from component name or .id() applicator
163
156
  const moduleId = this.extractModuleId(componentName, component.template);
@@ -166,7 +159,7 @@ export class Hypen {
166
159
  this.moduleInstance = new HypenModuleInstance(
167
160
  this.engine,
168
161
  component.module,
169
- routerContext,
162
+ this.router,
170
163
  this.globalContext
171
164
  );
172
165
 
@@ -305,15 +298,10 @@ export class Hypen {
305
298
  if (component.module && !this.moduleInstances.has(componentName)) {
306
299
  const moduleId = this.extractModuleId(componentName, template);
307
300
 
308
- const routerContext: RouterContext = {
309
- root: this.router,
310
- current: this.router,
311
- };
312
-
313
301
  const moduleInstance = new HypenModuleInstance(
314
302
  this.engine,
315
303
  component.module,
316
- routerContext,
304
+ this.router,
317
305
  this.globalContext
318
306
  );
319
307
 
@@ -366,17 +354,12 @@ export class Hypen {
366
354
  * Create module instances for all components that have state
367
355
  */
368
356
  private createNestedModuleInstances(): void {
369
- const routerContext: RouterContext = {
370
- root: this.router,
371
- current: this.router,
372
- };
373
-
374
357
  // Create built-in Router module instance
375
358
  if (Router && !this.moduleInstances.has("Router")) {
376
359
  const routerInstance = new HypenModuleInstance(
377
360
  this.engine!,
378
361
  Router,
379
- routerContext,
362
+ this.router,
380
363
  this.globalContext
381
364
  );
382
365
  this.globalContext.registerModule("Router", routerInstance);
@@ -408,7 +391,7 @@ export class Hypen {
408
391
  const moduleInstance = new HypenModuleInstance(
409
392
  this.engine!,
410
393
  comp.module,
411
- routerContext,
394
+ this.router,
412
395
  this.globalContext
413
396
  );
414
397