lahama 2.5.1 → 3.0.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 (2) hide show
  1. package/dist/lahama.js +100 -447
  2. package/package.json +6 -1
package/dist/lahama.js CHANGED
@@ -1,163 +1,26 @@
1
+ import 'vitest/dist/chunks/reporters.d.OXEK7y4s.d.ts';
2
+
1
3
  function withoutNulls(arr) {
2
4
  return arr.filter((item) => item != null)
3
5
  }
4
- function arraysDiff(oldArray, newArray) {
5
- return {
6
- added : newArray.filter(
7
- (newItem) => !oldArray.includes(newItem)
8
- ),
9
- removed : oldArray.filter(
10
- (oldItem) => !newArray.includes(oldItem)
11
- )
12
- }
13
- }
14
- const ARRAY_DIFF_OP = {
15
- ADD : 'add',
16
- REMOVE : 'remove',
17
- MOVE : 'move',
18
- NOOP : 'noop'
19
- };
20
6
  const a = { };
21
7
  const b = { };
22
8
  console.log(a === b);
23
- class ArrayWithOriginalIndices {
24
- #array = []
25
- #originalIndices = []
26
- #equalsFn
27
- constructor(array, equalsFn) {
28
- this.#array = [...array];
29
- this.#originalIndices = array.map((_, i) => i);
30
- this.#equalsFn = equalsFn;
31
- }
32
- get length() {
33
- return this.#array.length
34
- }
35
- isRemoval(index, newArray) {
36
- if (index >= this.length) {
37
- return false
38
- }
39
- const item = this.#array[index];
40
- const indexInNewArray = newArray.findIndex((newItem) =>
41
- this.#equalsFn(item, newItem)
42
- );
43
- return indexInNewArray === -1
44
- }
45
- removeItem(index) {
46
- const operation = {
47
- op : ARRAY_DIFF_OP.REMOVE,
48
- index,
49
- item : this.#array[index],
50
- };
51
- this.#array.splice(index, 1);
52
- return operation
53
- }
54
- isNoop(index, newArray) {
55
- if (index >= this.length) {
56
- return false
57
- }
58
- const item = this.#array[index];
59
- const newItem = newArray[index];
60
- return this.#equalsFn(item, newItem)
61
- }
62
- originalIndexAt(index) {
63
- return this.#originalIndices[index]
64
- }
65
- noopItem(index) {
66
- return {
67
- op : ARRAY_DIFF_OP.NOOP,
68
- originalIndex : this.originalIndexAt(index),
69
- item : this.#array[index],
70
- }
71
- }
72
- isAddition(item, fromIdx) {
73
- return this.findIndexFrom(item, fromIdx) === -1
74
- }
75
- findIndexFrom(item, fromIndex) {
76
- for (let i = fromIndex; i < this.length; i++) {
77
- if (this.#equalsFn(item, this.#array[i])) {
78
- return i
79
- }
80
- }
81
- return -1
82
- }
83
- addItem(item, index) {
84
- const operation = {
85
- op : ARRAY_DIFF_OP.ADD,
86
- index,
87
- item
88
- };
89
- this.#array.splice(index, 0, item);
90
- this.#originalIndices.splice(index, 0, -1);
91
- return operation
92
- }
93
- moveItem(item, toIndex) {
94
- const fromIndex = this.findIndexFrom(item, toIndex);
95
- const operation = {
96
- op : ARRAY_DIFF_OP.MOVE,
97
- originalIndex : this.originalIndexAt(fromIndex),
98
- from : fromIndex,
99
- index : toIndex,
100
- item : this.#array[fromIndex]
101
- };
102
- const [_item] = this.#array.splice(fromIndex, 1);
103
- this.#array.splice(toIndex, 0 , _item);
104
- const [originalIndex] =
105
- this.#originalIndices.splice(fromIndex, 1);
106
- this.#originalIndices.splice(toIndex, 0, originalIndex);
107
- return operation
108
- }
109
- removeItemsAfter(index) {
110
- const operations = [];
111
- while (this.length > index) {
112
- operations.push(this.removeItem(index));
113
- }
114
- return operations
115
- }
116
- }
117
- function arraysDiffSequence(
118
- oldArray,
119
- newArray,
120
- equalsFn = (a, b) => a === b
121
- ) {
122
- const sequence = [];
123
- const array = new ArrayWithOriginalIndices(oldArray, equalsFn);
124
- for (let index = 0; index < newArray.length; index++) {
125
- //!REMOVE CASE
126
- if (array.isRemoval(index, newArray)) {
127
- sequence.push(array.removeItem(index));
128
- index--;
129
- continue
130
- }
131
- //!noop case
132
- if (array.isNoop(index, newArray)) {
133
- sequence.push(array.noopItem(index));
134
- continue
135
- }
136
- //!addition case
137
- const item = newArray[index];
138
- if (array.isAddition(item , index)) {
139
- sequence.push(array.addItem(item, index));
140
- continue
141
- }
142
- //!move case
143
- sequence.push(array.moveItem(item, index));
144
- }
145
- //!remove extra items
146
- sequence.push(...array.removeItemsAfter(newArray.length));
147
- return sequence
148
- }
149
9
 
