lilact 0.16.3 → 0.18.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.
Files changed (70) hide show
  1. package/dist/lilact.development.js +227 -151
  2. package/dist/lilact.development.js.map +3 -3
  3. package/dist/lilact.development.min.js +20 -20
  4. package/dist/lilact.development.min.js.map +3 -3
  5. package/dist/lilact.production.min.js +20 -20
  6. package/docs/assets/hierarchy.js +1 -1
  7. package/docs/assets/navigation.js +1 -1
  8. package/docs/assets/search.js +1 -1
  9. package/docs/classes/accessories.ErrorBoundary.html +8 -8
  10. package/docs/classes/accessories.Suspense.html +7 -7
  11. package/docs/classes/components.Component.html +11 -11
  12. package/docs/classes/components.HTMLComponent.html +11 -11
  13. package/docs/classes/components.RootComponent.html +11 -11
  14. package/docs/functions/components.createComponent.html +1 -1
  15. package/docs/functions/components.createRoot.html +1 -1
  16. package/docs/functions/components.memo.html +7 -0
  17. package/docs/functions/components.render.html +1 -1
  18. package/docs/functions/hooks.startTransition.html +6 -0
  19. package/docs/functions/hooks.useActionState.html +1 -1
  20. package/docs/functions/hooks.useDebugValue.html +5 -0
  21. package/docs/functions/hooks.useDeferredValue.html +1 -1
  22. package/docs/functions/hooks.useEffect.html +5 -4
  23. package/docs/functions/hooks.useImperativeHandle.html +1 -1
  24. package/docs/functions/hooks.useInsertionEffect.html +6 -0
  25. package/docs/functions/hooks.useLayoutEffect.html +3 -2
  26. package/docs/functions/hooks.useMemo.html +3 -2
  27. package/docs/functions/hooks.useReducer.html +1 -1
  28. package/docs/functions/misc.Fragment.html +2 -2
  29. package/docs/functions/misc.deepEqual.html +1 -1
  30. package/docs/functions/misc.findDOMNode.html +1 -1
  31. package/docs/functions/misc.forwardRef.html +1 -1
  32. package/docs/functions/misc.getComponentByPointer.html +1 -1
  33. package/docs/functions/misc.isAsync.html +1 -1
  34. package/docs/functions/misc.isClass.html +1 -1
  35. package/docs/functions/misc.isEmpty.html +1 -1
  36. package/docs/functions/misc.isError.html +1 -1
  37. package/docs/functions/misc.isThenable.html +1 -1
  38. package/docs/functions/{misc.isValidElement.html → misc.isValidComponent.html} +1 -1
  39. package/docs/functions/misc.shallowEqual.html +3 -2
  40. package/docs/functions/misc.toBool.html +1 -1
  41. package/docs/functions/timers.timeoutPromise.html +10 -7
  42. package/docs/hierarchy.html +1 -1
  43. package/docs/modules/components.html +1 -1
  44. package/docs/modules/hooks.html +1 -1
  45. package/docs/modules/misc.html +1 -1
  46. package/docs/static/demos/actionstate.jsx +2 -2
  47. package/docs/static/demos/memo.jsx +26 -0
  48. package/docs/static/demos/priorities.jsx +68 -0
  49. package/docs/static/index.html +3 -1
  50. package/docs/static/lilact.development.js +227 -151
  51. package/docs/static/lilact.development.js.map +3 -3
  52. package/docs/static/lilact.development.min.js +20 -20
  53. package/docs/static/lilact.development.min.js.map +3 -3
  54. package/docs/static/lilact.production.min.js +20 -20
  55. package/docs/variables/misc.Children.html +2 -2
  56. package/docs/variables/misc.isValidElement.html +7 -0
  57. package/examples/demos/actionstate.jsx +2 -2
  58. package/examples/demos/memo.jsx +26 -0
  59. package/examples/demos/priorities.jsx +68 -0
  60. package/examples/index.html +3 -1
  61. package/examples/lilact.development.js +227 -151
  62. package/examples/lilact.development.js.map +3 -3
  63. package/examples/lilact.development.min.js +20 -20
  64. package/examples/lilact.development.min.js.map +3 -3
  65. package/examples/lilact.production.min.js +20 -20
  66. package/package.json +1 -1
  67. package/src/components.jsx +126 -40
  68. package/src/hooks.jsx +84 -11
  69. package/src/lilact.jsx +1 -1
  70. package/src/misc.jsx +13 -8
