my-framework-almaz 1.0.0 → 2.0.1

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