betal-fe 1.0.0 → 2.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/betal-fe.js +352 -16
  2. package/package.json +1 -1
package/dist/betal-fe.js CHANGED
@@ -15,9 +15,143 @@ function removeEventListeners(listeners = {}, el) {
15
15
  });
16
16
  }
17
17
 
18
+ const ARRAY_DIFF_OP = {
19
+ ADD: "add",
20
+ REMOVE: "remove",
21
+ MOVE: "move",
22
+ NOOP: "noop",
23
+ };
18
24
  function withoutNulls(arr) {
19
25
  return arr.filter((item) => item != null);
20
26
  }
27
+ function arraysDiff(oldArray, newArray) {
28
+ return {
29
+ added: newArray.filter((newItem) => !oldArray.includes(newItem)),
30
+ removed: oldArray.filter((oldItem) => !newArray.includes(oldItem)),
31
+ };
32
+ }
33
+ class ArrayWithOriginalIndices {
34
+ #array = [];
35
+ #originalIndices = [];
36
+ #equalsFn;
37
+ constructor(array, equalsFn) {
38
+ this.#array = [...array];
39
+ this.#originalIndices = array.map((_, i) => i);
40
+ this.#equalsFn = equalsFn;
41
+ }
42
+ get length() {
43
+ return this.#array.length;
44
+ }
45
+ originalIndexAt(index) {
46
+ return this.#originalIndices[index];
47
+ }
48
+ isRemoval(index, newArray) {
49
+ if (index >= this.length) {
50
+ return false;
51
+ }
52
+ const item = this.#array[index];
53
+ const indexInNewArray = newArray.findIndex((newItem) =>
54
+ this.#equalsFn(item, newItem)
55
+ );
56
+ return indexInNewArray === -1;
57
+ }
58
+ removeItem(index) {
59
+ const operation = {
60
+ op: ARRAY_DIFF_OP.REMOVE,
61
+ index,
62
+ item: this.#array[index],
63
+ };
64
+ this.#array.splice(index, 1);
65
+ this.#originalIndices.splice(index, 1);
66
+ return operation;
67
+ }
68
+ isNoop(index, newArray) {
69
+ if (index >= this.length) {
70
+ return false;
71
+ }
72
+ const item = this.#array[index];
73
+ const newItem = newArray[index];
74
+ return this.#equalsFn(item, newItem);
75
+ }
76
+ noopItem(index) {
77
+ return {
78
+ op: ARRAY_DIFF_OP.NOOP,
79
+ originalIndex: this.originalIndexAt(index),
80
+ index,
81
+ item: this.#array[index],
82
+ };
83
+ }
84
+ findIndexFrom(item, fromIndex) {
85
+ for (let i = fromIndex; i < this.length; i++) {
86
+ if (this.#equalsFn(item, this.#array[i])) {
87
+ return i;
88
+ }
89
+ }
90
+ return -1;
91
+ }
92
+ isAddition(item, fromIdx) {
93
+ return this.findIndexFrom(item, fromIdx) === -1;
94
+ }
95
+ addItem(item, index) {
96
+ const operation = {
97
+ op: ARRAY_DIFF_OP.ADD,
98
+ index,
99
+ item,
100
+ };
101
+ this.#array.splice(index, 0, item);
102
+ this.#originalIndices.splice(index, 0, -1);
103
+ return operation;
104
+ }
105
+ moveItem(item, toIndex) {
106
+ const fromIndex = this.findIndexFrom(item, toIndex);
107
+ const operation = {
108
+ op: ARRAY_DIFF_OP.MOVE,
109
+ originalIndex: this.originalIndexAt(fromIndex),
110
+ from: fromIndex,
111
+ index: toIndex,
112
+ item: this.#array[fromIndex],
113
+ };
114
+ const [_item] = this.#array.splice(fromIndex, 1);
115
+ this.#array.splice(toIndex, 0, _item);
116
+ const [originalIndex] = this.#originalIndices.splice(fromIndex, 1);
117
+ this.#originalIndices.splice(toIndex, 0, originalIndex);
118
+ return operation;
119
+ }
120
+ removeItemsAfter(index) {
121
+ const operations = [];
122
+ while (this.length > index) {
123
+ operations.push(this.removeItem(index));
124
+ }
125
+ return operations;
126
+ }
127
+ }
128
+ function arraysDiffSequence(
129
+ oldArray,
130
+ newArray,
131
+ equalsFn = (a, b) => a === b
132
+ ) {
133
+ const sequence = [];
134
+ const array = new ArrayWithOriginalIndices(oldArray, equalsFn);
135
+ for (let index = 0; index < newArray.length; index++) {
136
+ if (array.isRemoval(index, newArray)) {
137
+ sequence.push(array.removeItem(index));
138
+ index--;
139
+ continue;
140
+ }
141
+ if (array.isNoop(index, newArray)) {
142
+ sequence.push(array.noopItem(index));
143
+ continue;
144
+ }
145
+ const item = newArray[index];
146
+ if (array.isAddition(item, index)) {
147
+ sequence.push(array.addItem(item, index));
148
+ continue;
149
+ }
150
+ sequence.push(array.moveItem(item, index));
151
+ }
152
+ sequence.push(...array.removeItemsAfter(newArray.length));
153
+ return sequence;
154
+ }
21
155
 