@@ -94,7 +94,12 @@ class ComponentCache
94
94
  {
95
95
  this.current_map.forEach( (arr)=>{
96
96
  arr.slice(arr[IDX]).forEach((ex)=>{
97
- if(ex.cleanup) ex.cleanup();
97
+ if(ex.cleanup) {
98
+ ex.cleanup();
99
+ }
100
+ else if(ex.element) {
101
+ ex.element.parentElement.removeChild(ex.element);
102
+ }
98
103
  });
99
104
  });
100
105
 
@@ -157,19 +162,23 @@ class ComponentCore
157
162
  }
158
163
 
159
164
  /*
160
- //let do_rerender = true;
161
-
162
- // if(this.outlet && this.entity[MEMOIZED]) {
163
- // if(shallowEqual(this.props, next_props)) do_rerender=false;
164
- // delete this.entity[MEMOIZED];
165
- // }
166
-
167
- if(do_rerender) {
168
165
  */
169
166
  // TODO: should componentDidUpdate be called after arranging/appending the outlet or before?
170
167
  apply(next_props = this.props, next_state = this.next_state || this.state)
171
168
  {
172
169
 
170
+ let do_rerender = true;
171
+
172
+ if(this.outlet && this?.[MEMOIZED]) {
173
+
174
+ if( shallowEqual(this.props, next_props, "children") &&
175
+ shallowEqual(this.props?.children, next_props?.children) ) {
176
+ do_rerender=false;
177
+ }
178
+ }
179
+
180
+ if(do_rerender) {
181
+
173
182
  if(DEBUG) {
174
183
 
175
184
  if(this.entity?.propTypes) {
@@ -303,13 +312,14 @@ if(DEBUG) {
303
312
  if(this.cache) this.cache.commit();
304
313
 
305
314
 
306
- if(this.element) this.arrangeOutlet();
315
+ if(this.element) this.arrangeOutlet();
307
316
 
308
- if(this.component.componentDidUpdate) {
309
- this.component.componentDidUpdate(prev_props, prev_state, this.last_snapshot);
310
- }
317
+ if(this.component.componentDidUpdate) {
318
+ this.component.componentDidUpdate(prev_props, prev_state, this.last_snapshot);
319
+ }
311
320
 
312
- if(this.last_snapshot) delete this.last_snapshot;
321
+ if(this.last_snapshot) delete this.last_snapshot;
322
+ }
313
323
 
314
324
  }
315
325
 
@@ -630,17 +640,25 @@ function constructFunc(core, parent) // returns {text} or component, and not com
630
640
  // do nothing...
631
641
  }
632
642
  else {
633
- if(typeof(core.entity)==='string') {
634
- comp = new HTMLComponent(core.entity, core.props);
643
+ let entity = core.entity;
644
+ let memoized = false;
645
+
646
+ if(typeof(entity)==='object' && entity[MEMOIZED]) {
647
+ memoized = true;
648
+ entity = entity[MEMOIZED];
649
+ }
650
+
651
+ if(typeof(entity)==='string') {
652
+ comp = new HTMLComponent(entity, core.props);
635
653
  }
636
654
  else {
637
655
 
638
- if( isClass(core.entity) ) {
639
- if(core.entity?.defaultProps) {
640
- core.props = { ...core.entity.defaultProps, ...core.props };
656
+ if( isClass(entity) ) {
657
+ if(entity?.defaultProps) {
658
+ core.props = { ...entity.defaultProps, ...core.props };
641
659
  }
642
660
 
643
- comp = new core.entity(core.props);
661
+ comp = new entity(core.props);
644
662
 
645
663
  const desc = Object.getOwnPropertyDescriptor(comp, "state");
646
664
  if(desc) {
@@ -662,35 +680,35 @@ function constructFunc(core, parent) // returns {text} or component, and not com
662
680
  }
663
681
  }
664
682
  }
665
- else if(typeof(core.entity)==='function') {
683
+ else if(typeof(entity)==='function') {
666
684
 
667
- if(core.entity?.defaultProps) {
668
- core.props = { ...core.entity.defaultProps, ...core.props };
685
+ if(entity?.defaultProps) {
686
+ core.props = { ...entity.defaultProps, ...core.props };
669
687
  }
670
688
 
671
689
  comp = new Component(core.props);
672
690
 
673
691
  // the binding is not necessary and is not according to the specs,
674
692
  // probably not even recommended! but helpful.
675
- comp.render = core.entity.bind(comp);
693
+ comp.render = entity.bind(comp);
676
694
  comp[CORE].hooks = [];
677
695
  comp[CORE].hook_index = 0;
678
696
  }
679
697
  else {
680
- throw new Error("Invalid entity for createComponent.");
698
+ throw new Error("Error in constructing component.");
681
699
  }
682
700
 
683
- comp[CORE].entity = core.entity;
701
+ comp[CORE].entity = entity;
684
702
 
685
703
  if(core.container) {
686
704
  comp[CORE].container = core.container;
687
705
  }
688
706
  }
689
707
 
708
+ if(memoized) comp[CORE][MEMOIZED] = true;
690
709
  }
691
710
 
692
711
  if(parent instanceof ComponentCore) comp[CORE].parent = parent;
693
-
694
712
  return comp;
695
713
  }
696
714
 
@@ -716,23 +734,58 @@ function prepareCore(parent, core)
716
734
 
717
735
  function doUpdates()
718
736
  {
719
- requestAnimationFrame(()=>{
720
- let _layout_effects = Lilact.layout_effects;
721
- let _update_cbs = Lilact.update_cbs;
722
- let _update_set = Lilact.update_set;
737
+ /*
738
+ Priority:
723
739
 
724
- Lilact.layout_effects = new Set;
725
- Lilact.update_cbs = new Set;
726
- Lilact.update_set = new Set;
740
+ - Render + enqueue DOM work (runs in the same task where you schedule the update).
741
+ - Apply DOM updates (do this synchronously before you schedule any effects that must happen before paint).
742
+ - Insertion effects (run immediately after DOM is in place, still before paint).
743
+ - Layout effects (run immediately after insertion effects, still before paint).
744
+ - Passive effects (useEffect)
727
745
 
728
- for(const le of _layout_effects) le();
746
+ schedule them for after paint using requestAnimationFrame, typically:
747
+ run the “after paint” work in the next frame or in a callback scheduled such that it runs after the browser has performed the paint.
748
+
749
+
750
+ Current task: render → DOM updates → insertion effects → layout effects
751
+ Next paint timing boundary: requestAnimationFrame callback → passive effects (useEffect)
752
+ */
753
+
754
+
755
+ clearTimeout(Lilact.effect_timeout);
756
+
757
+ const _update_set = Lilact.update_set;
758
+ const _update_cbs = Lilact.update_cbs;
759
+ Lilact.update_set = new Set;
760
+ Lilact.update_cbs = new Set;
761
+
762
+ for(const u of _update_set) u.apply();
763
+ for(const cb of _update_cbs) cb();
764
+ processEffects();
729
765
 
730
- for(const u of _update_set) u.apply();
731
- for(const cb of _update_cbs) cb();
732
- });
733
766
  }
734
767
 
735
768
 
769
+ /** @ignore */
770
+ export function processEffects()
771
+ {
772
+ const _insertion_effects = Lilact.insertion_effects;
773
+ const _layout_effects = Lilact.layout_effects;
774
+ const _passive_effects = Lilact.passive_effects;
775
+
776
+ Lilact.insertion_effects = new Set;
777
+ Lilact.layout_effects = new Set;
778
+ Lilact.passive_effects = new Set;
779
+
780
+ for(const ie of _insertion_effects) ie();
781
+ for(const le of _layout_effects) le();
782
+
783
+ requestAnimationFrame(()=>{
784
+ for(const pe of _passive_effects) pe();
785
+ });
786
+
787
+ }
788
+
736
789
  function decode(html)
737
790
  {
738
791
  decode.parser ??= new DOMParser;
@@ -1049,8 +1102,12 @@ export class RootComponent extends HTMLComponent
1049
1102
 
1050
1103
  export function createComponent(entity, props={}, ...children)
1051
1104
  {
1052
- if(entity!==undefined && typeof(entity)!=='string' && typeof(entity)!=='function') {
1053
- throw new Error("Invalid entity for createComponent.");
1105
+
1106
+ if (typeof(entity)!=='string' && typeof(entity)!=='function' )
1107
+ {
1108
+ if(typeof(entity)!=='object' || !entity[MEMOIZED]) {
1109
+ throw new Error("Invalid entity for createComponent.");
1110
+ }
1054
1111
  }
1055
1112
 
1056
1113
  for(let i=0; i<children.length; i++) {
@@ -1141,6 +1198,21 @@ export function render(component, element)
1141
1198
  return createRoot(element).render(component);
1142
1199
  }
1143
1200
 
1201
+ /**
1202
+ * Memoizes the given component. The memoized component will only rerender if its props or children are modified.
1203
+ * Unlike React, Lilact memo also works on class components.
1204
+ *
1205
+ * @param {Object} component - Component instance to render.
1206
+ *
1207
+ * @returns {component} - Memoized component.
1208
+ */
1209
+
1210
+ export function memo(component)
1211
+ {
1212
+ return { [MEMOIZED]: component }
1213
+ }
1214
+
1215
+
1144
1216
  /** @ignore */
1145
1217
  export const createElement = createComponent;
1146
1218
 
@@ -1155,6 +1227,18 @@ export let update_cbs = new Set;
1155
1227
  export let roots = new Set;
1156
1228
  /** @ignore */
1157
1229
  export let layout_effects = new Set;
1230
+ /** @ignore */
1231
+ export let insertion_effects = new Set;
1232
+ /** @ignore */
1233
+ export let passive_effects = new Set;
1234
+
1235
+
1236
+ /** @ignore */
1237
+ export let update_timeout = undefined;
1238
+ /** @ignore */
1239
+ export let effect_timeout = undefined;
1240
+ /** @ignore */
1241
+ export let update_interval_margin = 0;
1158
1242
 
1159
1243
  /** @ignore */
1160
1244
  export const special_attributes = new Set([
@@ -1201,3 +1285,5 @@ export const length_css_attributes_set = new Set([
1201
1285
  export const boolean_html_attributes_set = new
1202
1286
  Set(["disabled", "readOnly", "required", "checked", "multiple",
1203
1287
  "hidden","open","loop","muted","controls","playsInline","allowFullScreen"]);
1288
+
1289
+
package/src/hooks.jsx CHANGED
@@ -182,12 +182,12 @@ export function useTransition()
182
182
 
183
183
  (async function(core, hk, fn) {
184
184
 
185
- hk.count++;
186
-
187
- if(hk.count===1) {
185
+ if(hk.count===0) {
188
186
  core.component.forceUpdate();
189
187
  }
190
188
 
189
+ hk.count++;
190
+
191
191
  await fn();
192
192
 
193
193
  hk.count--;
@@ -269,13 +269,14 @@ export function useRef(initialValue = null)
269
269
  * Runs an effect synchronously after all DOM mutations but before the browser paints.
270
270
  *
271
271
  * @param {Function} effect - Effect callback.
272
- * @param {Array<any>} [deps] - Dependency list.
272
+ * @param {Array<any>} [deps] Optional dependency list (or an object for shallow comparison)
273
+ * used to determine when to re-run the effect.
273
274
  * @returns {void}
274
275
  */
275
276
  export function useLayoutEffect(effect, deps=undefined)
276
277
  {
277
278
  if( deps!==undefined && (typeof(deps)!=='object' || deps.constructor.name!=='Array') ) {
278
- throw new Error("Layout effect dependencies must be an array or omitted.");
279
+ throw new Error("Layout effect dependencies must be an array, object or omitted.");
279
280
  }
280
281
 
281
282
  const hk = useHook();
@@ -290,20 +291,55 @@ export function useLayoutEffect(effect, deps=undefined)
290
291
 
291
292
  hk.deps = deps;
292
293
  Lilact.layout_effects.add( ()=>{ hk.cleanup = effect(); });
293
- Lilact.current_component[0].component.forceUpdate();
294
+
295
+ Lilact.clearTimeout( Lilact.effect_timeout );
296
+ Lilact.setTimeout( Lilact.processEffects, 0 );
294
297
  }
295
298
 
296
299
  /**
297
300
  * Runs a side effect after render commits.
298
301
  *
299
- * @param {Function} effect - Effect callback.
300
- * @param {Array<any>} [deps] - Dependency list.
302
+ * @param effect - Callback invoked.
303
+ * @param {Array<any>} [deps] Optional dependency list (or an object for shallow comparison)
304
+ * used to determine when to re-run the effect.
301
305
  * @returns {void}
302
306
  */
303
307
  export function useEffect(effect, deps=undefined)
304
308
  {
305
309
  if( deps!==undefined && (typeof(deps)!=='object' || deps.constructor.name!=='Array') ) {
306
- throw new Error("Effect dependencies must be an array or omitted.");
310
+ throw new Error("Effect dependencies must be an array, object or omitted.");
311
+ }
312
+
313
+ const hk = useHook();
314
+
315
+ if( !isEmpty(hk) ) {
316
+ if(deps!==undefined && hk?.deps!==undefined && shallowEqual(deps, hk.deps)) return;
317
+ }
318
+
319
+ if(hk?.cleanup) {
320
+ hk.cleanup();
321
+ }
322
+
323
+ hk.deps = deps;
324
+ Lilact.passive_effects.add( ()=>{ hk.cleanup = effect(); });
325
+
326
+ Lilact.clearTimeout( Lilact.effect_timeout );
327
+ Lilact.setTimeout( Lilact.processEffects, 0 );
328
+ }
329
+
330
+
331
+ /**
332
+ * Executes a side effect after the DOM updates have been committed.
333
+ *
334
+ * @param effect - Callback invoked after render commit.
335
+ * @param {Array<any>} [deps] Optional dependency list (or an object for shallow comparison)
336
+ * used to determine when to re-run the effect.
337
+ * @returns void
338
+ */
339
+ export function useInsertionEffect(effect, deps=undefined)
340
+ {
341
+ if( deps!==undefined && (typeof(deps)!=='object' || deps.constructor.name!=='Array') ) {
342
+ throw new Error("Insertion effect dependencies must be an array, object, or omitted.");
307
343
  }
308
344
 
309
345
  const hk = useHook();
@@ -317,15 +353,19 @@ export function useEffect(effect, deps=undefined)
317
353
  }
318
354
 
319
355
  hk.deps = deps;
320
- Lilact.setTimeout( ()=>{ hk.cleanup = effect(); }, 0 );
356
+ Lilact.insertion_effects.add( ()=>{ hk.cleanup = effect(); });
321
357
 
358
+ Lilact.clearTimeout( Lilact.effect_timeout );
359
+ Lilact.setTimeout( Lilact.processEffects, 0 );
322
360
  }
323
361
 
362
+
324
363
  /**
325
364
  * Memoizes a computed value until dependencies change.
326
365
  *
327
366
  * @param {Function} factory - Function that creates the value.
328
- * @param {Array<any>} deps - Dependency list.
367
+ * @param {Array<any>} [deps] Optional dependency list (or an object for shallow comparison)
368
+ * used to determine when to recompute the value.
329
369
  * @returns {any} Memoized value.
330
370
  */
331
371
  export function useMemo(factory,deps=undefined)
@@ -487,3 +527,36 @@ export function useImperativeHandle(ref, factory, deps=undefined)
487
527
 
488
528
  }
489
529
 
530
+
531
+ /**
532
+ * A hook for debug purposes. By default it only logs the output to console, but it can be overrided
533
+ * for the development environment.
534
+ *
535
+ * @param {any} val
536
+ * The debug value.
537
+ * @param {function(): any} formatter
538
+ * Function that creates a representation of the value.
539
+ * @returns {void}
540
+ */
541
+ export function useDebugValue(val, formatter=(x)=>x)
542
+ {
543
+ if(DEBUG) {
544
+ console.log(formatter(val));
545
+ }
546
+ }
547
+
548
+
549
+ /**
550
+ * Schedule a state update as a low-priority transition.
551
+ *
552
+ * Updates triggered inside the transition callback are treated as lower priority than
553
+ * updates outside of it.
554
+ *
555
+ * @param transition - Function that performs the state updates for the transition.
556
+ * @returns void
557
+ */
558
+ export function startTransition(transition)
559
+ {
560
+ transition();
561
+ }
562
+
package/src/lilact.jsx CHANGED
@@ -92,7 +92,7 @@ export {transpileJSX, transpilerConfig} from "./jsx";
92
92
  export const Lilact =
93
93
  {
94
94
 
95
- VERSION: "beta.16",
95
+ VERSION: "beta.18",
96
96
 
97
97
  // Configuration
98
98
 
package/src/misc.jsx CHANGED
@@ -54,10 +54,18 @@ const typeOf = (input) => {
54
54
  * @param value - Value to inspect.
55
55
  * @returns True if the value is a class component; otherwise false.
56
56
  */
57
- export const isValidElement = (value) => {
57
+ export const isValidComponent = (value) => {
58
58
  return value[CORE]!==undefined || value[TEXT]!==undefined;
59
59
  }
60
60
 
61
+ /**
62
+ * Checks whether a value is a Lilact component. It is the same as `isValidComponent`.
63
+ *
64
+ * @param value - Value to inspect.
65
+ * @returns True if the value is a class component; otherwise false.
66
+ */
67
+ export const isValidElement = isValidComponent
68
+
61
69
  /**
62
70
  * Utility to find the underlying DOM node for a mounted Lilact component.
63
71
  *
@@ -212,9 +220,10 @@ export function isEmpty(value) {
212
220
  *
213
221
  * @param source - First object to compare.
214
222
  * @param target - Second object to compare.
223
+ * @param ignore - specific property/index to be ignored in comparison.
215
224
  * @returns True if shallowly equal; otherwise false.
216
225
  */
217
- export const shallowEqual = (source, target) => {
226
+ export const shallowEqual = (source, target, ignore) => {
218
227
  if (typeOf(source) !== typeOf(target)) {
219
228
  return false;
220
229
  }
@@ -223,9 +232,9 @@ export const shallowEqual = (source, target) => {
223
232
  if (source.length !== target.length) {
224
233
  return false;
225
234
  }
226
- return source.every((el, index) => el === target[index]);
235
+ return source.every((el, index) => el === target[index] || index===ignore );
227
236
  } else if (typeOf(source) === "object") {
228
- return Object.keys(source).every((key) => source[key] === target[key]);
237
+ return Object.keys(source).every((key) => source[key] === target[key] || key===ignore );
229
238
  } else if (typeOf(source) === "date") {
230
239
  return source.getTime() === target.getTime();
231
240
  }
@@ -337,10 +346,6 @@ export function toBool(value) {
337
346
 
338
347
  // Internals
339
348
 
340
- /** @ignore */
341
- export let update_timeout = undefined;
342
- /** @ignore */
343
- export let update_interval_margin = 0;
344
349
  /** @ignore */
345
350
  export let id_num = Math.floor(Math.random()*10000);
346
351
  /** @ignore */