betal-fe 1.0.0 → 2.1.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 +360 -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,222 @@ 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
+ const added = [];
389
+ const updated = [];
390
+ newKeys.forEach(key => {
391
+ if (!(key in oldObj)) {
392
+ added.push(key);
393
+ }
394
+ if (key in oldObj && oldObj[key] !== newObj[key]) {
395
+ updated.push(key);
396
+ }
397
+ });
398
+ return {
399
+ added,
400
+ removed: oldKeys.filter((key) => !(key in newObj)),
401
+ updated,
402
+ };
403
+ }
404
+
405
+ function isNotEmptyString(str) {
406
+ return str !== ''
407
+ }
408
+ function isNotBlankOrEmptyString(str) {
409
+ return isNotEmptyString(str.trim())
410
+ }
411
+
412
+ function patchDOM(oldVdom, newVdom, parentEl) {
413
+ if (!areNodesEqual(oldVdom, newVdom)) {
414
+ const index = findIndexInParent(parentEl, oldVdom.el);
415
+ destroyDOM(oldVdom);
416
+ mountDOM(newVdom, parentEl, index);
417
+ return newVdom;
418
+ }
419
+ newVdom.el = oldVdom.el;
420
+ switch (newVdom.type) {
421
+ case DOM_TYPES.TEXT: {
422
+ patchText(oldVdom, newVdom);
423
+ return newVdom;
424
+ }
425
+ case DOM_TYPES.ELEMENT: {
426
+ patchElement(oldVdom, newVdom);
427
+ break;
428
+ }
429
+ }
430
+ patchChildren(oldVdom, newVdom);
431
+ return newVdom;
432
+ }
433
+ function findIndexInParent(parentEl, el) {
434
+ const index = Array.from(parentEl.childNodes).indexOf(el);
435
+ if (index < 0) {
436
+ return null;
437
+ }
438
+ return index;
439
+ }
440
+ function patchText(oldVdom, newVdom) {
441
+ const { value: oldText } = oldVdom;
442
+ const { value: newText } = newVdom;
443
+ if (oldText !== newText) {
444
+ newVdom.el.nodeValue = newText;
445
+ }
446
+ }
447
+ function patchElement(oldVdom, newVdom) {
448
+ const el = oldVdom.el;
449
+ const {
450
+ class: oldClass,
451
+ style: oldStyle,
452
+ on: oldEvents,
453
+ ...oldAttrs
454
+ } = oldVdom.props;
455
+ const {
456
+ class: newClass,
457
+ style: newStyle,
458
+ on: newEvents,
459
+ ...newAttrs
460
+ } = newVdom.props;
461
+ const { listeners: oldListeners } = oldVdom;
462
+ patchAttrs(el, oldAttrs, newAttrs);
463
+ patchClasses(el, oldClass, newClass);
464
+ patchStyles(el, oldStyle, newStyle);
465
+ newVdom.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
466
+ }
467
+ function patchAttrs(el, oldAttrs, newAttrs) {
468
+ const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
469
+ for (const attr of removed) {
470
+ removeAttribute(el, attr);
471
+ }
472
+ for (const attr of added.concat(updated)) {
473
+ setAttribute(el, attr, newAttrs[attr]);
474
+ }
475
+ }
476
+ function patchClasses(el, oldClass, newClass) {
477
+ const oldClasses = toClassList(oldClass);
478
+ const newClasses = toClassList(newClass);
479
+ const { added, removed } = arraysDiff(oldClasses, newClasses);
480
+ if (removed.length > 0) {
481
+ el.classList.remove(...removed);
482
+ }
483
+ if (added.length > 0) {
484
+ el.classList.add(...added);
485
+ }
486
+ }
487
+ function toClassList(classes = "") {
488
+ return Array.isArray(classes)
489
+ ? classes.filter(isNotBlankOrEmptyString)
490
+ : classes.split(/(\s+)/).filter(isNotBlankOrEmptyString);
491
+ }
492
+ function patchStyles(el, oldStyle = {}, newStyle = {}) {
493
+ const { added, removed, updated } = objectsDiff(oldStyle, newStyle);
494
+ for (const style of removed) {
495
+ removeStyle(el, style);
496
+ }
497
+ for (const style of added.concat(updated)) {
498
+ setStyle(el, style, newStyle[style]);
499
+ }
500
+ }
501
+ function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}) {
502
+ const { removed, added, updated } = objectsDiff(oldEvents, newEvents);
503
+ for (const eventName of removed.concat(updated)) {
504
+ el.removeEventListener(eventName, oldListeners[eventName]);
505
+ }
506
+ const addedListeners = {};
507
+ for (const eventName of added.concat(updated)) {
508
+ const listener = addEventListener(eventName, newEvents[eventName], el);
509
+ addedListeners[eventName] = listener;
510
+ }
511
+ return addedListeners;
512
+ }
513
+ function patchChildren(oldVdom, newVdom) {
514
+ const oldChildren = extractChildren(oldVdom);
515
+ const newChildren = extractChildren(newVdom);
516
+ const parentEl = oldVdom.el;
517
+ const diffSeq = arraysDiffSequence(
518
+ oldChildren,
519
+ newChildren,
520
+ areNodesEqual
521
+ );
522
+ for (const operation of diffSeq) {
523
+ const { originalIndex, index, item } = operation;
524
+ switch (operation.op) {
525
+ case ARRAY_DIFF_OP.ADD: {
526
+ mountDOM(item, parentEl, index);
527
+ break;
528
+ }
529
+ case ARRAY_DIFF_OP.REMOVE: {
530
+ destroyDOM(item);
531
+ break;
532
+ }
533
+ case ARRAY_DIFF_OP.MOVE: {
534
+ const oldChild = oldChildren[originalIndex];
535
+ const newChild = newChildren[index];
536
+ const el = oldChild.el;
537
+ const elAtTargetIndex = parentEl.childNodes[index];
538
+ parentEl.insertBefore(el, elAtTargetIndex);
539
+ patchDOM(oldChild, newChild, parentEl);
540
+ break;
541
+ }
542
+ case ARRAY_DIFF_OP.NOOP: {
543
+ patchDOM(oldChildren[originalIndex], newChildren[index], parentEl);
544
+ break;
545
+ }
546
+ }
547
+ }
548
+ }
206
549
 
207
550
  function createApp({ state, view, reducers = {} }) {
208
551
  let parentEl = null;
@@ -220,16 +563,17 @@ function createApp({ state, view, reducers = {} }) {
220
563
  subscriptions.push(subs);
221
564
  }
222
565
  function renderApp() {
223
- if (vdom) {
224
- destroyDOM(vdom);
225
- }
226
- vdom = view(state, emit);
227
- mountDOM(vdom, parentEl);
566
+ const newVdom = view(state, emit);
567
+ vdom = patchDOM(vdom, newVdom, parentEl);
228
568
  }
229
569
  return {
230
570
  mount(_parentEl) {
571
+ if (vdom) {
572
+ throw new Error('App is already mounted');
573
+ }
231
574
  parentEl = _parentEl;
232
- renderApp();
575
+ vdom = view(state, emit);
576
+ mountDOM(vdom, parentEl);
233
577
  },
234
578
  unmount() {
235
579
  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.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/betal-fe.js",
6
6
  "files": [