@jay-framework/runtime 0.6.10 → 0.7.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/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ interface updateFunc<T> {
26
26
  _origUpdates?: Array<updateFunc<T>>;
27
27
  }
28
28
  type MountFunc = () => void;
29
+ declare const noop: () => void;
29
30
  declare const noopUpdate: updateFunc<any>;
30
31
  declare const noopMount: MountFunc;
31
32
  interface BaseJayElement<ViewState> {
@@ -72,6 +73,15 @@ type JayContract<ViewState extends object, Refs extends object> = {
72
73
  };
73
74
  type ExtractViewState<A> = A extends JayContract<infer ViewState, any> ? ViewState : never;
74
75
  type ExtractRefs<A> = A extends JayContract<any, infer Refs> ? Refs : never;
76
+ declare enum LogType {
77
+ ASYNC_ERROR = 0,
78
+ CONTEXT_NOT_FOUND = 1
79
+ }
80
+ type JayLog = {
81
+ log: (type: LogType) => void;
82
+ error: (type: LogType, error: Error) => void;
83
+ };
84
+ declare const jayLog: JayLog;
75
85
 
76
86
  declare module '@jay-framework/runtime' {
77
87
  interface BaseJayElement<ViewState> {
@@ -433,21 +443,41 @@ interface Conditional<ViewState> {
433
443
  condition: (newData: ViewState) => boolean;
434
444
  elem: () => BaseJayElement<ViewState> | TextElement<ViewState>;
435
445
  }
436
- declare function isCondition<ViewState>(c: Conditional<ViewState> | ForEach<ViewState, any> | TextElement<ViewState> | BaseJayElement<ViewState>): c is Conditional<ViewState>;
437
- declare function isForEach<ViewState, Item>(c: Conditional<ViewState> | ForEach<ViewState, Item> | TextElement<ViewState> | BaseJayElement<ViewState>): c is ForEach<ViewState, Item>;
446
+ declare enum WhenRole {
447
+ pending = 0,
448
+ resolved = 1,
449
+ rejected = 2
450
+ }
451
+ interface When<ViewState, ResolvedViewValue> {
452
+ role: WhenRole;
453
+ promise: (newData: ViewState) => Promise<ResolvedViewValue>;
454
+ elem: () => BaseJayElement<ResolvedViewValue> | TextElement<ResolvedViewValue> | string;
455
+ }
456
+ declare const resolved: <ViewState, ResolvedViewValue>(promise: (newData: ViewState) => Promise<ResolvedViewValue>, elem: () => string | BaseJayElement<ResolvedViewValue> | TextElement<ResolvedViewValue>) => When<ViewState, ResolvedViewValue>;
457
+ declare const rejected: <ViewState>(promise: (newData: ViewState) => Promise<any>, elem: () => BaseJayElement<Error> | TextElement<Error> | string) => When<ViewState, any>;
458
+ declare const pending: <ViewState>(promise: (newData: ViewState) => Promise<any>, elem: () => BaseJayElement<never> | TextElement<never> | string) => When<ViewState, any>;
459
+ declare function isCondition<ViewState>(c: Conditional<ViewState> | When<ViewState, any> | ForEach<ViewState, any> | WithData<ViewState, any> | TextElement<ViewState> | BaseJayElement<ViewState>): c is Conditional<ViewState>;
460
+ declare function isForEach<ViewState, Item>(c: Conditional<ViewState> | When<ViewState, any> | ForEach<ViewState, Item> | WithData<ViewState, any> | TextElement<ViewState> | BaseJayElement<ViewState>): c is ForEach<ViewState, Item>;
461
+ declare function isWhen<ViewState, Item>(c: Conditional<ViewState> | When<ViewState, any> | ForEach<ViewState, Item> | WithData<ViewState, any> | TextElement<ViewState> | BaseJayElement<ViewState>): c is When<ViewState, any>;
462
+ declare function isWithData<ViewState, ChildViewState>(c: Conditional<ViewState> | When<ViewState, any> | ForEach<ViewState, any> | WithData<ViewState, ChildViewState> | TextElement<ViewState> | BaseJayElement<ViewState>): c is WithData<ViewState, ChildViewState>;
438
463
  declare function forEach<T, Item>(getItems: (T: any) => Array<Item>, elemCreator: (Item: any) => BaseJayElement<Item>, matchBy: string): ForEach<T, Item>;
439
464
  interface ForEach<ViewState, Item> {
440
465
  getItems: (T: any) => Array<Item>;
441
466
  elemCreator: (Item: any, String: any) => BaseJayElement<Item>;
442
467
  trackBy: string;
443
468
  }
469
+ declare function withData<ParentViewState, ChildViewState>(accessor: (data: ParentViewState) => ChildViewState | null | undefined, elem: () => BaseJayElement<ChildViewState>): WithData<ParentViewState, ChildViewState>;
470
+ interface WithData<ParentViewState, ChildViewState> {
471
+ accessor: (data: ParentViewState) => ChildViewState | null | undefined;
472
+ elem: () => BaseJayElement<ChildViewState>;
473
+ }
444
474
  declare function mkUpdateCollection<ViewState, Item>(child: ForEach<ViewState, Item>, group: KindergartenGroup): [updateFunc<ViewState>, MountFunc, MountFunc];
445
475
  declare function dynamicText<ViewState>(textContent: (vs: ViewState) => string | number | boolean): TextElement<ViewState>;
446
476
  type ElementChildren<ViewState> = Array<BaseJayElement<ViewState> | TextElement<ViewState> | string>;
447
477
  declare const element: <ViewState>(tagName: string, attributes: Attributes<ViewState>, children?: ElementChildren<ViewState>, ref?: PrivateRef<ViewState, BaseJayElement<ViewState>>) => BaseJayElement<ViewState>;
448
478
  declare const svgElement: <ViewState>(tagName: string, attributes: Attributes<ViewState>, children?: ElementChildren<ViewState>, ref?: PrivateRef<ViewState, BaseJayElement<ViewState>>) => BaseJayElement<ViewState>;
449
479
  declare const mathMLElement: <ViewState>(tagName: string, attributes: Attributes<ViewState>, children?: ElementChildren<ViewState>, ref?: PrivateRef<ViewState, BaseJayElement<ViewState>>) => BaseJayElement<ViewState>;
450
- type DynamicElementChildren<ViewState> = Array<Conditional<ViewState> | ForEach<ViewState, any> | TextElement<ViewState> | BaseJayElement<ViewState> | string>;
480
+ type DynamicElementChildren<ViewState> = Array<Conditional<ViewState> | ForEach<ViewState, any> | WithData<ViewState, any> | TextElement<ViewState> | BaseJayElement<ViewState> | When<ViewState, any> | string>;
451
481
  declare const dynamicElementNS: (ns: string) => <ViewState>(tagName: string, attributes: Attributes<ViewState>, children?: DynamicElementChildren<ViewState>, ref?: PrivateRef<ViewState, BaseJayElement<ViewState>>) => BaseJayElement<ViewState>;
452
482
  declare const dynamicElement: <ViewState>(tagName: string, attributes: Attributes<ViewState>, children?: DynamicElementChildren<ViewState>, ref?: PrivateRef<ViewState, BaseJayElement<ViewState>>) => BaseJayElement<ViewState>;
453
483
  declare const svgDynamicElement: <ViewState>(tagName: string, attributes: Attributes<ViewState>, children?: DynamicElementChildren<ViewState>, ref?: PrivateRef<ViewState, BaseJayElement<ViewState>>) => BaseJayElement<ViewState>;
@@ -481,7 +511,8 @@ declare class ConstructContext<ViewState> {
481
511
  get currData(): ViewState;
482
512
  coordinate: (refName: string) => Coordinate;
483
513
  forItem<ChildViewState>(childViewState: ChildViewState, id: string): ConstructContext<ChildViewState>;
514
+ forAsync<ChildViewState>(childViewState: ChildViewState): ConstructContext<ChildViewState>;
484
515
  static withRootContext<ViewState, Refs>(viewState: ViewState, refManager: ReferencesManager, elementConstructor: () => BaseJayElement<ViewState>): JayElement<ViewState, Refs>;
485
516
  }
486
517
 
487
- export { type Attribute, type Attributes, type BaseJayElement, BaseReferencesManager, type ComponentCollectionProxy, ComponentCollectionRefImpl, type ComponentProxy, ComponentRefImpl, ComponentRefsImpl, type Conditional, ConstructContext, type ContextMarker, type Coordinate, type DynamicAttributeOrProperty, EVENT_TRAP, type ElementFrom, type EventEmitter, type EventTypeFrom, type ExtractRefs, type ExtractViewState, type ForEach, GetTrapProxy, type GlobalJayEvents, type HTMLElementCollectionProxy, type HTMLElementCollectionProxyTarget, type HTMLElementProxy, type HTMLElementProxyTarget, type HTMLNativeExec, type HeadLink, type JayComponent, type JayComponentConstructor, type JayContract, type JayElement, type JayEvent, type JayEventHandler, type JayEventHandlerWrapper, type JayNativeEventBuilder, type JayNativeFunction, type ManagedRefConstructor, ManagedRefType, type ManagedRefs, type MapEventEmitterViewState, type MountFunc, type OnlyEventEmitters, type PreRenderElement, type PrivateRef, type PrivateRefConstructor, PrivateRefs, type PropsFrom, ReferencesManager, type RenderElement, type RenderElementOptions, type TextElement, type ViewStateFrom, booleanAttribute, childComp, conditional, createJayContext, currentConstructionContext, defaultEventWrapper, dynamicAttribute, dynamicElement, dynamicElementNS, dynamicProperty, dynamicText, element, findContext, forEach, injectHeadLinks, isCondition, isForEach, mathMLDynamicElement, mathMLElement, mkUpdateCollection, noopMount, noopUpdate, normalizeMount, normalizeUpdates, restoreContext, saveContext, svgDynamicElement, svgElement, type updateFunc, useContext, withContext };
518
+ export { type Attribute, type Attributes, type BaseJayElement, BaseReferencesManager, type ComponentCollectionProxy, ComponentCollectionRefImpl, type ComponentProxy, ComponentRefImpl, ComponentRefsImpl, type Conditional, ConstructContext, type ContextMarker, type Coordinate, type DynamicAttributeOrProperty, EVENT_TRAP, type ElementFrom, type EventEmitter, type EventTypeFrom, type ExtractRefs, type ExtractViewState, type ForEach, GetTrapProxy, type GlobalJayEvents, type HTMLElementCollectionProxy, type HTMLElementCollectionProxyTarget, type HTMLElementProxy, type HTMLElementProxyTarget, type HTMLNativeExec, type HeadLink, type JayComponent, type JayComponentConstructor, type JayContract, type JayElement, type JayEvent, type JayEventHandler, type JayEventHandlerWrapper, type JayLog, type JayNativeEventBuilder, type JayNativeFunction, LogType, type ManagedRefConstructor, ManagedRefType, type ManagedRefs, type MapEventEmitterViewState, type MountFunc, type OnlyEventEmitters, type PreRenderElement, type PrivateRef, type PrivateRefConstructor, PrivateRefs, type PropsFrom, ReferencesManager, type RenderElement, type RenderElementOptions, type TextElement, type ViewStateFrom, type When, WhenRole, type WithData, booleanAttribute, childComp, conditional, createJayContext, currentConstructionContext, defaultEventWrapper, dynamicAttribute, dynamicElement, dynamicElementNS, dynamicProperty, dynamicText, element, findContext, forEach, injectHeadLinks, isCondition, isForEach, isWhen, isWithData, jayLog, mathMLDynamicElement, mathMLElement, mkUpdateCollection, noop, noopMount, noopUpdate, normalizeMount, normalizeUpdates, pending, rejected, resolved, restoreContext, saveContext, svgDynamicElement, svgElement, type updateFunc, useContext, withContext, withData };
package/dist/index.js CHANGED
@@ -80,9 +80,18 @@ class Kindergarten {
80
80
  return offset;
81
81
  }
82
82
  }
83
- const noopUpdate = (_newData) => {
83
+ const noop = () => {
84
84
  };
85
- const noopMount = () => {
85
+ const noopUpdate = noop;
86
+ const noopMount = noop;
87
+ var LogType = /* @__PURE__ */ ((LogType2) => {
88
+ LogType2[LogType2["ASYNC_ERROR"] = 0] = "ASYNC_ERROR";
89
+ LogType2[LogType2["CONTEXT_NOT_FOUND"] = 1] = "CONTEXT_NOT_FOUND";
90
+ return LogType2;
91
+ })(LogType || {});
92
+ const jayLog = {
93
+ log: noop,
94
+ error: noop
86
95
  };
87
96
  let currentContext = void 0;
88
97
  function NewContextStack(context, marker, parent) {
@@ -157,6 +166,9 @@ class ConstructContext {
157
166
  forItem(childViewState, id) {
158
167
  return new ConstructContext(childViewState, false, [...this.coordinateBase, id]);
159
168
  }
169
+ forAsync(childViewState) {
170
+ return new ConstructContext(childViewState, false, [...this.coordinateBase]);
171
+ }
160
172
  static withRootContext(viewState, refManager, elementConstructor) {
161
173
  let context = new ConstructContext(viewState);
162
174
  let element2 = withContext(
@@ -239,15 +251,110 @@ function conditional(condition, elem) {
239
251
  }
240
252
  };
241
253
  }
254
+ var WhenRole = /* @__PURE__ */ ((WhenRole2) => {
255
+ WhenRole2[WhenRole2["pending"] = 0] = "pending";
256
+ WhenRole2[WhenRole2["resolved"] = 1] = "resolved";
257
+ WhenRole2[WhenRole2["rejected"] = 2] = "rejected";
258
+ return WhenRole2;
259
+ })(WhenRole || {});
260
+ function when(status, promise, elem) {
261
+ return {
262
+ role: status,
263
+ promise,
264
+ elem
265
+ };
266
+ }
267
+ const resolved = (promise, elem) => when(1, promise, elem);
268
+ const rejected = (promise, elem) => when(2, promise, elem);
269
+ const pending = (promise, elem) => when(0, promise, elem);
242
270
  function isCondition(c) {
243
271
  return c.condition !== void 0;
244
272
  }
245
273
  function isForEach(c) {
246
274
  return c.elemCreator !== void 0;
247
275
  }
276
+ function isWhen(c) {
277
+ return c.promise !== void 0;
278
+ }
279
+ function isWithData(c) {
280
+ return c.accessor !== void 0;
281
+ }
282
+ function mkWhenCondition(when2, group) {
283
+ switch (when2.role) {
284
+ case 1:
285
+ return mkWhenResolvedCondition(when2, group);
286
+ case 2:
287
+ return mkWhenRejectedCondition(when2, group);
288
+ default:
289
+ return mkWhenPendingCondition(when2, group);
290
+ }
291
+ }
292
+ const Hide = Symbol();
293
+ function mkWhenConditionBase(when2, group, setupPromise) {
294
+ let show = false;
295
+ let savedValue = void 0;
296
+ const parentContext = currentConstructionContext();
297
+ const savedContext = saveContext();
298
+ const [cUpdate, cMouth, cUnmount] = mkUpdateCondition(
299
+ conditional(
300
+ () => show,
301
+ () => {
302
+ let childContext = parentContext.forAsync(savedValue);
303
+ return restoreContext(savedContext, () => {
304
+ return withContext(CONSTRUCTION_CONTEXT_MARKER, childContext, () => {
305
+ return when2.elem();
306
+ });
307
+ });
308
+ }
309
+ ),
310
+ group
311
+ );
312
+ let currentPromise = when2.promise(parentContext.currData);
313
+ const handleValue = (changedPromise) => (value) => {
314
+ if (changedPromise === currentPromise) {
315
+ show = value !== Hide;
316
+ savedValue = value;
317
+ cUpdate(value);
318
+ }
319
+ };
320
+ setupPromise(currentPromise, handleValue(currentPromise));
321
+ const update = (viewState) => {
322
+ const newValue = when2.promise(viewState);
323
+ if (currentPromise !== newValue) {
324
+ currentPromise = newValue;
325
+ setupPromise(currentPromise, handleValue(currentPromise));
326
+ show = false;
327
+ restoreContext(savedContext, () => cUpdate(void 0));
328
+ }
329
+ };
330
+ return [update, cMouth, cUnmount];
331
+ }
332
+ function mkWhenResolvedCondition(when2, group) {
333
+ return mkWhenConditionBase(
334
+ when2,
335
+ group,
336
+ (promise, handleValue) => promise.then(handleValue).catch((err) => jayLog.error(LogType.ASYNC_ERROR, err))
337
+ );
338
+ }
339
+ function mkWhenRejectedCondition(when2, group) {
340
+ return mkWhenConditionBase(when2, group, (promise, handleValue) => promise.catch(handleValue));
341
+ }
342
+ function mkWhenPendingCondition(when2, group) {
343
+ let timeout;
344
+ return mkWhenConditionBase(when2, group, (promise, handleValue) => {
345
+ timeout = setTimeout(() => handleValue(void 0), 1);
346
+ promise.finally(() => {
347
+ clearTimeout(timeout);
348
+ handleValue(Hide);
349
+ }).catch((err) => jayLog.error(LogType.ASYNC_ERROR, err));
350
+ });
351
+ }
248
352
  function forEach(getItems, elemCreator, matchBy) {
249
353
  return { getItems, elemCreator, trackBy: matchBy };
250
354
  }
355
+ function withData(accessor, elem) {
356
+ return { accessor, elem };
357
+ }
251
358
  function applyListChanges(group, instructions) {
252
359
  instructions.forEach((instruction) => {
253
360
  if (instruction.action === ITEM_ADDED) {
@@ -303,13 +410,16 @@ function mkUpdateCondition(child, group) {
303
410
  let mount = noopMount, unmount = noopMount;
304
411
  let lastResult = false;
305
412
  let childElement = void 0;
413
+ const savedContext = saveContext();
306
414
  const update = (newData) => {
307
- if (!childElement) {
308
- childElement = child.elem();
415
+ const result = child.condition(newData);
416
+ if (!childElement && result) {
417
+ restoreContext(savedContext, () => {
418
+ childElement = child.elem();
419
+ });
309
420
  mount = () => lastResult && childElement.mount();
310
421
  unmount = () => childElement.unmount();
311
422
  }
312
- let result = child.condition(newData);
313
423
  if (result) {
314
424
  if (!lastResult) {
315
425
  group.ensureNode(childElement.dom);
@@ -324,6 +434,46 @@ function mkUpdateCondition(child, group) {
324
434
  };
325
435
  return [update, mount, unmount];
326
436
  }
437
+ function mkUpdateWithData(child, group) {
438
+ let mount = noopMount, unmount = noopMount;
439
+ let lastResult = false;
440
+ let childElement = void 0;
441
+ const parentContext = currentConstructionContext();
442
+ const savedContext = saveContext();
443
+ const update = (newData) => {
444
+ const childData = child.accessor(newData);
445
+ const result = childData != null;
446
+ if (!childElement && result) {
447
+ const childContext = parentContext.forAsync(childData);
448
+ childElement = restoreContext(
449
+ savedContext,
450
+ () => withContext(CONSTRUCTION_CONTEXT_MARKER, childContext, () => child.elem())
451
+ );
452
+ mount = () => lastResult && childElement.mount();
453
+ unmount = () => childElement.unmount();
454
+ }
455
+ if (result) {
456
+ if (!lastResult) {
457
+ group.ensureNode(childElement.dom);
458
+ childElement.mount();
459
+ }
460
+ const childContext = parentContext.forAsync(childData);
461
+ restoreContext(
462
+ savedContext,
463
+ () => withContext(
464
+ CONSTRUCTION_CONTEXT_MARKER,
465
+ childContext,
466
+ () => childElement.update(childData)
467
+ )
468
+ );
469
+ } else if (lastResult) {
470
+ childElement.unmount();
471
+ group.removeNode(childElement.dom);
472
+ }
473
+ lastResult = result;
474
+ };
475
+ return [update, mount, unmount];
476
+ }
327
477
  function text(content) {
328
478
  return {
329
479
  dom: document.createTextNode(content),
@@ -386,6 +536,10 @@ const dynamicElementNS = (ns) => (tagName, attributes, children = [], ref) => {
386
536
  [update, mount, unmount] = mkUpdateCondition(child, group);
387
537
  } else if (isForEach(child)) {
388
538
  [update, mount, unmount] = mkUpdateCollection(child, group);
539
+ } else if (isWithData(child)) {
540
+ [update, mount, unmount] = mkUpdateWithData(child, group);
541
+ } else if (isWhen(child)) {
542
+ [update, mount, unmount] = mkWhenCondition(child, group);
389
543
  } else {
390
544
  group.ensureNode(child.dom);
391
545
  if (child.update !== noopUpdate)
@@ -822,9 +976,11 @@ export {
822
976
  ConstructContext,
823
977
  EVENT_TRAP,
824
978
  GetTrapProxy,
979
+ LogType,
825
980
  ManagedRefType,
826
981
  PrivateRefs,
827
982
  ReferencesManager,
983
+ WhenRole,
828
984
  booleanAttribute,
829
985
  childComp,
830
986
  conditional,
@@ -842,17 +998,25 @@ export {
842
998
  injectHeadLinks,
843
999
  isCondition,
844
1000
  isForEach,
1001
+ isWhen,
1002
+ isWithData,
1003
+ jayLog,
845
1004
  mathMLDynamicElement,
846
1005
  mathMLElement,
847
1006
  mkUpdateCollection,
1007
+ noop,
848
1008
  noopMount,
849
1009
  noopUpdate,
850
1010
  normalizeMount,
851
1011
  normalizeUpdates,
1012
+ pending,
1013
+ rejected,
1014
+ resolved,
852
1015
  restoreContext,
853
1016
  saveContext,
854
1017
  svgDynamicElement,
855
1018
  svgElement,
856
1019
  useContext,
857
- withContext
1020
+ withContext,
1021
+ withData
858
1022
  };
package/docs/runtime.md CHANGED
@@ -319,23 +319,271 @@ at which
319
319
 
320
320
  ## forEach
321
321
 
322
+ The `forEach` function creates a dynamic collection of child elements that automatically updates when the underlying array data changes. It efficiently manages adding, removing, and reordering child elements based on a tracking key.
323
+
324
+ `forEach` is used within a `dynamicElement` to render lists of items, where each item in the array becomes a separate child element. The runtime intelligently tracks elements by a specified key property to minimize DOM manipulations during updates.
325
+
322
326
  ```typescript
323
327
  declare function forEach<ViewState, Item>(
324
- getItems: (T: any) => Array<Item>,
325
- elemCreator: (Item: any) => JayElement<Item>,
326
- matchBy: string,
328
+ getItems: (vs: ViewState) => Array<Item>,
329
+ elemCreator: (item: Item, trackByValue: string) => BaseJayElement<Item>,
330
+ trackBy: string,
327
331
  ): ForEach<ViewState, Item>;
328
332
  ```
329
333
 
334
+ at which
335
+
336
+ - `ViewState` - the type of the parent element's view state containing the array
337
+ - `Item` - the type of individual items in the array
338
+ - `getItems` - a function that extracts the array of items from the parent's view state
339
+ - `elemCreator` - a factory function that creates a child element for each item, receives:
340
+ - `item` - the current item data
341
+ - `trackByValue` - the value of the tracking key for this item (useful for setting element IDs)
342
+ - `trackBy` - the property name used to track elements across updates (typically an `id` or `key` field)
343
+
344
+ The `trackBy` parameter is crucial for performance - it tells Jay how to identify which elements are the same across updates, enabling efficient reordering and minimizing unnecessary DOM operations.
345
+
346
+ Example usage for a simple list:
347
+
348
+ ```typescript
349
+ import {
350
+ forEach,
351
+ dynamicElement as de,
352
+ element as e,
353
+ dynamicText as dt,
354
+ } from '@jay-framework/runtime';
355
+
356
+ interface TodoItem {
357
+ id: string;
358
+ text: string;
359
+ completed: boolean;
360
+ }
361
+
362
+ interface ViewState {
363
+ todos: TodoItem[];
364
+ }
365
+
366
+ de('ul', { class: 'todo-list' }, [
367
+ forEach(
368
+ (vs) => vs.todos,
369
+ (item: TodoItem) =>
370
+ e('li', { id: item.id, class: item.completed ? 'completed' : '' }, [dt((item) => item.text)]),
371
+ 'id',
372
+ ),
373
+ ]);
374
+ ```
375
+
376
+ Example with nested structure:
377
+
378
+ ```typescript
379
+ interface Message {
380
+ id: string;
381
+ author: string;
382
+ text: string;
383
+ timestamp: number;
384
+ }
385
+
386
+ interface ChatViewState {
387
+ messages: Message[];
388
+ }
389
+
390
+ de('div', { class: 'chat' }, [
391
+ forEach(
392
+ (vs) => vs.messages,
393
+ (msg: Message) =>
394
+ e('div', { class: 'message', id: msg.id }, [
395
+ e('div', { class: 'author' }, [dt((m) => m.author)]),
396
+ e('div', { class: 'text' }, [dt((m) => m.text)]),
397
+ e('div', { class: 'time' }, [dt((m) => new Date(m.timestamp).toLocaleTimeString())]),
398
+ ]),
399
+ 'id',
400
+ ),
401
+ ]);
402
+ ```
403
+
404
+ **Key Features**:
405
+
406
+ - **Efficient Updates**: Only modifies DOM elements that actually changed
407
+ - **Smart Reordering**: Moves existing elements rather than recreating them when order changes
408
+ - **Memory Management**: Properly unmounts removed elements
409
+ - **Empty Arrays**: Gracefully handles empty arrays and null/undefined values
410
+
411
+ The Jay-HTML compiler automatically generates `forEach` calls when encountering `forEach` attributes in Jay-HTML files.
412
+
330
413
  ## conditional
331
414
 
415
+ The `conditional` function conditionally renders a child element based on a boolean expression. When the condition is true, the element is rendered and mounted; when false, it is unmounted and removed from the DOM.
416
+
417
+ `conditional` is similar to an `if` statement in programming - it determines whether a piece of UI should be visible based on the current view state. The child element is lazily created only when the condition first becomes true.
418
+
332
419
  ```typescript
333
420
  declare function conditional<ViewState>(
334
- condition: (newData: ViewState) => boolean,
335
- elem: JayElement<ViewState> | TextElement<ViewState> | string,
421
+ condition: (vs: ViewState) => boolean,
422
+ elem: () => BaseJayElement<ViewState> | TextElement<ViewState>,
336
423
  ): Conditional<ViewState>;
337
424
  ```
338
425
 
426
+ at which
427
+
428
+ - `ViewState` - the type of the element's view state
429
+ - `condition` - a function that evaluates to true (render element) or false (hide element)
430
+ - `elem` - a factory function that creates the conditional element (called only when condition is first true)
431
+
432
+ Example usage for showing/hiding UI elements:
433
+
434
+ ```typescript
435
+ import {
436
+ conditional,
437
+ dynamicElement as de,
438
+ element as e,
439
+ dynamicText as dt,
440
+ } from '@jay-framework/runtime';
441
+
442
+ interface ViewState {
443
+ isLoggedIn: boolean;
444
+ username: string;
445
+ hasErrors: boolean;
446
+ errorMessage: string;
447
+ }
448
+
449
+ de('div', { class: 'header' }, [
450
+ conditional(
451
+ (vs) => vs.isLoggedIn,
452
+ () => e('div', { class: 'user-info' }, [dt((vs) => `Welcome, ${vs.username}`)]),
453
+ ),
454
+ conditional(
455
+ (vs) => !vs.isLoggedIn,
456
+ () => e('button', {}, ['Login']),
457
+ ),
458
+ conditional(
459
+ (vs) => vs.hasErrors,
460
+ () => e('div', { class: 'error' }, [dt((vs) => vs.errorMessage)]),
461
+ ),
462
+ ]);
463
+ ```
464
+
465
+ Example with complex conditions:
466
+
467
+ ```typescript
468
+ interface FormState {
469
+ step: number;
470
+ isValid: boolean;
471
+ isSubmitting: boolean;
472
+ }
473
+
474
+ de('div', { class: 'form' }, [
475
+ conditional(
476
+ (vs) => vs.step === 1,
477
+ () => e('div', { class: 'step-1' }, ['Step 1: Basic Info']),
478
+ ),
479
+ conditional(
480
+ (vs) => vs.step === 2,
481
+ () => e('div', { class: 'step-2' }, ['Step 2: Details']),
482
+ ),
483
+ conditional(
484
+ (vs) => vs.step === 3 && vs.isValid,
485
+ () => e('div', { class: 'step-3' }, ['Step 3: Review']),
486
+ ),
487
+ conditional(
488
+ (vs) => vs.isSubmitting,
489
+ () => e('div', { class: 'spinner' }, ['Loading...']),
490
+ ),
491
+ ]);
492
+ ```
493
+
494
+ **Key Features**:
495
+
496
+ - **Lazy Creation**: The element is only created when the condition first becomes true
497
+ - **Efficient Toggling**: Mounting/unmounting is efficient when toggling visibility
498
+ - **Clean DOM**: When false, the element is completely removed from the DOM (not just hidden)
499
+ - **Lifecycle Management**: Properly calls mount/unmount hooks when condition changes
500
+
501
+ **Note**: Complex boolean expressions with `&&` or `||` may need to be split into nested conditionals depending on the Jay-HTML parser capabilities.
502
+
503
+ The Jay-HTML compiler automatically generates `conditional` calls when encountering `if` attributes in Jay-HTML files.
504
+
505
+ ## withData
506
+
507
+ The `withData` function enables context switching for child elements with a different data type. This is primarily used for recursive structures where a child element needs to operate on a subset or related piece of the parent's data.
508
+
509
+ `withData` is similar to `conditional`, but instead of checking a boolean condition, it:
510
+
511
+ 1. Checks if the accessor function returns a non-null/undefined value
512
+ 2. If present, renders the child element with the accessed data as its ViewState
513
+ 3. If null/undefined, hides the child element
514
+
515
+ This is essential for recursive Jay-HTML structures where a node may have optional child nodes (like a tree or linked list).
516
+
517
+ ```typescript
518
+ declare function withData<ParentViewState, ChildViewState>(
519
+ accessor: (data: ParentViewState) => ChildViewState | null | undefined,
520
+ elem: () => BaseJayElement<ChildViewState>,
521
+ ): WithData<ParentViewState, ChildViewState>;
522
+ ```
523
+
524
+ at which
525
+
526
+ - `ParentViewState` - the type of the parent element's view state
527
+ - `ChildViewState` - the type of the child element's view state (can be the same as parent for recursive structures)
528
+ - `accessor` - a function that extracts child data from parent data, returns null/undefined if child should not render
529
+ - `elem` - a factory function that creates the child element (called only when accessor returns non-null data)
530
+
531
+ Example usage for a binary tree structure:
532
+
533
+ ```typescript
534
+ import {
535
+ element as e,
536
+ dynamicElement as de,
537
+ withData,
538
+ dynamicText as dt,
539
+ } from '@jay-framework/runtime';
540
+
541
+ interface TreeNode {
542
+ value: number;
543
+ left: TreeNode | null;
544
+ right: TreeNode | null;
545
+ }
546
+
547
+ function renderTree(): BaseJayElement<TreeNode> {
548
+ return e('div', { class: 'tree-node' }, [
549
+ e('div', { class: 'value' }, [dt((vs) => vs.value)]),
550
+ de('div', { class: 'children' }, [
551
+ withData(
552
+ (vs) => vs.left,
553
+ () => renderTree(),
554
+ ),
555
+ withData(
556
+ (vs) => vs.right,
557
+ () => renderTree(),
558
+ ),
559
+ ]),
560
+ ]);
561
+ }
562
+ ```
563
+
564
+ Example usage for a linked list:
565
+
566
+ ```typescript
567
+ interface ListNode {
568
+ value: string;
569
+ next: ListNode | null;
570
+ }
571
+
572
+ function renderList(): BaseJayElement<ListNode> {
573
+ return e('div', { class: 'list-item' }, [
574
+ e('span', {}, [dt((vs) => vs.value)]),
575
+ de('div', { class: 'next' }, [
576
+ withData(
577
+ (vs) => vs.next,
578
+ () => renderList(),
579
+ ),
580
+ ]),
581
+ ]);
582
+ }
583
+ ```
584
+
585
+ The generated Jay-HTML compiler automatically uses `withData` when encountering recursive regions with the `<recurse>` element and an `accessor` attribute.
586
+
339
587
  ## ConstructionContext
340
588
 
341
589
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/runtime",
3
- "version": "0.6.10",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -23,11 +23,11 @@
23
23
  "test:watch": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@jay-framework/list-compare": "^0.6.10",
27
- "@jay-framework/reactive": "^0.6.10"
26
+ "@jay-framework/list-compare": "^0.7.0",
27
+ "@jay-framework/reactive": "^0.7.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@jay-framework/dev-environment": "^0.6.10",
30
+ "@jay-framework/dev-environment": "^0.7.0",
31
31
  "@testing-library/jest-dom": "^6.2.0",
32
32
  "@types/jsdom": "^21.1.6",
33
33
  "@types/node": "^20.11.5",