mark-fe-fwk 1.0.2 → 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/mark-fe-fwk.js +389 -42
  2. package/package.json +1 -1
@@ -15,9 +15,145 @@ 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 || item === undefined))
20
26
  }
27
+ function arraysDiff(oldArray, newArray) {
28
+ return {
29
+ added: newArray.filter(
30
+ (newItem) => !oldArray.includes(newItem)
31
+ ),
32
+ removed: oldArray.filter(
33
+ (oldItem) => !newArray.includes(oldItem)
34
+ )
35
+ }
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, newArray) {
50
+ if (index >= this.length) {
51
+ return false
52
+ }
53
+ const item = this.#array[index];
54
+ const indexInNewArray = newArray.findIndex((newItem) => this.#equalsFn(item, newItem));
55
+ return indexInNewArray === -1
56
+ }
57
+ removeItem(index) {
58
+ const operation = {
59
+ op: ARRAY_DIFF_OP.REMOVE,
60
+ index,
61
+ item: this.#array[index]
62
+ };
63
+ this.#array.splice(index, 1);
64
+ this.#originalIndices.splice(index,1);
65
+ return operation
66
+ }
67
+ isNoop(index, newArray) {
68
+ if(index >= this.length) {
69
+ return false
70
+ }
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.findIndexFrom(item, fromIdx) === -1
88
+ }
89
+ findIndexFrom(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 = this.findIndexFrom(item, toIndex);
109
+ const operation = {
110
+ op: ARRAY_DIFF_OP.MOVE,
111
+ originalIndex: this.originalIndexAt(fromIndex),
112
+ from: fromIndex,
113
+ index: toIndex,
114
+ item: this.#array[fromIndex]
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
+ removeItemsAfter(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.removeItemsAfter(newArray.length));
155
+ return sequence
156
+ }
21
157
 
22
158
  const DOM_TYPES = {
23
159
  TEXT: 'text',
@@ -44,6 +180,20 @@ function hFragment(vNodes) {
44
180
  children: mapTextNodes(withoutNulls(vNodes)),
45
181
  }
46
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
+ }
47
197
 
48
198
  function destroyDOM(vdom) {
49
199
  const { type } = vdom;
@@ -84,6 +234,40 @@ function removeFragmentNodes(vdom) {
84
234
  children.forEach(destroyDOM);
85
235
  }
86
236
 
237
+ class Dispatcher {
238
+ #subs = new Map()
239
+ #afterHandlers = []
240
+ subscribe(commandName, handler) {
241
+ if(!this.#subs.has(commandName)) {
242
+ this.#subs.set(commandName, []);
243
+ }
244
+ const handlers = this.#subs.get(commandName);
245
+ if (handlers.includes(handler)) {
246
+ return () => {}
247
+ }
248
+ handlers.push(handler);
249
+ return () => {
250
+ const idx = handlers.indexOf(handler);
251
+ handlers.splice(idx,1);
252
+ }
253
+ }
254
+ afterEveryCommand(handler) {
255
+ this.#afterHandlers.push(handler);
256
+ return () => {
257
+ const idx = this.#afterHandlers.indexOf(handler);
258
+ this.#afterHandlers.splice(idx, 1);
259
+ }
260
+ }
261
+ dispatch(commandName, payload) {
262
+ if (this.#subs.has(commandName)) {
263
+ this.#subs.get(commandName).forEach((handler) => handler(payload));
264
+ } else {
265
+ console.warn(`No handlers for command: ${commandName}`);
266
+ }
267
+ this.#afterHandlers.forEach((handler) => handler());
268
+ }
269
+ }
270
+
87
271
  function setAttributes(el, attrs) {
88
272
  const { class: className, style, ...otherAttrs } = attrs;
89
273
  if (className) {
@@ -110,6 +294,9 @@ function setClass(el, className) {
110
294
  function setStyle(el, name, value) {
111
295
  el.style[name] = value;
112
296
  }
297
+ function removeStyle(el, name) {
298
+ el.style[name] = null;
299
+ }
113
300
  function setAttribute(el, name, value) {
114
301
  if(value == null) {
115
302
  removeAttribute(el, name);
@@ -124,18 +311,18 @@ function removeAttribute(el, name) {
124
311
  el.removeAttribute(name);
125
312
  }
126
313
 
127
- function mountDOM(vdom, parentEl) {
314
+ function mountDOM(vdom, parentEl, index) {
128
315
  switch (vdom.type) {
129
316
  case DOM_TYPES.TEXT: {
130
- createTextNode(vdom, parentEl);
317
+ createTextNode(vdom, parentEl, index);
131
318
  break
132
319
  }
133
320
  case DOM_TYPES.ELEMENT: {
134
- createElementNode(vdom, parentEl);
321
+ createElementNode(vdom, parentEl, index);
135
322
  break
136
323
  }
137
324
  case DOM_TYPES.FRAGMENT: {
138
- createFragmentNodes(vdom, parentEl);
325
+ createFragmentNodes(vdom, parentEl, index);
139
326
  break
140
327
  }
141
328
  default: {
@@ -143,68 +330,224 @@ function mountDOM(vdom, parentEl) {
143
330
  }
144
331
  }
145
332
  }
146
- function createTextNode(vdom, parentEl) {
333
+ function createTextNode(vdom, parentEl, index) {
147
334
  const value = vdom.value;
148
335
  const textNode = document.createTextNode(value);
149
336
  vdom.el = textNode;
150
- parentEl.append(textNode);
337
+ insert(textNode, parentEl, index);
151
338
  }
152
- function createFragmentNodes(vdom, parentEl) {
339
+ function createFragmentNodes(vdom, parentEl, index) {
153
340
  const { children } = vdom;
154
341
  vdom.el = parentEl;
155
- children.forEach((child) => mountDOM(child, parentEl));
342
+ children.forEach((child, i) => mountDOM(child, parentEl, index ? index + i : null));
156
343
  }
157
- function createElementNode(vdom, parentEl) {
344
+ function createElementNode(vdom, parentEl, index) {
158
345
  const { tag, props, children } = vdom;
159
346
  const element = document.createElement(tag);
160
347
  addProps(element, props, vdom);
161
348
  vdom.el = element;
162
349
  children.forEach((child) => mountDOM(child, element));
163
- parentEl.append(element);
350
+ insert(element, parentEl, index);
164
351
  }
165
352
  function addProps(el, props, vdom) {
166
353
  const { on: events, ...attrs } = props;
167
354
  vdom.listeners = addEventListeners(events, el);
168
355
  setAttributes(el, attrs);
169
356
  }
357
+ function insert(el, parentEl, index) {
358
+ if(index == null) {
359
+ parentEl.append(el);
360
+ return
361
+ }
362
+ if(index < 0)
363
+ {
364
+ throw new Error(`Index must be a positive integer, got ${index}`)
365
+ }
366
+ const children = parentEl.childNodes;
367
+ if(index >= children.length) {
368
+ parentEl.append(el);
369
+ } else {
370
+ parentEl.insertBefore(el, children[index]);
371
+ }
372
+ }
170
373
 
171
- class Dispatcher {
172
- #subs = new Map()
173
- #afterHandlers = []
174
- subscribe(commandName, handler) {
175
- if(!this.#subs.has(commandName)) {
176
- this.#subs.set(commandName, []);
177
- }
178
- const handlers = this.#subs.get(commandName);
179
- if (handlers.includes(handler)) {
180
- return () => {}
374
+ function areNodesEqual(nodeOne, nodeTwo) {
375
+ if(nodeOne.type !== nodeTwo.type) {
376
+ return false
377
+ }
378
+ if (nodeOne.type === DOM_TYPES.ELEMENT) {
379
+ const { tag: tagOne } = nodeOne;
380
+ const { tag: tagTwo } = nodeTwo;
381
+ return tagOne === tagTwo
382
+ }
383
+ return true
384
+ }
385
+
386
+ function objectsDiff(oldObj, newObj) {
387
+ const oldKeys = Object.keys(oldObj);
388
+ const newKeys = Object.keys(newObj);
389
+ return {
390
+ added: newKeys.filter((key) => !(key in oldObj)),
391
+ removed: oldKeys.filter((key) => !(key in newObj)),
392
+ updated: newKeys.filter(
393
+ (key) => key in oldObj && oldObj[key] !== newObj[key]
394
+ )
395
+ }
396
+ }
397
+
398
+ function isNotEmptyString(str) {
399
+ return str !== ''
400
+ }
401
+ function isNotBlankOrEmptyString(str) {
402
+ return isNotEmptyString(str.trim())
403
+ }
404
+
405
+ function patchDOM(oldVdom, newVdom, parentEl) {
406
+ if (!areNodesEqual(oldVdom, newVdom)) {
407
+ const index = findIndexInParent(parentEl, oldVdom.el);
408
+ destroyDOM(oldVdom);
409
+ mountDOM(newVdom, parentEl, index);
410
+ return newVdom
411
+ }
412
+ newVdom.el = oldVdom.el;
413
+ switch (newVdom.type) {
414
+ case DOM_TYPES.TEXT: {
415
+ patchText(oldVdom, newVdom);
416
+ return newVdom
181
417
  }
182
- handlers.push(handler);
183
- return () => {
184
- const idx = handlers.indexOf(handler);
185
- handlers.splice(idx,1);
418
+ case DOM_TYPES.ELEMENT: {
419
+ patchElement(oldVdom, newVdom);
420
+ break
186
421
  }
187
422
  }
188
- afterEveryCommand(handler) {
189
- this.#afterHandlers.push(handler);
190
- return () => {
191
- const idx = this.#afterHandlers.indexOf(handler);
192
- this.#afterHandlers.splice(idx, 1);
193
- }
423
+ patchChildren(oldVdom, newVdom);
424
+ return newVdom
425
+ }
426
+ function findIndexInParent(parentEl, el) {
427
+ const index = Array.from(parentEl.childNodes).indexOf(el);
428
+ if (index < 0 ) {
429
+ return null
194
430
  }
195
- dispatch(commandName, payload) {
196
- if (this.#subs.has(commandName)) {
197
- this.#subs.get(commandName).forEach((handler) => handler(payload));
198
- } else {
199
- console.warn(`No handlers for command: ${commandName}`);
431
+ return index
432
+ }
433
+ function patchText(oldVdom, newVdom) {
434
+ const el = oldVdom.el;
435
+ const oldText = oldVdom.value;
436
+ const newText = newVdom.value;
437
+ if (oldText !== newText) {
438
+ el.nodeValue = newText;
439
+ }
440
+ }
441
+ function patchElement(oldVdom, newVdom) {
442
+ const el = oldVdom.el;
443
+ const {
444
+ class: oldClass,
445
+ style: oldStyle,
446
+ on: oldEvents,
447
+ ...oldAttrs
448
+ } = newVdom.props;
449
+ const {
450
+ class: newClass,
451
+ style: newStyle,
452
+ on: newEvents,
453
+ ...newAttrs
454
+ } = newVdom.props;
455
+ patchAttrs(el, oldAttrs, newAttrs);
456
+ patchClasses(el, oldClass, newClass);
457
+ patchStyles(el, oldStyle, newStyle);
458
+ newVdom.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
459
+ }
460
+ function patchAttrs(el, oldAttrs, newAttrs) {
461
+ const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
462
+ for (const attr of removed) {
463
+ removeAttribute(el, attr);
464
+ }
465
+ for (const attr of added.concat(updated)) {
466
+ setAttribute(el, attr, newAttrs[attr]);
467
+ }
468
+ }
469
+ function patchClasses(el, oldClass, newClass) {
470
+ const oldClasses = toClassList(oldClass);
471
+ const newClasses = toClassList(newClass);
472
+ const { added, removed } = arraysDiff(oldClasses, newClasses);
473
+ if(removed.length > 0) {
474
+ el.classList.remove(...removed);
475
+ }
476
+ if(added.length > 0) {
477
+ el.classList.add(...added);
478
+ }
479
+ }
480
+ function toClassList(classes = '') {
481
+ return Array.isArray(classes)
482
+ ? classes.filter(isNotBlankOrEmptyString)
483
+ : classes.split(/(\s+)/).filter(isNotBlankOrEmptyString)
484
+ }
485
+ function patchStyles(el, oldStyle = {}, newStyle = {}) {
486
+ const {added, removed, updated } = objectsDiff(oldStyle, newStyle);
487
+ for( const style of removed)
488
+ {
489
+ removeStyle(el, style);
490
+ }
491
+ for (const style of added.concat(updated)) {
492
+ setStyle(el, style, newStyle[style]);
493
+ }
494
+ }
495
+ function patchEvents(
496
+ el,
497
+ oldListeners = {},
498
+ oldEvents = {},
499
+ newEvents = {}
500
+ ) {
501
+ const { removed, added, updated } = objectsDiff(oldEvents, newEvents);
502
+ for (const eventName of removed.concat(updated)) {
503
+ el.removeEventListener(eventName, oldListeners[eventName]);
504
+ }
505
+ const addedListeners = {};
506
+ for (const eventName of added.concat(updated)) {
507
+ const listener = addEventListener(eventName, newEvents[eventName], el);
508
+ addedListeners[eventName] = listener;
509
+ }
510
+ return addedListeners
511
+ }
512
+ function patchChildren(oldVdom, newVdom) {
513
+ const oldChildren = extractChildren(oldVdom);
514
+ const newChildren = extractChildren(newVdom);
515
+ const parentEl = oldVdom.el;
516
+ const diffSeq = arraysDiffSequence(
517
+ oldChildren, newChildren, areNodesEqual
518
+ );
519
+ for(const operation of diffSeq) {
520
+ const { originalIndex, index, item } = operation;
521
+ switch (operation.op) {
522
+ case ARRAY_DIFF_OP.ADD: {
523
+ mountDOM(item, parentEl, index);
524
+ break
525
+ }
526
+ case ARRAY_DIFF_OP.REMOVE: {
527
+ destroyDOM(item);
528
+ break
529
+ }
530
+ case ARRAY_DIFF_OP.MOVE: {
531
+ const oldChild = oldChildren[originalIndex];
532
+ const newChild = newChildren[index];
533
+ const el = oldChild.el;
534
+ const elAtTargetIndex = parentEl.childNodes[index];
535
+ parentEl.insertBefore(el, elAtTargetIndex);
536
+ patchDOM(oldChild, newChild, parentEl);
537
+ break
538
+ }
539
+ case ARRAY_DIFF_OP.NOOP: {
540
+ patchDOM(oldChildren[originalIndex], newChildren[index], parentEl);
541
+ break
542
+ }
200
543
  }
201
- this.#afterHandlers.forEach((handler) => handler());
202
544
  }
203
545
  }
204
546
 
205
547
  function createApp({state, view, reducers = {} }) {
206
548
  let parentEl = null;
207
549
  let vdom = null;
550
+ let isMounted = false;
208
551
  const dispatcher = new Dispatcher();
209
552
  const subscriptions = [dispatcher.afterEveryCommand(renderApp)];
210
553
  function emit(eventName, payload) {
@@ -218,21 +561,25 @@ function createApp({state, view, reducers = {} }) {
218
561
  subscriptions.push(subs);
219
562
  }
220
563
  function renderApp() {
221
- if(vdom) {
222
- destroyDOM(vdom);
223
- }
224
- vdom = view(state, emit);
225
- mountDOM(vdom, parentEl);
564
+ const newVdom = view(state, emit);
565
+ vdom = patchDOM(vdom, newVdom, parentEl);
226
566
  }
227
567
  return {
228
568
  mount(_parentEl) {
569
+ if(isMounted)
570
+ {
571
+ throw new Error(`Already mounted!`)
572
+ }
229
573
  parentEl = _parentEl;
230
- renderApp();
574
+ vdom = view(state, emit);
575
+ mountDOM(vdom, parentEl);
576
+ isMounted = true;
231
577
  },
232
578
  unmount() {
233
579
  destroyDOM(vdom);
234
580
  vdom = null;
235
581
  subscriptions.forEach((unsubscribe) => unsubscribe());
582
+ isMounted = false;
236
583
  }
237
584
  }
238
585
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mark-fe-fwk",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "author": "Mark C",
5
5
  "main": "dist/mark-fe-fwk.js",
6
6
  "files": [