@arrai-innovations/reactive-helpers 14.0.1 → 14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arrai-innovations/reactive-helpers",
3
- "version": "14.0.1",
3
+ "version": "14.1.0",
4
4
  "description": "VueJS 3 utility composition functions to help manipulate objects and lists.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -2,6 +2,8 @@ import {
2
2
  addOrUpdateReactiveObject,
3
3
  assignReactiveObject,
4
4
  AssignReactiveObjectError,
5
+ assignReactiveObjectDeep,
6
+ assignReactiveArray,
5
7
  } from "../../../utils/assignReactiveObject.js";
6
8
  import { computed, EffectScope, effectScope, reactive, toRef, unref } from "vue";
7
9
  import { deepUnref } from "vue-deepunref";
@@ -164,6 +166,280 @@ describe("utils/assignReactiveObject", function () {
164
166
  );
165
167
  });
166
168
  });
169
+ it("should update nested Sets within reactive objects", async () => {
170
+ const target = reactive({
171
+ setA: new Set([1, 2, 3]),
172
+ value: 10,
173
+ });
174
+ const source = reactive({
175
+ setA: new Set([3, 4, 5]),
176
+ value: 20,
177
+ });
178
+
179
+ const es = effectScope();
180
+ let computedSum;
181
+ es.run(() => {
182
+ computedSum = computed(() => {
183
+ let sum = 0;
184
+ for (const num of target.setA) {
185
+ sum += num;
186
+ }
187
+ return sum + target.value;
188
+ });
189
+ });
190
+
191
+ try {
192
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
193
+ expect(computedSum.value).toBe(16);
194
+
195
+ assignReactiveObject(target, source);
196
+
197
+ expect(Array.from(target.setA)).toEqual([3, 4, 5]);
198
+ expect(target.value).toBe(20);
199
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
200
+ expect(computedSum.value).toBe(32);
201
+
202
+ source.setA.add(6);
203
+ expect(target.setA.has(6)).toBe(true);
204
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
205
+ expect(computedSum.value).toBe(38);
206
+
207
+ source.value = 25;
208
+ expect(target.value).toBe(25);
209
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
210
+ expect(computedSum.value).toBe(43);
211
+ } finally {
212
+ es.stop();
213
+ }
214
+ });
215
+ it("should update nested Maps within reactive objects", () => {
216
+ const target = reactive({
217
+ mapA: new Map([
218
+ ["x", 1],
219
+ ["y", 2],
220
+ ]),
221
+ value: 5,
222
+ });
223
+ const source = reactive({
224
+ mapA: new Map([
225
+ ["y", 3],
226
+ ["z", 4],
227
+ ]),
228
+ value: 15,
229
+ });
230
+
231
+ const es = effectScope();
232
+ let computedSum;
233
+ es.run(() => {
234
+ computedSum = computed(() => {
235
+ let sum = 0;
236
+ for (const val of target.mapA.values()) {
237
+ sum += val;
238
+ }
239
+ return sum + target.value;
240
+ });
241
+ });
242
+
243
+ try {
244
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
245
+ expect(computedSum.value).toBe(8);
246
+
247
+ assignReactiveObject(target, source);
248
+
249
+ expect(Array.from(target.mapA.entries())).toEqual([
250
+ ["y", 3],
251
+ ["z", 4],
252
+ ]);
253
+ expect(target.value).toBe(15);
254
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
255
+ expect(computedSum.value).toBe(22);
256
+
257
+ source.mapA.set("w", 5);
258
+ expect(target.mapA.has("w")).toBe(true);
259
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
260
+ expect(computedSum.value).toBe(27);
261
+
262
+ source.value = 20;
263
+ expect(target.value).toBe(20);
264
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
265
+ expect(computedSum.value).toBe(32);
266
+ } finally {
267
+ es.stop();
268
+ }
269
+ });
270
+ it("should update Sets within arrays", () => {
271
+ const target = reactive([new Set([1, 2]), new Set([3, 4])]);
272
+ const source = reactive([new Set([2, 3]), new Set([4, 5])]);
273
+
274
+ const es = effectScope();
275
+ let computedSum;
276
+ es.run(() => {
277
+ computedSum = computed(() => {
278
+ return target.reduce((sum, set) => {
279
+ for (const num of set) {
280
+ sum += num;
281
+ }
282
+ return sum;
283
+ }, 0);
284
+ });
285
+ });
286
+
287
+ try {
288
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
289
+ expect(computedSum.value).toBe(10);
290
+
291
+ assignReactiveObject(target, source);
292
+
293
+ expect(target.length).toBe(2);
294
+ expect(Array.from(target[0])).toEqual([2, 3]);
295
+ expect(Array.from(target[1])).toEqual([4, 5]);
296
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
297
+ expect(computedSum.value).toBe(14);
298
+
299
+ source[0].add(6);
300
+ expect(target[0].has(6)).toBe(true);
301
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
302
+ expect(computedSum.value).toBe(20);
303
+
304
+ source[1].delete(4);
305
+ expect(target[1].has(4)).toBe(false);
306
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
307
+ expect(computedSum.value).toBe(16);
308
+ } finally {
309
+ es.stop();
310
+ }
311
+ });
312
+ it("should update Maps within arrays", () => {
313
+ const target = reactive([
314
+ new Map([
315
+ ["a", 1],
316
+ ["b", 2],
317
+ ]),
318
+ new Map([["c", 3]]),
319
+ ]);
320
+ const source = reactive([
321
+ new Map([
322
+ ["a", 2],
323
+ ["d", 4],
324
+ ]),
325
+ new Map([
326
+ ["c", 3],
327
+ ["e", 5],
328
+ ]),
329
+ ]);
330
+
331
+ const es = effectScope();
332
+ let computedSum;
333
+ es.run(() => {
334
+ computedSum = computed(() => {
335
+ return target.reduce((sum, map) => {
336
+ for (const val of map.values()) {
337
+ sum += val;
338
+ }
339
+ return sum;
340
+ }, 0);
341
+ });
342
+ });
343
+
344
+ try {
345
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
346
+ expect(computedSum.value).toBe(6);
347
+
348
+ assignReactiveObject(target, source);
349
+
350
+ expect(target.length).toBe(2);
351
+ expect(Array.from(target[0].entries())).toEqual([
352
+ ["a", 2],
353
+ ["d", 4],
354
+ ]);
355
+ expect(Array.from(target[1].entries())).toEqual([
356
+ ["c", 3],
357
+ ["e", 5],
358
+ ]);
359
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
360
+ expect(computedSum.value).toBe(14);
361
+
362
+ source[0].set("f", 6);
363
+ expect(target[0].has("f")).toBe(true);
364
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
365
+ expect(computedSum.value).toBe(20);
366
+
367
+ source[1].delete("c");
368
+ expect(target[1].has("c")).toBe(false);
369
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
370
+ expect(computedSum.value).toBe(17);
371
+ } finally {
372
+ es.stop();
373
+ }
374
+ });
375
+ });
376
+ describe("assignReactiveObjectDeep", () => {
377
+ it("should recursively assign nested Sets and Maps", () => {
378
+ const target = reactive({
379
+ level1: {
380
+ set: new Set([1]),
381
+ map: new Map([["a", 1]]),
382
+ level2: {
383
+ array: [new Set([2])],
384
+ object: {
385
+ map: new Map([["b", 2]]),
386
+ },
387
+ },
388
+ },
389
+ });
390
+
391
+ const source = reactive({
392
+ level1: {
393
+ set: new Set([3]),
394
+ map: new Map([["a", 3]]),
395
+ level2: {
396
+ array: [new Set([4])],
397
+ object: {
398
+ map: new Map([["b", 4]]),
399
+ },
400
+ },
401
+ },
402
+ });
403
+
404
+ const es = effectScope();
405
+ let computedSum;
406
+ es.run(() => {
407
+ computedSum = computed(() => {
408
+ let sum = 0;
409
+ for (const num of target.level1.set) sum += num;
410
+ for (const val of target.level1.map.values()) sum += val;
411
+ for (const num of target.level1.level2.array[0]) sum += num;
412
+ for (const val of target.level1.level2.object.map.values()) sum += val;
413
+ return sum;
414
+ });
415
+ });
416
+
417
+ try {
418
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
419
+ expect(computedSum.value).toBe(6);
420
+
421
+ assignReactiveObjectDeep(target, source);
422
+
423
+ expect(Array.from(target.level1.set)).toEqual([3]);
424
+ expect(Array.from(target.level1.map.entries())).toEqual([["a", 3]]);
425
+ expect(Array.from(target.level1.level2.array[0])).toEqual([4]);
426
+ expect(Array.from(target.level1.level2.object.map.entries())).toEqual([["b", 4]]);
427
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
428
+ expect(computedSum.value).toBe(14);
429
+
430
+ source.level1.set.add(5);
431
+ expect(target.level1.set.has(5)).toBe(true);
432
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
433
+ expect(computedSum.value).toBe(19);
434
+
435
+ source.level1.level2.object.map.set("c", 6);
436
+ expect(target.level1.level2.object.map.has("c")).toBe(true);
437
+ // @ts-ignore - computedSum.value won't be undefined, it is calculated immediately as required
438
+ expect(computedSum.value).toBe(25);
439
+ } finally {
440
+ es.stop();
441
+ }
442
+ });
167
443
  });