150
10
  const DOM_TYPES = {
151
11
  TEXT : 'text',
152
12
  ELEMENT : 'element',
153
13
  FRAGMENT : 'fragment',
14
+ COMPONENT : 'component',
154
15
  };
155
16
  function h(tag, props = {} , children = []) {
17
+ const type =
18
+ typeof tag === 'string' ? DOM_TYPES.ELEMENT : DOM_TYPES.COMPONENT;
156
19
  return {
157
20
  tag,
158
21
  props,
22
+ type,
159
23
  children: mapTextNodes(withoutNulls(children)),
160
- type : DOM_TYPES.ELEMENT,
161
24
  }
162
25
  }
163
26
  function mapTextNodes(children) {
@@ -182,16 +45,26 @@ function addEventListener(
182
45
  ) {
183
46
  function boundHandler() {
184
47
  hostComponent
185
- ? handler.apply(hostComponent, arguments)
48
+ ? handler.apply(hostComponent, arguments)
186
49
  : handler(...arguments);
187
50
  }
188
51
  el.addEventListener(eventName, boundHandler);
189
- return boundHandler
52
+ return boundHandler;
190
53
  }
191
- function addEventListeners(listeners = {}, el) {
54
+ function addEventListeners(
55
+ listeners = {},
56
+ el,
57
+ hostComponent = null
58
+ ) {
192
59
  const addedListeners = {};
193
60
  Object.entries(listeners).forEach(([eventName, handler]) => {
194
- addedListeners[eventName] = addEventListener(eventName, handler, el);
61
+ const listener = addEventListener(
62
+ eventName,
63
+ handler,
64
+ el,
65
+ hostComponent
66
+ );
67
+ addedListeners[eventName] = listener;
195
68
  });
196
69
  return addedListeners
197
70
  }
@@ -201,45 +74,6 @@ function removeEventListeners(listeners = {}, el) {
201
74
  });
202
75
  }
203
76
 
