lahama 2.5.2 → 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 +107 -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) {
@@ -177,15 +40,31 @@ function hFragment(vNodes) {
177
40
  function addEventListener(
178
41
  eventName,
179
42
  handler,
180
- el
43
+ el,
44
+ hostComponent = null
181
45
  ) {
182
- el.addEventListener(eventName, handler);
183
- return handler
46
+ function boundHandler() {
47
+ hostComponent
48
+ ? handler.apply(hostComponent, arguments)
49
+ : handler(...arguments);
50
+ }
51
+ el.addEventListener(eventName, boundHandler);
52
+ return boundHandler;
184
53
  }
185
- function addEventListeners(listeners = {}, el) {
54
+ function addEventListeners(
55
+ listeners = {},
56
+ el,
57
+ hostComponent = null
58
+ ) {
186
59
  const addedListeners = {};
187
60
  Object.entries(listeners).forEach(([eventName, handler]) => {
188
- addedListeners[eventName] = addEventListener(eventName, handler, el);
61
+ const listener = addEventListener(
62
+ eventName,
63
+ handler,
64
+ el,
65
+ hostComponent
66
+ );
67
+ addedListeners[eventName] = listener;
189
68
  });
190
69
  return addedListeners
191
70
  }
@@ -195,45 +74,6 @@ function removeEventListeners(listeners = {}, el) {
195
74
  });
196
75
  }
197
76
 
198
- function destroyDom(vdom) {
199
- const { type } = vdom;
200
- switch (type) {
201
- case DOM_TYPES.TEXT : {
202
- removeTextNode(vdom);
203
- break
204
- }
205
- case DOM_TYPES.ELEMENT : {
206
- removeElementNode(vdom);
207
- break
208
- }
209
- case DOM_TYPES.FRAGMENT : {
210
- removeFragmentNode(vdom);
211
- break
212
- }
213
- default : {
214
- throw new Error(`Can't destroy DOM of type ${type}`)
215
- }
216
- }
217
- delete vdom.el;
218
- }
219
- function removeTextNode(vdom) {
220
- const { el } = vdom;
221
- el.remove();
222
- }
223
- function removeElementNode(vdom) {
224
- const { el , children, listeners } = vdom;
225
- el.remove();
226
- children.forEach(destroyDom);
227
- if (listeners) {
228
- removeEventListeners(listeners, el);
229
- delete vdom.listeners;
230
- }
231
- }
232
- function removeFragmentNode(vdom) {
233
- const { children } = vdom;
234
- children.forEach(destroyDom);
235
- }
236
-
237
77
  function setAttributes(el, attrs) {
238
78
  const { class : className, style, ...otherAttrs} = attrs;
239
79
  if (className) {
@@ -260,9 +100,6 @@ function setClass(el, className) {
260
100
  function setStyle(el, name, value) {
261
101
  el.style[name] = value;
262
102
  }
263
- function removeStyle(el, name) {
264
- el.style[name] = null;
265
- }
266
103
  function removeAttributeCustom(el, name) {
267
104
  el[name] = null;
268
105
  el.removeAttribute(name);
@@ -277,18 +114,33 @@ function setAttribute(el, name, value) {
277
114
  }
278
115
  }
279
116
 
280
- 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
+ ) {
281
129
  switch (vdom.type) {
282
130
  case DOM_TYPES.TEXT : {
283
- createTextNode(vdom, parentEl, index);
131
+ createTextNode(vdom, parentEl, index, );
284
132
  break
285
133
  }
286
134
  case DOM_TYPES.ELEMENT : {
287
- createElementNode(vdom, parentEl, index);
135
+ createElementNode(vdom, parentEl, index, hostComponent);
288
136
  break
289
137
  }
290
138
  case DOM_TYPES.FRAGMENT : {
291
- createFragmentNodes(vdom, parentEl, index);
139
+ createFragmentNodes(vdom, parentEl, index, hostComponent);
140
+ break
141
+ }
142
+ case DOM_TYPES.COMPONENT : {
143
+ createComponentNode(vdom, parentEl, index, hostComponent);
292
144
  break
293
145
  }
294
146
  default : {
@@ -318,295 +170,103 @@ function createTextNode(vdom, parentEl, index) {
318
170
  vdom.el = textNode;
319
171
  insert(textNode, parentEl, index);
320
172
  }
321
- function createElementNode(vdom, parentEl, index) {
322
- const { tag, props , children } = vdom;
173
+ function createElementNode(vdom, parentEl, index, hostComponent) {
174
+ const { tag, children } = vdom;
323
175
  const element = document.createElement(tag);
324
- addProps(element, props , vdom);
176
+ addProps(element, vdom, hostComponent);
325
177
  vdom.el = element;
326
178
  //!function mountDom(vnode, parentEl) {
327
- children.forEach((child) => mountDom(child, element));
179
+ children.forEach((child) => mountDom(child, element, null, hostComponent));
328
180
  insert(element, parentEl, index);
329
181
  }
330
- function addProps(el, props, vdom) {
331
- const { on : events, ...attrs } = props;
332
- 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);
333
185
  setAttributes(el, attrs);
334
186
  }
335
- function createFragmentNodes(vdom, parentEl, index) {
187
+ function createFragmentNodes(vdom, parentEl, index, hostComponent) {
336
188
  const { children } = vdom;
337
189
  vdom.el = parentEl;
338
- 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;
339
199
  }
340
200
 
341
- class Dispatcher {
342
- #subs = new Map()
343
- #afterHandlers = []
344
- subscribe(commandName, handler) {
345
- if (!this.#subs.has(commandName)) {
346
- this.#subs.set(commandName, []);
347
- }
348
- const handlersArray = this.#subs.get(commandName);
349
- if (handlersArray.includes(handler)) {
350
- return () => {
351
- }
352
- }
353
- handlersArray.push(handler);
354
- return () => {
355
- const idx = handlersArray.indexOf(handler);
356
- handlersArray.splice(idx, 1);
357
- }
358
- }
359
- afterEveryCommand(handler) {
360
- this.#afterHandlers.push(handler);
361
- return () => {
362
- const idx = this.#afterHandlers.indexOf(handler);
363
- 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
364
207
  }
365
- }
366
- dispatch(commandName, payload) {
367
- if (this.#subs.has(commandName)) {
368
- this.#subs.get(commandName).forEach((handler) => handler(payload));
369
- } else {
370
- console.warn(`No handlers for command : ${commandName}`);
208
+ case DOM_TYPES.ELEMENT : {
209
+ removeElementNode(vdom);
210
+ break
371
211
  }
372
- this.#afterHandlers.forEach((handler) => handler());
373
- }
374
- }
375
-
376
- function areNodesEqual(nodeOne, nodeTwo) {
377
- if (!nodeOne || !nodeTwo) {
378
- console.error('Invalid VDOM node', { nodeOne, nodeTwo });
379
- }
380
- if (!nodeOne.type || !nodeTwo.type) {
381
- console.error('Invalid VDOM node', { nodeOne, nodeTwo });
382
- }
383
- if (nodeOne.type !== nodeTwo.type) {
384
- return false
385
- }
386
- if (nodeOne.type === DOM_TYPES.ELEMENT) {
387
- const { tag : tagOne } = nodeOne;
388
- const { tag : tagTwo} = nodeTwo;
389
- return tagOne === tagTwo
390
- }
391
- return true
392
- }
393
-
394
- function objectsDiff(oldObj, newObj) {
395
- const oldKeys = Object.keys(oldObj);
396
- const newKeys = Object.keys(newObj);
397
- return {
398
- added : newKeys.filter((key) => !(key in oldObj)),
399
- removed : oldKeys.filter((key) => !(key in newObj)),
400
- updated : newKeys.filter(
401
- (key) => key in oldObj && oldObj[key] !== newObj[key]
402
- ),
403
- }
404
- }
405
-
406
- function isNotEmptyString(str) {
407
- return str !== ''
408
- }
409
- function isNotBlankOrEmptyString(str) {
410
- return isNotEmptyString(str.trim())
411
- }
412
-
413
- function patchDOM(oldVdom, newVdom, parentEl) {
414
- if (!areNodesEqual(oldVdom, newVdom)) {
415
- const index = findIndexInParent(parentEl, oldVdom.el);
416
- destroyDom(oldVdom);
417
- mountDom(newVdom, parentEl, index);
418
- return newVdom
419
- }
420
- newVdom.el = oldVdom.el;
421
- switch (newVdom.type) {
422
- case DOM_TYPES.TEXT: {
423
- patchText(oldVdom, newVdom);
424
- return newVdom
212
+ case DOM_TYPES.FRAGMENT : {
213
+ removeFragmentNode(vdom);
214
+ break
425
215
  }
426
- case DOM_TYPES.ELEMENT: {
427
- patchElement(oldVdom, newVdom);
216
+ case DOM_TYPES.COMPONENT : {
217
+ vdom.component.unmount();
428
218
  break
429
219
  }
220
+ default : {
221
+ throw new Error(`Can't destroy DOM of type ${type}`)
222
+ }
430
223
  }
431
- patchChildren(oldVdom, newVdom);
432
- return newVdom
433
- }
434
- function patchText(oldVdom, newVdom) {
435
- const el = oldVdom.el;
436
- const { value : oldText} = oldVdom;
437
- const { value : newText} = newVdom;
438
- if (oldText !== newText) {
439
- el.nodeValue = newText;
440
- }
441
- }
442
- function findIndexInParent(parentEl, el) {
443
- const index = Array.from(parentEl.childNodes).indexOf(el);
444
- if (index < 0) {
445
- return null
446
- }
447
- return index
448
- }
449
- function patchElement(oldVdom, newVdom) {
450
- const el = oldVdom.el;
451
- const {
452
- class : oldClass,
453
- style : oldStyle,
454
- on: oldEvents,
455
- ...oldAttrs
456
- } = oldVdom.props;
457
- const {
458
- class: newClass,
459
- style: newStyle,
460
- on: newEvents,
461
- ...newAttrs
462
- } = newVdom.props;
463
- const { listeners: oldListeners } = oldVdom;
464
- patchAttrs(el, oldAttrs, newAttrs);
465
- patchClasses(el, oldClass, newClass);
466
- patchStyles(el, oldStyle, newStyle);
467
- newVdom.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
468
- }
469
- function patchAttrs(el, oldAttrs, newAttrs) {
470
- const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
471
- for (const attr of removed) {
472
- removeAttributeCustom(el, attr);
473
- }
474
- for (const attr of added.concat(updated)) {
475
- setAttribute(el, attr, newAttrs[attr]);
476
- }
477
- }
478
- function patchClasses(el, oldClass, newClass) {
479
- const oldClasses = toClassList(oldClass);
480
- const newClasses = toClassList(newClass);
481
- const {added, removed} =
482
- arraysDiff(oldClasses, newClasses);
483
- if (removed.length > 0) {
484
- el.classList.remove(...removed);
485
- }
486
- if (added.length > 0) {
487
- el.classList.add(...added);
488
- }
489
- }
490
- function toClassList(classes = '') {
491
- return Array.isArray(classes)
492
- ? classes.filter(isNotBlankOrEmptyString)
493
- : classes.split(/(\s+)/)
494
- .filter(isNotBlankOrEmptyString)
495
- }
496
- function patchStyles(el, oldStyle = {}, newStyle = {}) {
497
- const {added, removed, updated } = objectsDiff(oldStyle, newStyle);
498
- for (const style of removed) {
499
- removeStyle(el, style);
500
- }
501
- for (const style of added.concat(updated)) {
502
- setStyle(el, style, newStyle[style]);
503
- }
224
+ delete vdom.el;
504
225
  }
505
- function patchEvents(
506
- el,
507
- oldListeners = {},
508
- oldEvents = {},
509
- newEvents = {},
510
- ) {
511
- const { removed, added, updated} =
512
- objectsDiff(oldEvents, newEvents);
513
- for (const eventName of removed.concat(updated)) {
514
- el.removeEventListener(eventName, oldListeners[eventName]);
515
- }
516
- const addedListeners = {};
517
- for (const eventName of added.concat(updated)) {
518
- const listener = addEventListener(eventName, newEvents[eventName], el);
519
- addedListeners[eventName] = listener;
520
- }
521
- return addedListeners
226
+ function removeTextNode(vdom) {
227
+ const { el } = vdom;
228
+ el.remove();
522
229
  }
523
- function extractChildren(vdom) {
524
- if (vdom.children == null) {
525
- return []
526
- }
527
- const children = [];
528
- for (const child of vdom.children) {
529
- if (child.type === DOM_TYPES.FRAGMENT) {
530
- children.push(...extractChildren(child));
531
- } else {
532
- children.push(child);
533
- }
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;
534
237
  }
535
- return children
536
238
  }
537
- function patchChildren(oldVdom, newVdom) {
538
- const oldChildren = extractChildren(oldVdom);
539
- const newChildren = extractChildren(newVdom);
540
- const parentEl = oldVdom.el;
541
- const diffSeq = arraysDiffSequence(oldChildren, newChildren, areNodesEqual);
542
- for (const operation of diffSeq) {
543
- const { originalIndex, index, item} = operation;
544
- switch (operation.op) {
545
- case ARRAY_DIFF_OP.ADD: {
546
- mountDom(item, parentEl, index);
547
- break
548
- }
549
- case ARRAY_DIFF_OP.REMOVE: {
550
- destroyDom(item);
551
- break
552
- }
553
- case ARRAY_DIFF_OP.MOVE: {
554
- const oldChild = oldChildren[originalIndex];
555
- const newChild = newChildren[index];
556
- const el = oldChild.el;
557
- const elAtTargetIndex = parentEl.childNodes[index];
558
- parentEl.insertBefore(el, elAtTargetIndex);
559
- patchDOM(oldChildren[originalIndex], newChild, parentEl);
560
- break
561
- }
562
- case ARRAY_DIFF_OP.NOOP : {
563
- patchDOM(oldChildren[originalIndex], newChildren[index], parentEl);
564
- break
565
- }
566
- }
567
- }
239
+ function removeFragmentNode(vdom) {
240
+ const { children } = vdom;
241
+ children.forEach(destroyDom);
568
242
  }
569
243
 
570
- function createApp({ state, view, reducers = {} }) {
244
+ function createApp(RootComponent, props = {}) {
571
245
  let parentEl = null;
572
- let vdom = null;
573
246
  let isMounted = false;
574
- const dispatcher = new Dispatcher();
575
- const subscriptions = [dispatcher.afterEveryCommand(renderApp)];
576
- function emit(eventName, payload) {
577
- dispatcher.dispatch(eventName, payload);
578
- }
579
- for (const actionName in reducers) {
580
- const reducer = reducers[actionName];
581
- const subs = dispatcher.subscribe(actionName, (payload) => {
582
- state = reducer(state, payload);
583
- });
584
- subscriptions.push(subs);
585
- }
586
- function renderApp() {
587
- const newVdom = view(state, emit);
588
- if (!newVdom) {
589
- console.error('View returned', newVdom);
590
- return;
591
- }
592
- vdom = patchDOM(vdom, newVdom, parentEl);
247
+ let vdom = null;
248
+ function reset() {
249
+ parentEl = null;
250
+ isMounted = false;
251
+ vdom = null;
593
252
  }
594
253
  return {
595
254
  mount(_parentEl) {
596
255
  if (isMounted) {
597
- throw new Error("The application is already mounted")
256
+ throw new Error(`The application is already mounted`)
598
257
  }
599
258
  parentEl = _parentEl;
600
- vdom = view(state, emit);
259
+ vdom = h(RootComponent, props);
601
260
  mountDom(vdom, parentEl);
602
261
  isMounted = true;
603
262
  },
604
263
  unmount() {
264
+ if (!isMounted) {
265
+ throw new Error(`The application is not mounted`)
266
+ }
605
267
  destroyDom(vdom);
606
- vdom = null;
607
- subscriptions.forEach((unsubscribe) => unsubscribe());
608
- isMounted = false;
609
- },
268
+ reset();
269
+ }
610
270
  }
611
271
  }
612
272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lahama",
3
- "version": "2.5.2",
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
  }