168
444
  it("should work as in the example for the readme", function () {
169
445
  const es = new EffectScope();
@@ -234,4 +510,84 @@ describe("utils/assignReactiveObject", function () {
234
510
  expect(target.a).toEqual({ b: 7, c: 8, d: 9 });
235
511
  });
236
512
  });
513
+ describe("assignReactiveArray", () => {
514
+ it("should assign values from source to target array", () => {
515
+ const target = reactive([1, 2, 3]);
516
+ const source = reactive([4, 5, 6]);
517
+
518
+ const didAnything = assignReactiveArray(target, source);
519
+ expect(didAnything).toBe(true);
520
+
521
+ // Unref the elements for comparison
522
+ const unrefTarget = target.map(unref);
523
+ expect(unrefTarget).toEqual([4, 5, 6]);
524
+ });
525
+
526
+ it("should handle source array longer than target", () => {
527
+ const target = reactive([1, 2]);
528
+ const source = reactive([3, 4, 5]);
529
+
530
+ assignReactiveArray(target, source);
531
+
532
+ const unrefTarget = target.map(unref);
533
+ expect(unrefTarget).toEqual([3, 4, 5]);
534
+ });
535
+
536
+ it("should handle source array shorter than target", () => {
537
+ const target = reactive([1, 2, 3]);
538
+ const source = reactive([4]);
539
+
540
+ assignReactiveArray(target, source);
541
+
542
+ const unrefTarget = target.map(unref);
543
+ expect(unrefTarget).toEqual([4]);
544
+ });
545
+
546
+ it("should not create reactive links between array elements", () => {
547
+ const target = reactive([{ val: 1 }, { val: 2 }]);
548
+ const source = reactive([{ val: 3 }, { val: 4 }]);
549
+
550
+ assignReactiveArray(target, source);
551
+
552
+ const unrefTarget = target.map(unref);
553
+ expect(unrefTarget).toEqual([{ val: 3 }, { val: 4 }]);
554
+
555
+ // Modify source
556
+ source[0].val = 5;
557
+
558
+ // Since the elements are refs to source, target should reflect changes
559
+ expect(unref(target[0]).val).toBe(5);
560
+ });
561
+
562
+ it("should reverse the target array if it matches the reversed source", () => {
563
+ const target = reactive([1, 2, 3]);
564
+ const source = reactive([3, 2, 1]);
565
+
566
+ const didAnything = assignReactiveArray(target, source);
567
+ expect(didAnything).toBe(true);
568
+
569
+ const unrefTarget = target.map(unref);
570
+ expect(unrefTarget).toEqual([3, 2, 1]);
571
+ });
572
+
573
+ it("should handle nested arrays and objects", () => {
574
+ const target = reactive([[1], [2], [3]]);
575
+ const source = reactive([[4], [5], [6]]);
576
+
577
+ assignReactiveArray(target, source);
578
+
579
+ const unrefTarget = target.map(unref).map((item) => (Array.isArray(item) ? item.map(unref) : item));
580
+ expect(unrefTarget).toEqual([[4], [5], [6]]);
581
+ });
582
+
583
+ it("should handle nested arrays and objects with different lengths", () => {
584
+ const target = reactive([[1], [2], [3]]);
585
+ const source = reactive([[4], [5]]);
586
+
587
+ assignReactiveArray(target, source);
588
+
589
+ const unrefTarget = target.map(unref).map((item) => (Array.isArray(item) ? item.map(unref) : item));
590
+ expect(unrefTarget).toEqual([[4], [5]]);
591
+ });
592
+ });
237
593
  });