204
- function destroyDom(vdom) {
205
- const { type } = vdom;
206
- switch (type) {
207
- case DOM_TYPES.TEXT : {
208
- removeTextNode(vdom);
209
- break
210
- }
211
- case DOM_TYPES.ELEMENT : {
212
- removeElementNode(vdom);
213
- break
214
- }
215
- case DOM_TYPES.FRAGMENT : {
216
- removeFragmentNode(vdom);
217
- break
218
- }
219
- default : {
220
- throw new Error(`Can't destroy DOM of type ${type}`)
221
- }
222
- }
223
- delete vdom.el;
224
- }
225
- function removeTextNode(vdom) {
226
- const { el } = vdom;
227
- el.remove();
228
- }
229
- function removeElementNode(vdom) {
230
- const { el , children, listeners } = vdom;
231
- el.remove();
232
- children.forEach(destroyDom);
233
- if (listeners) {
234
- removeEventListeners(listeners, el);
235
- delete vdom.listeners;
236
- }
237
- }
238
- function removeFragmentNode(vdom) {
239
- const { children } = vdom;
240
- children.forEach(destroyDom);
241
- }
242
-
243
77
  function setAttributes(el, attrs) {
244
78
  const { class : className, style, ...otherAttrs} = attrs;
245
79
  if (className) {
@@ -266,9 +100,6 @@ function setClass(el, className) {
266
100
  function setStyle(el, name, value) {
267
101
  el.style[name] = value;
268
102
  }
269
- function removeStyle(el, name) {
270
- el.style[name] = null;
271
- }
272
103
  function removeAttributeCustom(el, name) {
273
104
  el[name] = null;
274
105
  el.removeAttribute(name);
@@ -283,18 +114,33 @@ function setAttribute(el, name, value) {
283
114
  }
284
115
  }
285
116
 
286
- function mountDom(vdom, parentEl, index) {
117
+ function extractPropsAndEvents(vdom) {
118
+ const { on : events = {}, ...props } = vdom.props;
119
+ delete props.key;
120
+ return { props, events }
121
+ }
122
+
123
+ function mountDom(
124
+ vdom,
125
+ parentEl,
126
+ index,
127
+ hostComponent = null
128
+ ) {
287
129
  switch (vdom.type) {
288
130
  case DOM_TYPES.TEXT : {
289
- createTextNode(vdom, parentEl, index);
131
+ createTextNode(vdom, parentEl, index, );
290
132
  break
291
133
  }
292
134
  case DOM_TYPES.ELEMENT : {
293
- createElementNode(vdom, parentEl, index);
135
+ createElementNode(vdom, parentEl, index, hostComponent);
294
136
  break
295
137
  }
296
138
  case DOM_TYPES.FRAGMENT : {
297
- createFragmentNodes(vdom, parentEl, index);
139
+ createFragmentNodes(vdom, parentEl, index, hostComponent);
140
+ break
141
+ }
142
+ case DOM_TYPES.COMPONENT : {
143
+ createComponentNode(vdom, parentEl, index, hostComponent);
298
144
  break
299
145
  }
300
146
  default : {
@@ -324,296 +170,103 @@ function createTextNode(vdom, parentEl, index) {
324
170
  vdom.el = textNode;
325
171
  insert(textNode, parentEl, index);
326
172
  }
327
- function createElementNode(vdom, parentEl, index) {
328
- const { tag, props , children } = vdom;
173
+ function createElementNode(vdom, parentEl, index, hostComponent) {
174
+ const { tag, children } = vdom;
329
175
  const element = document.createElement(tag);
330
- addProps(element, props , vdom);
176
+ addProps(element, vdom, hostComponent);
331
177
  vdom.el = element;
332
178
  //!function mountDom(vnode, parentEl) {
333
- children.forEach((child) => mountDom(child, element));
179
+ children.forEach((child) => mountDom(child, element, null, hostComponent));
334
180
  insert(element, parentEl, index);
335
181
  }
336
- function addProps(el, props, vdom) {
337
- const { on : events, ...attrs } = props;
338
- vdom.listeners = addEventListeners(events, el);
182
+ function addProps(el, props, vdom, hostComponent) {
183
+ const { props : attrs, events } = extractPropsAndEvents(vdom);
184
+ vdom.listeners = addEventListeners(events, el, hostComponent);
339
185
  setAttributes(el, attrs);
340
186
  }
341
- function createFragmentNodes(vdom, parentEl, index) {
187
+ function createFragmentNodes(vdom, parentEl, index, hostComponent) {
342
188
  const { children } = vdom;
343
189
  vdom.el = parentEl;
344
- children.forEach((child, i) => mountDom(child, parentEl, index ? index + i : null));
190
+ children.forEach((child, i) => mountDom(child, parentEl, index ? index + i : null, hostComponent));
191
+ }
192
+ function createComponentNode(vdom, parentEl, index, hostComponent) {
193
+ const Component = vdom.tag;
194
+ const { props, events } = extractPropsAndEvents(vdom);
195
+ const component = new Component(props, events, hostComponent);
196
+ component.mount(parentEl, index);
197
+ vdom.component = component;
198
+ vdom.el = component.firstElement;
345
199
  }
346
200
 
347
- class Dispatcher {
348
- #subs = new Map()
349
- #afterHandlers = []
350
- subscribe(commandName, handler) {
351
- if (!this.#subs.has(commandName)) {
352
- this.#subs.set(commandName, []);
353
- }
354
- const handlersArray = this.#subs.get(commandName);
355
- if (handlersArray.includes(handler)) {
356
- return () => {
357
- }
358
- }
359
- handlersArray.push(handler);
360
- return () => {
361
- const idx = handlersArray.indexOf(handler);
362
- handlersArray.splice(idx, 1);
363
- }
364
- }
365
- afterEveryCommand(handler) {
366
- this.#afterHandlers.push(handler);
367
- return () => {
368
- const idx = this.#afterHandlers.indexOf(handler);
369
- this.#afterHandlers.splice(idx, 1);
201
+ function destroyDom(vdom) {
202
+ const { type } = vdom;
203
+ switch (type) {
204
+ case DOM_TYPES.TEXT : {
205
+ removeTextNode(vdom);
206
+ break
370
207
  }
371
- }
372
- dispatch(commandName, payload) {
373
- if (this.#subs.has(commandName)) {
374
- this.#subs.get(commandName).forEach((handler) => handler(payload));
375
- } else {
376
- console.warn(`No handlers for command : ${commandName}`);
208
+ case DOM_TYPES.ELEMENT : {
209
+ removeElementNode(vdom);
210
+ break
377
211
  }
378
- this.#afterHandlers.forEach((handler) => handler());
379
- }
380
- }
381
-
382
- function areNodesEqual(nodeOne, nodeTwo) {
383
- if (!nodeOne || !nodeTwo) {
384
- console.error('Invalid VDOM node', { nodeOne, nodeTwo });
385
- }
386
- if (!nodeOne.type || !nodeTwo.type) {
387
- console.error('Invalid VDOM node', { nodeOne, nodeTwo });
388
- }
389
- if (nodeOne.type !== nodeTwo.type) {
390
- return false
391
- }
392
- if (nodeOne.type === DOM_TYPES.ELEMENT) {
393
- const { tag : tagOne } = nodeOne;
394
- const { tag : tagTwo} = nodeTwo;
395
- return tagOne === tagTwo
396
- }
397
- return true
398
- }
399
-
400
- function objectsDiff(oldObj, newObj) {
401
- const oldKeys = Object.keys(oldObj);
402
- const newKeys = Object.keys(newObj);
403
- return {
404
- added : newKeys.filter((key) => !(key in oldObj)),
405
- removed : oldKeys.filter((key) => !(key in newObj)),
406
- updated : newKeys.filter(
407
- (key) => key in oldObj && oldObj[key] !== newObj[key]
408
- ),
409
- }
410
- }
411
-
412
- function isNotEmptyString(str) {
413
- return str !== ''
414
- }
415
- function isNotBlankOrEmptyString(str) {
416
- return isNotEmptyString(str.trim())
417
- }
418
-
419
- function patchDOM(oldVdom, newVdom, parentEl) {
420
- if (!areNodesEqual(oldVdom, newVdom)) {
421
- const index = findIndexInParent(parentEl, oldVdom.el);
422
- destroyDom(oldVdom);
423
- mountDom(newVdom, parentEl, index);
424
- return newVdom
425
- }
426
- newVdom.el = oldVdom.el;
427
- switch (newVdom.type) {
428
- case DOM_TYPES.TEXT: {
429
- patchText(oldVdom, newVdom);
430
- return newVdom
212
+ case DOM_TYPES.FRAGMENT : {
213
+ removeFragmentNode(vdom);
214
+ break
431
215
  }
432
- case DOM_TYPES.ELEMENT: {
433
- patchElement(oldVdom, newVdom);
216
+ case DOM_TYPES.COMPONENT : {
217
+ vdom.component.unmount();
434
218
  break
435
219
  }
220
+ default : {
221
+ throw new Error(`Can't destroy DOM of type ${type}`)
222
+ }
436
223
  }
437
- patchChildren(oldVdom, newVdom);
438
- return newVdom
439
- }
440
- function patchText(oldVdom, newVdom) {
441
- const el = oldVdom.el;
442
- const { value : oldText} = oldVdom;
443
- const { value : newText} = newVdom;
444
- if (oldText !== newText) {
445
- el.nodeValue = newText;
446
- }
447
- }
448
- function findIndexInParent(parentEl, el) {
449
- const index = Array.from(parentEl.childNodes).indexOf(el);
450
- if (index < 0) {
451
- return null
452
- }
453
- return index
454
- }
455
- function patchElement(oldVdom, newVdom) {
456
- const el = oldVdom.el;
457
- const {
458
- class : oldClass,
459
- style : oldStyle,
460
- on: oldEvents,
461
- ...oldAttrs
462
- } = oldVdom.props;
463
- const {
464
- class: newClass,
465
- style: newStyle,
466
- on: newEvents,
467
- ...newAttrs
468
- } = newVdom.props;
469
- const { listeners: oldListeners } = oldVdom;
470
- patchAttrs(el, oldAttrs, newAttrs);
471
- patchClasses(el, oldClass, newClass);
472
- patchStyles(el, oldStyle, newStyle);
473
- newVdom.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
474
- }
475
- function patchAttrs(el, oldAttrs, newAttrs) {
476
- const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
477
- for (const attr of removed) {
478
- removeAttributeCustom(el, attr);
479
- }
480
- for (const attr of added.concat(updated)) {
481
- setAttribute(el, attr, newAttrs[attr]);
482
- }
483
- }
484
- function patchClasses(el, oldClass, newClass) {
485
- const oldClasses = toClassList(oldClass);
486
- const newClasses = toClassList(newClass);
487
- const {added, removed} =
488
- arraysDiff(oldClasses, newClasses);
489
- if (removed.length > 0) {
490
- el.classList.remove(...removed);
491
- }
492
- if (added.length > 0) {
493
- el.classList.add(...added);
494
- }
495
- }
496
- function toClassList(classes = '') {
497
- return Array.isArray(classes)
498
- ? classes.filter(isNotBlankOrEmptyString)
499
- : classes.split(/(\s+)/)
500
- .filter(isNotBlankOrEmptyString)
501
- }
502
- function patchStyles(el, oldStyle = {}, newStyle = {}) {
503
- const {added, removed, updated } = objectsDiff(oldStyle, newStyle);
504
- for (const style of removed) {
505
- removeStyle(el, style);
506
- }
507
- for (const style of added.concat(updated)) {
508
- setStyle(el, style, newStyle[style]);
509
- }
224
+ delete vdom.el;
510
225
  }
511
- function patchEvents(
512
- el,
513
- oldListeners = {},
514
- oldEvents = {},
515
- newEvents = {},
516
- ) {
517
- const { removed, added, updated} =
518
- objectsDiff(oldEvents, newEvents);
519
- for (const eventName of removed.concat(updated)) {
520
- el.removeEventListener(eventName, oldListeners[eventName]);
521
- }
522
- const addedListeners = {};
523
- for (const eventName of added.concat(updated)) {
524
- const listener =
525
- addEventListener(eventName, newEvents[eventName], el);
526
- addedListeners[eventName] = listener;
527
- }
528
- return addedListeners
226
+ function removeTextNode(vdom) {
227
+ const { el } = vdom;
228
+ el.remove();
529
229
  }
530
- function extractChildren(vdom) {
531
- if (vdom.children == null) {
532
- return []
533
- }
534
- const children = [];
535
- for (const child of vdom.children) {
536
- if (child.type === DOM_TYPES.FRAGMENT) {
537
- children.push(...extractChildren(child));
538
- } else {
539
- children.push(child);
540
- }
230
+ function removeElementNode(vdom) {
231
+ const { el , children, listeners } = vdom;
232
+ el.remove();
233
+ children.forEach(destroyDom);
234
+ if (listeners) {
235
+ removeEventListeners(listeners, el);
236
+ delete vdom.listeners;
541
237
  }
542
- return children
543
238
  }
544
- function patchChildren(oldVdom, newVdom) {
545
- const oldChildren = extractChildren(oldVdom);
546
- const newChildren = extractChildren(newVdom);
547
- const parentEl = oldVdom.el;
548
- const diffSeq = arraysDiffSequence(oldChildren, newChildren, areNodesEqual);
549
- for (const operation of diffSeq) {
550
- const { originalIndex, index, item} = operation;
551
- switch (operation.op) {
552
- case ARRAY_DIFF_OP.ADD: {
553
- mountDom(item, parentEl, index);
554
- break
555
- }
556
- case ARRAY_DIFF_OP.REMOVE: {
557
- destroyDom(item);
558
- break
559
- }
560
- case ARRAY_DIFF_OP.MOVE: {
561
- const oldChild = oldChildren[originalIndex];
562
- const newChild = newChildren[index];
563
- const el = oldChild.el;
564
- const elAtTargetIndex = parentEl.childNodes[index];
565
- parentEl.insertBefore(el, elAtTargetIndex);
566
- patchDOM(oldChildren[originalIndex], newChild, parentEl);
567
- break
568
- }
569
- case ARRAY_DIFF_OP.NOOP : {
570
- patchDOM(oldChildren[originalIndex], newChildren[index], parentEl);
571
- break
572
- }
573
- }
574
- }
239
+ function removeFragmentNode(vdom) {
240
+ const { children } = vdom;
241
+ children.forEach(destroyDom);
575
242
  }
576
243
 
577
- function createApp({ state, view, reducers = {} }) {
244
+ function createApp(RootComponent, props = {}) {
578
245
  let parentEl = null;
579
- let vdom = null;
580
246
  let isMounted = false;
581
- const dispatcher = new Dispatcher();
582
- const subscriptions = [dispatcher.afterEveryCommand(renderApp)];
583
- function emit(eventName, payload) {
584
- dispatcher.dispatch(eventName, payload);
585
- }
586
- for (const actionName in reducers) {
587
- const reducer = reducers[actionName];
588
- const subs = dispatcher.subscribe(actionName, (payload) => {
589
- state = reducer(state, payload);
590
- });
591
- subscriptions.push(subs);
592
- }
593
- function renderApp() {
594
- const newVdom = view(state, emit);
595
- if (!newVdom) {
596
- console.error('View returned', newVdom);
597
- return;
598
- }
599
- vdom = patchDOM(vdom, newVdom, parentEl);
247
+ let vdom = null;
248
+ function reset() {
249
+ parentEl = null;
250
+ isMounted = false;
251
+ vdom = null;
600
252
  }
601
253
  return {
602
254
  mount(_parentEl) {
603
255
  if (isMounted) {
604
- throw new Error("The application is already mounted")
256
+ throw new Error(`The application is already mounted`)
605
257
  }
606
258
  parentEl = _parentEl;
607
- vdom = view(state, emit);
259
+ vdom = h(RootComponent, props);
608
260
  mountDom(vdom, parentEl);
609
261
  isMounted = true;
610
262
  },
611
263
  unmount() {
264
+ if (!isMounted) {
265
+ throw new Error(`The application is not mounted`)
266
+ }
612
267
  destroyDom(vdom);
613
- vdom = null;
614
- subscriptions.forEach((unsubscribe) => unsubscribe());
615
- isMounted = false;
616
- },
268
+ reset();
269
+ }
617
270
  }
618
271
  }
619
272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lahama",
3
- "version": "2.5.1",
3
+ "version": "3.0.0",
4
4
  "description": "",
5
5
  "main": "dist/lahama.js",
6
6
  "files": [
@@ -20,6 +20,8 @@
20
20
  "type": "module",
21
21
  "devDependencies": {
22
22
  "@eslint/js": "^9.39.0",
23
+ "@rollup/plugin-commonjs": "^29.0.0",
24
+ "@rollup/plugin-node-resolve": "^16.0.3",
23
25
  "globals": "^16.5.0",
24
26
  "jsdom": "^27.2.0",
25
27
  "rimraf": "^3.0.2",
@@ -27,5 +29,8 @@
27
29
  "rollup-plugin-cleanup": "^3.2.1",
28
30
  "rollup-plugin-filesize": "^10.0.0",
29
31
  "vitest": "^4.0.15"
32
+ },
33
+ "dependencies": {
34
+ "fast-deep-equal": "^3.1.3"
30
35
  }
31
36
  }