22
156
  const DOM_TYPES = {
23
157
  TEXT: "text",
@@ -46,6 +180,20 @@ function hFragment(vNodes) {
46
180
  children: mapTextNodes(withoutNulls(vNodes)),
47
181
  };
48
182
  }
183
+ function extractChildren(vdom) {
184
+ if (vdom.children == null) {
185
+ return [];
186
+ }
187
+ const children = [];
188
+ for (const child of vdom.children) {
189
+ if (child.type === DOM_TYPES.FRAGMENT) {
190
+ children.push(...extractChildren(child));
191
+ } else {
192
+ children.push(child);
193
+ }
194
+ }
195
+ return children;
196
+ }
49
197
 
50
198
  function destroyDOM(vDom) {
51
199
  const { type } = vDom;
@@ -146,6 +294,9 @@ function setClass(el, className) {
146
294
  function setStyle(el, prop, value) {
147
295
  el.style[prop] = value;
148
296
  }
297
+ function removeStyle(el, prop) {
298
+ el.style[prop] = null;
299
+ }
149
300
  function setAttribute(el, name, value) {
150
301
  if (value == null) {
151
302
  removeAttribute(el, name);
@@ -160,18 +311,18 @@ function removeAttribute(el, name) {
160
311
  el.removeAttribute(name);
161
312
  }
162
313
 
163
- function mountDOM(vDom, parentElement) {
314
+ function mountDOM(vDom, parentElement, index) {
164
315
  switch (vDom.type) {
165
316
  case DOM_TYPES.TEXT: {
166
- createTextNode(vDom, parentElement);
317
+ createTextNode(vDom, parentElement, index);
167
318
  break;
168
319
  }
169
320
  case DOM_TYPES.ELEMENT: {
170
- createElementNode(vDom, parentElement);
321
+ createElementNode(vDom, parentElement, index);
171
322
  break;
172
323
  }
173
324
  case DOM_TYPES.FRAGMENT: {
174
- createFragmentNode(vDom, parentElement);
325
+ createFragmentNode(vDom, parentElement, index);
175
326
  break;
176
327
  }
177
328
  default: {
@@ -179,30 +330,214 @@ function mountDOM(vDom, parentElement) {
179
330
  }
180
331
  }
181
332
  }
182
- function createTextNode(vDom, parentElement) {
333
+ function createTextNode(vDom, parentElement, index) {
183
334
  const { value } = vDom;
184
335
  const textNode = document.createTextNode(value);
185
336
  vDom.el = textNode;
186
- parentElement.appendChild(textNode);
337
+ insert(textNode, parentElement, index);
187
338
  }
188
- function createFragmentNode(vDom, parentElement) {
339
+ function createFragmentNode(vDom, parentElement, index) {
189
340
  const { children } = vDom;
190
341
  vDom.el = parentElement;
191
- children.forEach((child) => mountDOM(child, parentElement));
342
+ children.forEach((child) => mountDOM(child, parentElement, index ? index + 1 : null));
192
343
  }
193
- function createElementNode(vDom, parentElement) {
344
+ function createElementNode(vDom, parentElement, index) {
194
345
  const { tag, props, children } = vDom;
195
346
  const element = document.createElement(tag);
196
347
  addProps(element, props, vDom);
197
348
  vDom.el = element;
198
349
  children.forEach((child) => mountDOM(child, element));
199
- parentElement.appendChild(element);
350
+ insert(element, parentElement, index);
200
351
  }
201
352
  function addProps(el, props, vdom) {
202
353
  const { on: events, ...attrs } = props;
203
354
  vdom.listeners = addEventListeners(events, el);
204
355
  setAttributes(el, attrs);
205
356
  }
357
+ function insert(el, parentEl, index) {
358
+ if (index == null) {
359
+ parentEl.append(el);
360
+ return;
361
+ }
362
+ if (index < 0) {
363
+ throw new Error(`Index must be a positive integer, got ${index}`);
364
+ }
365
+ const children = parentEl.childNodes;
366
+ if (index >= children.length) {
367
+ parentEl.append(el);
368
+ } else {
369
+ parentEl.insertBefore(el, children[index]);
370
+ }
371
+ }
372
+
373
+ function areNodesEqual(nodeOne, nodeTwo) {
374
+ if (nodeOne.type !== nodeTwo.type) {
375
+ return false;
376
+ }
377
+ if (nodeOne.type === DOM_TYPES.ELEMENT) {
378
+ const { tag: tagOne } = nodeOne;
379
+ const { tag: tagTwo } = nodeTwo;
380
+ return tagOne === tagTwo;
381
+ }
382
+ return true;
383
+ }
384
+
385
+ function objectsDiff(oldObj, newObj) {
386
+ const oldKeys = Object.keys(oldObj);
387
+ const newKeys = Object.keys(newObj);
388
+ return {
389
+ added: newKeys.filter((key) => !(key in oldObj)),
390
+ removed: oldKeys.filter((key) => !(key in newObj)),
391
+ updated: newKeys.filter(
392
+ (key) => key in oldObj && oldObj[key] !== newObj[key]
393
+ ),
394
+ };
395
+ }
396
+
397
+ function isNotEmptyString(str) {
398
+ return str !== ''
399
+ }
400
+ function isNotBlankOrEmptyString(str) {
401
+ return isNotEmptyString(str.trim())
402
+ }
403
+
404
+ function patchDOM(oldVdom, newVdom, parentEl) {
405
+ if (!areNodesEqual(oldVdom, newVdom)) {
406
+ const index = findIndexInParent(parentEl, oldVdom.el);
407
+ destroyDOM(oldVdom);
408
+ mountDOM(newVdom, parentEl, index);
409
+ return newVdom;
410
+ }
411
+ newVdom.el = oldVdom.el;
412
+ switch (newVdom.type) {
413
+ case DOM_TYPES.TEXT: {
414
+ patchText(oldVdom, newVdom);
415
+ return newVdom;
416
+ }
417
+ case DOM_TYPES.ELEMENT: {
418
+ patchElement(oldVdom, newVdom);
419
+ break;
420
+ }
421
+ }
422
+ patchChildren(oldVdom, newVdom);
423
+ return newVdom;
424
+ }
425
+ function findIndexInParent(parentEl, el) {
426
+ const index = Array.from(parentEl.childNodes).indexOf(el);
427
+ if (index < 0) {
428
+ return null;
429
+ }
430
+ return index;
431
+ }
432
+ function patchText(oldVdom, newVdom) {
433
+ const { value: oldText } = oldVdom;
434
+ const { value: newText } = newVdom;
435
+ if (oldText !== newText) {
436
+ newVdom.el.nodeValue = newText;
437
+ }
438
+ }
439
+ function patchElement(oldVdom, newVdom) {
440
+ const el = oldVdom.el;
441
+ const {
442
+ class: oldClass,
443
+ style: oldStyle,
444
+ on: oldEvents,
445
+ ...oldAttrs
446
+ } = oldVdom.props;
447
+ const {
448
+ class: newClass,
449
+ style: newStyle,
450
+ on: newEvents,
451
+ ...newAttrs
452
+ } = newVdom.props;
453
+ const { listeners: oldListeners } = oldVdom;
454
+ patchAttrs(el, oldAttrs, newAttrs);
455
+ patchClasses(el, oldClass, newClass);
456
+ patchStyles(el, oldStyle, newStyle);
457
+ newVdom.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
458
+ }
459
+ function patchAttrs(el, oldAttrs, newAttrs) {
460
+ const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
461
+ for (const attr of removed) {
462
+ removeAttribute(el, attr);
463
+ }
464
+ for (const attr of added.concat(updated)) {
465
+ setAttribute(el, attr, newAttrs[attr]);
466
+ }
467
+ }
468
+ function patchClasses(el, oldClass, newClass) {
469
+ const oldClasses = toClassList(oldClass);
470
+ const newClasses = toClassList(newClass);
471
+ const { added, removed } = arraysDiff(oldClasses, newClasses);
472
+ if (removed.length > 0) {
473
+ el.classList.remove(...removed);
474
+ }
475
+ if (added.length > 0) {
476
+ el.classList.add(...added);
477
+ }
478
+ }
479
+ function toClassList(classes = "") {
480
+ return Array.isArray(classes)
481
+ ? classes.filter(isNotBlankOrEmptyString)
482
+ : classes.split(/(\s+)/).filter(isNotBlankOrEmptyString);
483
+ }
484
+ function patchStyles(el, oldStyle = {}, newStyle = {}) {
485
+ const { added, removed, updated } = objectsDiff(oldStyle, newStyle);
486
+ for (const style of removed) {
487
+ removeStyle(el, style);
488
+ }
489
+ for (const style of added.concat(updated)) {
490
+ setStyle(el, style, newStyle[style]);
491
+ }
492
+ }
493
+ function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}) {
494
+ const { removed, added, updated } = objectsDiff(oldEvents, newEvents);
495
+ for (const eventName of removed.concat(updated)) {
496
+ el.removeEventListener(eventName, oldListeners[eventName]);
497
+ }
498
+ const addedListeners = {};
499
+ for (const eventName of added.concat(updated)) {
500
+ const listener = addEventListener(eventName, newEvents[eventName], el);
501
+ addedListeners[eventName] = listener;
502
+ }
503
+ return addedListeners;
504
+ }
505
+ function patchChildren(oldVdom, newVdom) {
506
+ const oldChildren = extractChildren(oldVdom);
507
+ const newChildren = extractChildren(newVdom);
508
+ const parentEl = oldVdom.el;
509
+ const diffSeq = arraysDiffSequence(
510
+ oldChildren,
511
+ newChildren,
512
+ areNodesEqual
513
+ );
514
+ for (const operation of diffSeq) {
515
+ const { originalIndex, index, item } = operation;
516
+ switch (operation.op) {
517
+ case ARRAY_DIFF_OP.ADD: {
518
+ mountDOM(item, parentEl, index);
519
+ break;
520
+ }
521
+ case ARRAY_DIFF_OP.REMOVE: {
522
+ destroyDOM(item);
523
+ break;
524
+ }
525
+ case ARRAY_DIFF_OP.MOVE: {
526
+ const oldChild = oldChildren[originalIndex];
527
+ const newChild = newChildren[index];
528
+ const el = oldChild.el;
529
+ const elAtTargetIndex = parentEl.childNodes[index];
530
+ parentEl.insertBefore(el, elAtTargetIndex);
531
+ patchDOM(oldChild, newChild, parentEl);
532
+ break;
533
+ }
534
+ case ARRAY_DIFF_OP.NOOP: {
535
+ patchDOM(oldChildren[originalIndex], newChildren[index], parentEl);
536
+ break;
537
+ }
538
+ }
539
+ }
540
+ }
206
541
 
207
542
  function createApp({ state, view, reducers = {} }) {
208
543
  let parentEl = null;
@@ -220,16 +555,17 @@ function createApp({ state, view, reducers = {} }) {
220
555
  subscriptions.push(subs);
221
556
  }
222
557
  function renderApp() {
223
- if (vdom) {
224
- destroyDOM(vdom);
225
- }
226
- vdom = view(state, emit);
227
- mountDOM(vdom, parentEl);
558
+ const newVdom = view(state, emit);
559
+ vdom = patchDOM(vdom, newVdom, parentEl);
228
560
  }
229
561
  return {
230
562
  mount(_parentEl) {
563
+ if (vdom) {
564
+ throw new Error('App is already mounted');
565
+ }
231
566
  parentEl = _parentEl;
232
- renderApp();
567
+ vdom = view(state, emit);
568
+ mountDOM(vdom, parentEl);
233
569
  },
234
570
  unmount() {
235
571
  destroyDOM(vdom);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "betal-fe",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/betal-fe.js",
6
6
  "files": [