@@ -2,9 +2,10 @@ import { keyDiff } from "./keyDiff.js";
2
2
  import inspect from "browser-util-inspect";
3
3
  import isArray from "lodash-es/isArray.js";
4
4
  import isObject from "lodash-es/isObject.js";
5
- import isObjectLike from "lodash-es/isObjectLike.js";
6
5
  import { isReactive, isRef, toRef, unref } from "vue";
7
6
  import isSet from "lodash-es/isSet.js";
7
+ import isMap from "lodash-es/isMap.js";
8
+ import isPlainObject from "lodash-es/isPlainObject.js";
8
9
 
9
10
  /**
10
11
  * Reactive object assignment utilities.
@@ -100,16 +101,20 @@ function reactiveReplaceKeys(target, source, keys, exclude) {
100
101
  let didAnything = false;
101
102
  for (const key of keys) {
102
103
  if (!exclude?.includes(key)) {
104
+ const sourceValue = source[key];
105
+ const targetValue = target[key];
106
+ if (isSet(sourceValue) || isMap(sourceValue)) {
107
+ if (targetValue === sourceValue) {
108
+ continue;
109
+ }
110
+ target[key] = sourceValue;
111
+ didAnything = true;
112
+ }
103
113
  if (targetIsReactive && sourceIsReactive) {
104
114
  const targetPropRaw = unref(toRef(target, key));
105
- // if they are object like we can see if the values are the same
106
- if (isObjectLike(targetPropRaw)) {
107
- const sourcePropRaw = unref(toRef(source, key));
108
- if (isObjectLike(sourcePropRaw)) {
109
- if (targetPropRaw === sourcePropRaw) {
110
- continue;
111
- }
112
- }
115
+ const sourcePropRaw = unref(toRef(source, key));
116
+ if (targetPropRaw === sourcePropRaw) {
117
+ continue;
113
118
  }
114
119
  target[key] = toRef(source, key);
115
120
  didAnything = true;
@@ -348,28 +353,37 @@ export function assignReactiveObject(target, source, exclude) {
348
353
  * @returns {boolean} True if any keys were added, updated, or removed, false otherwise.
349
354
  */
350
355
  function recursiveInner(target, source, exclude, addedKeys, sameKeys, path, fn) {
356
+ let didAnything = false;
351
357
  const wasAdded = addReactiveObject(target, source, exclude, addedKeys);
352
- const keysForRecurse = [];
353
- const keysForReplace = [];
358
+ didAnything = didAnything || wasAdded;
354
359
  for (const key of sameKeys) {
355
360
  if (!exclude?.includes(key)) {
356
- if (isObject(source[key]) && isObject(target[key])) {
357
- keysForRecurse.push(key);
358
- } else if (target[key] !== source[key]) {
359
- keysForReplace.push(key);
361
+ const sourceValue = source[key];
362
+ const targetValue = target[key];
363
+
364
+ if (isSet(sourceValue) || isMap(sourceValue)) {
365
+ if (targetValue !== sourceValue) {
366
+ target[key] = sourceValue;
367
+ didAnything = true;
368
+ }
369
+ } else if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
370
+ const nextLevelExclude = exclude
371
+ ?.filter((excludeKey) => !excludeKey.startsWith(path))
372
+ .map((excludeKey) => excludeKey.replace(path, ""));
373
+ const nextPath = `${path}.${key}`;
374
+ const didRecurse = fn(targetValue, sourceValue, nextLevelExclude, nextPath);
375
+ didAnything = didAnything || didRecurse;
376
+ } else if (isArray(sourceValue) && isArray(targetValue)) {
377
+ const didAssignArray = assignReactiveArray(targetValue, sourceValue);
378
+ didAnything = didAnything || didAssignArray;
379
+ } else if (targetValue !== sourceValue) {
380
+ // Assign other types directly
381
+ target[key] = sourceValue;
382
+ didAnything = true;
360
383
  }
361
384
  }
362
385
  }
363
- const wasReplaced = reactiveReplaceKeys(target, source, keysForReplace, exclude);
364
- for (const key of keysForRecurse) {
365
- // scope exclude for this next level, remove keys that don't start with the current path, trim keys that do to remove the current path
366
- const nextLevelExclude = exclude
367
- ?.filter((excludeKey) => !excludeKey.startsWith(path))
368
- .map((excludeKey) => excludeKey.replace(path, ""));
369
- const nextPath = isArray(source[key]) ? `${path}[${key}]` : `${path}.${key}`;
370
- fn(target[key], source[key], nextLevelExclude, nextPath);
371
- }
372
- return wasAdded || wasReplaced;
386
+ return didAnything;
373
387
  }
374
388
 
375
389
  /**