@arrai-innovations/reactive-helpers 10.0.0 → 10.0.2

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/docs.md CHANGED
@@ -221,7 +221,7 @@ Logs debug messages based on the specified categories and logging rules.
221
221
 
222
222
  Get all paths from an array or object.
223
223
 
224
- ### utils/flattenPaths~flattenPaths(arrayOrObject, currentPath)
224
+ ### utils/flattenPaths~flattenPaths(arrayOrObject, options)
225
225
 
226
226
  Turn an array or object into an array of path strings. Recurses for any found arrays or objects.
227
227
 
@@ -230,10 +230,13 @@ Array indexes are wrapped in square brackets and object keys are prefixed with a
230
230
  **Kind**: inner method of [`utils/flattenPaths`]
231
231
  **Returns**: `Array.<string>` - paths
232
232
 
233
- | Param | Type | Description |
234
- | ------------- | ------------------- | -------------------------------------------------- |
235
- | arrayOrObject | `Array` \| `object` | array or object to flatten |
236
- | currentPath | `string` | current path, for recursion or as a starting point |
233
+ | Param | Type | Description |
234
+ | ------------------- | ------------------- | -------------------------------------------------- |
235
+ | arrayOrObject | `Array` \| `object` | array or object to flatten |
236
+ | options | `object` | options |
237
+ | options.currentPath | `string` | current path, for recursion or as a starting point |
238
+ | options.depth | `number` | current depth, for recursion |
239
+ | options.limit | `number` | limit the depth of recursion |
237
240
 
238
241
  ## utils/keyDiff
239
242
 
@@ -268,11 +271,15 @@ what keys are removed, and what keys are added. Keys are sourced deeply in the o
268
271
  **Kind**: inner method of [`utils/keyDiff`]
269
272
  **Returns**: `KeyDiffResult` - - the differences
270
273
 
271
- | Param | Type | Description |
272
- | ----------- | ---------------- | ------------------------------ |
273
- | newObj | `object` | the new version of the object |
274
- | oldObj | `object` | the old version of the object |
275
- | \[options\] | `KeyDiffOptions` | which differences are returned |
274
+ | Param | Type | Description |
275
+ | ----------------------- | ---------------- | -------------------------------------- |
276
+ | newObj | `object` | the new version of the object |
277
+ | oldObj | `object` | the old version of the object |
278
+ | \[options\] | `KeyDiffOptions` | which differences are returned |
279
+ | \[options.sameKeys\] | `boolean` | if true, return keys that are the same |
280
+ | \[options.removedKeys\] | `boolean` | if true, return keys that are removed |
281
+ | \[options.addedKeys\] | `boolean` | if true, return keys that are added |
282
+ | \[options.limit\] | `number` | limit the depth of recursion |
276
283
 
277
284
  ### utils/keyDiff~KeyDiffOptions
278
285
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arrai-innovations/reactive-helpers",
3
- "version": "10.0.0",
3
+ "version": "10.0.2",
4
4
  "description": "VueJS 3 utility composition functions to help manipulate objects and lists.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -45,7 +45,7 @@
45
45
  "jsdoc-to-markdown": "^8.0.0",
46
46
  "lint-staged": "^13.2.2",
47
47
  "prettier": "2.6.2",
48
- "vitest": "^0.32.2"
48
+ "vitest": "^0.34.1"
49
49
  },
50
50
  "dependencies": {
51
51
  "browser-util-inspect": "^0.2.0",
@@ -68,7 +68,7 @@ describe("use/useListSort", () => {
68
68
  expect(listSort.state.order).toEqual([]);
69
69
  expect(listSort.state.objectsInOrder).toEqual([]);
70
70
  expect(listSort.state.sortCriteria).toEqual({});
71
- expect(listSort.state.sortCriteriaWatches).toEqual({});
71
+ expect(listSort.state.sortCriteriaEffectScopes).toEqual({});
72
72
  expect(listSort.state.orderByDesc).toEqual([true, false]);
73
73
  });
74
74
  describe("addSortCriteria and removeSortCriteria", () => {
@@ -15,6 +15,10 @@ describe("utils/flattenPaths", () => {
15
15
  it("with a single level and a nested object and array", () => {
16
16
  expect(flattenPaths({ a: 1, b: { c: [2] } })).toEqual(["a", "b.c[0]"]);
17
17
  });
18
+ it("should limit to a specific depth", () => {
19
+ expect(flattenPaths({ a: 1, b: { c: [2] } }, { limit: 1 })).toEqual(["a"]);
20
+ expect(flattenPaths({ a: 1, b: { c: 1, d: [2], e: { f: 3 } } }, { limit: 2 })).toEqual(["a", "b.c"]);
21
+ });
18
22
  });
19
23
  describe("should work on arrays as the base", () => {
20
24
  it("with a single level", () => {
@@ -29,6 +33,10 @@ describe("utils/flattenPaths", () => {
29
33
  it("with a single level and a nested object and array", () => {
30
34
  expect(flattenPaths([1, { c: [2] }])).toEqual(["[0]", "[1].c[0]"]);
31
35
  });
36
+ it("should limit to a specific depth", () => {
37
+ expect(flattenPaths([1, { c: [2] }], { limit: 1 })).toEqual(["[0]"]);
38
+ expect(flattenPaths([1, [1, [2, [3], 4], 5]], { limit: 2 })).toEqual(["[0]", "[1][0]", "[1][2]"]);
39
+ });
32
40
  });
33
41
  it("should work on a real world example", () => {
34
42
  const toBeFlattened = {
@@ -1,4 +1,4 @@
1
- import { keyDiff } from "../../../utils/keyDiff.js";
1
+ import { keyDiff, keyDiffDeep } from "../../../utils/keyDiff.js";
2
2
 
3
3
  describe("keyDiff", function () {
4
4
  describe("should return the difference between two arrays of object keys", function () {
@@ -64,4 +64,47 @@ describe("keyDiff", function () {
64
64
  expect(removedKeys).toEqual(new Set(["c"]));
65
65
  expect(sameKeys).toEqual(new Set(["a"]));
66
66
  });
67
+ describe("keyDiffDeep", function () {
68
+ it("should return the difference between two objects", function () {
69
+ const newObj = {
70
+ a: 1,
71
+ b: {
72
+ c: 2,
73
+ },
74
+ c: 3,
75
+ };
76
+ const oldObj = {
77
+ a: 1,
78
+ b: 2,
79
+ c: 3,
80
+ };
81
+ // if using to patch, be sure to remove keys before adding them
82
+ expect(keyDiffDeep(newObj, oldObj)).toEqual({
83
+ addedKeys: new Set(["b.c"]),
84
+ removedKeys: new Set(["b"]),
85
+ sameKeys: new Set(["a", "c"]),
86
+ });
87
+ });
88
+ it("should be able to limit the depth", function () {
89
+ const newObj = {
90
+ a: 1,
91
+ b: {
92
+ c: { d: 1, e: 2, f: 3 },
93
+ },
94
+ g: 3,
95
+ };
96
+ const oldObj = {
97
+ a: 1,
98
+ b: {
99
+ c: {},
100
+ },
101
+ g: 3,
102
+ };
103
+ expect(keyDiffDeep(newObj, oldObj, { limit: 1 })).toEqual({
104
+ addedKeys: new Set(),
105
+ removedKeys: new Set(),
106
+ sameKeys: new Set(["a", "g"]),
107
+ });
108
+ });
109
+ });
67
110
  });
package/use/list.js CHANGED
@@ -16,7 +16,7 @@ export const useLists = (listArgs) => {
16
16
  };
17
17
 
18
18
  // the big brother of useObject, managing a chain of useList* instances.
19
- export const useList = ({ props, functions, paged = false, keepOldPages = false }) => {
19
+ export const useList = ({ props, functions = {}, paged = false, keepOldPages = false, useTextSearch = false }) => {
20
20
  const managed = shallowReactive({
21
21
  listInstance: null,
22
22
  listSubscription: null,
@@ -58,7 +58,7 @@ export const useList = ({ props, functions, paged = false, keepOldPages = false
58
58
 
59
59
  managed.listFilter = useListFilter({
60
60
  parentState: managed.listCalculated.state,
61
- useTextSearch: toRef(props, "useTextSearch"),
61
+ useTextSearch,
62
62
  textSearchRules: toRef(props, "textSearchRules"),
63
63
  textSearchValue: toRef(props, "textSearchValue"),
64
64
  allowedValues: toRef(props, "allowedValues"),
@@ -4,8 +4,10 @@ import { listInstanceStateKeys } from "./listInstance.js";
4
4
  import { listSubscriptionStateKeys } from "./listSubscription.js";
5
5
  import { useWatchesRunning } from "./watchesRunning.js";
6
6
  import get from "lodash-es/get.js";
7
+ import identity from "lodash-es/identity.js";
7
8
  import isArray from "lodash-es/isArray.js";
8
9
  import isEmpty from "lodash-es/isEmpty.js";
10
+ import isEqual from "lodash-es/isEqual.js";
9
11
  import isUndefined from "lodash-es/isUndefined.js";
10
12
  import { computed, effectScope, onScopeDispose, reactive, toRef, unref, watch } from "vue";
11
13
 
@@ -55,7 +57,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
55
57
  const relatedObjectsRulesIsEmpty = !state.relatedObjectsRules || isEmpty(state.relatedObjectsRules);
56
58
  for (const objectKey of Object.keys(state.relatedObjects)) {
57
59
  const relatedObjectsObject = state.relatedObjects[objectKey];
58
- const originalObject = parentState.objects[objectKey];
60
+ const originalObjectRef = toRef(parentState.objects, objectKey);
59
61
  let removedRuleKeys, addedRuleKeys;
60
62
  if (!relatedObjectsRulesIsEmpty) {
61
63
  ({ removedKeys: removedRuleKeys, addedKeys: addedRuleKeys } = keyDiff(
@@ -77,22 +79,48 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
77
79
  }
78
80
  relatedObjectsEffectScopes[objectKey].run(() => {
79
81
  for (const addedRuleKey of addedRuleKeys) {
80
- relatedObjectsObject[addedRuleKey] = computed(() => {
82
+ relatedObjectsObject[addedRuleKey] = undefined;
83
+ const relatedObjectsObjectWatchFn = () => {
81
84
  // deal with computed objects being passed.
82
85
  const ruleObjects = unref(state.relatedObjectsRules?.[addedRuleKey]?.objects);
83
86
  const rulePkKey = state.relatedObjectsRules?.[addedRuleKey]?.pkKey || addedRuleKey;
87
+ const ruleOrder = unref(state.relatedObjectsRules?.[addedRuleKey]?.order);
84
88
  if (!ruleObjects || !rulePkKey) {
85
- return undefined;
89
+ relatedObjectsObject[addedRuleKey] = undefined;
90
+ return;
86
91
  }
87
- const value = get(originalObject, rulePkKey);
92
+ let value = get(unref(originalObjectRef), rulePkKey);
88
93
  if (isUndefined(value)) {
89
- return undefined;
94
+ relatedObjectsObject[addedRuleKey] = undefined;
95
+ return;
90
96
  }
91
97
  if (isArray(value)) {
92
- return value.map((e) => ruleObjects[e]);
98
+ // the related list could be sorted differently than the original list.
99
+ if (ruleOrder?.length) {
100
+ value = value.filter(identity);
101
+ const indexById = Object.fromEntries(ruleOrder.map((e, i) => [e, i]));
102
+ value.sort((a, b) => {
103
+ const aIndex = indexById[a];
104
+ const bIndex = indexById[b];
105
+ return aIndex - bIndex;
106
+ });
107
+ }
108
+ value = value.map((e) => ruleObjects[e]).filter(identity);
109
+ } else {
110
+ value = ruleObjects[value];
93
111
  }
94
- return ruleObjects[value];
95
- });
112
+ if (!isEqual(value, relatedObjectsObject[addedRuleKey])) {
113
+ relatedObjectsObject[addedRuleKey] = value;
114
+ }
115
+ };
116
+ watch(
117
+ [toRef(state.relatedObjectsRules, addedRuleKey), originalObjectRef],
118
+ relatedObjectsObjectWatchFn,
119
+ {
120
+ deep: true,
121
+ immediate: true,
122
+ }
123
+ );
96
124
  }
97
125
  });
98
126
  }
package/use/listSort.js CHANGED
@@ -10,17 +10,16 @@ import identity from "lodash-es/identity.js";
10
10
  import isEmpty from "lodash-es/isEmpty.js";
11
11
  import isNull from "lodash-es/isNull.js";
12
12
  import isUndefined from "lodash-es/isUndefined.js";
13
- import partial from "lodash-es/partial.js";
14
13
  import throttle from "lodash-es/throttle.js";
15
14
  import zip from "lodash-es/zip.js";
16
- import { effectScope, onScopeDispose, reactive, toRef, unref, watch } from "vue";
15
+ import { effectScope, reactive, toRef, unref, watch } from "vue";
17
16
 
18
17
  export const listSortStateKeys = [
19
18
  "orderByRules",
20
19
  // "order",
21
20
  // "objectsInOrder",
22
21
  "sortCriteria",
23
- "sortCriteriaWatches",
22
+ "sortCriteriaEffectScopes",
24
23
  "orderByDesc",
25
24
  ];
26
25
 
@@ -55,42 +54,43 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
55
54
  order: [],
56
55
  objectsInOrder: [],
57
56
  sortCriteria: {},
58
- sortCriteriaWatches: {},
57
+ sortCriteriaEffectScopes: {},
59
58
  orderByDesc: [],
60
59
  });
61
60
  const es = effectScope();
62
61
 
63
62
  function removeSortCriteria(removedKey) {
64
- const stopWatches = state.sortCriteriaWatches[removedKey] || [];
65
- let stopWatch = stopWatches.pop();
66
- while (stopWatch) {
67
- stopWatch();
68
- stopWatch = stopWatches.pop();
63
+ const oldScope = state.sortCriteriaEffectScopes[removedKey];
64
+ if (oldScope) {
65
+ oldScope.stop();
66
+ delete state.sortCriteriaEffectScopes[removedKey];
69
67
  }
70
- delete state.sortCriteriaWatches[removedKey];
71
68
  delete state.sortCriteria[removedKey];
72
69
  }
73
70
 
74
71
  function addSortCriteria(object, key) {
75
- const oldStopWatches = state.sortCriteriaWatches[key] || [];
76
- let stopWatch = oldStopWatches.pop();
77
- while (stopWatch) {
78
- stopWatch();
79
- stopWatch = oldStopWatches.pop();
72
+ const oldScope = state.sortCriteriaEffectScopes[key];
73
+ if (oldScope) {
74
+ oldScope.stop();
80
75
  }
81
- const stopWatches = [];
82
- if (!state.sortCriteria[key]) {
83
- state.sortCriteria[key] = [];
84
- }
85
- stopWatches.push(
76
+ const newScope = effectScope();
77
+ newScope.run(() => {
78
+ if (!state.sortCriteria[key]) {
79
+ state.sortCriteria[key] = [];
80
+ }
86
81
  watch(
87
- [object, state.orderByRules],
82
+ [object, toRef(state, "orderByRules")],
88
83
  () => {
84
+ const obj = unref(object);
89
85
  const newSearchCriteria = [];
90
86
  for (const orderByObj of state.orderByRules.filter(identity)) {
91
- const obo = unref(orderByObj);
92
- const getter = obo.keyFn ? obo.keyFn : partial(get, partial.placeholder, obo.key);
93
- newSearchCriteria.push(getter(object));
87
+ let newItem;
88
+ if (orderByObj.keyFn) {
89
+ newItem = orderByObj.keyFn(obj, state);
90
+ } else {
91
+ newItem = get(obj, orderByObj.key);
92
+ }
93
+ newSearchCriteria.push(newItem);
94
94
  }
95
95
  assignReactiveObject(state.sortCriteria[key], newSearchCriteria);
96
96
  },
@@ -98,9 +98,9 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
98
98
  deep: true,
99
99
  immediate: true,
100
100
  }
101
- )
102
- );
103
- state.sortCriteriaWatches[key] = stopWatches;
101
+ );
102
+ });
103
+ state.sortCriteriaEffectScopes[key] = newScope;
104
104
  }
105
105
 
106
106
  function sortCriteriaWatch() {
@@ -110,6 +110,8 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
110
110
  removeSortCriteria(removedKey);
111
111
  }
112
112
  }
113
+ assignReactiveObject(state.order, cloneDeep(parentState.order));
114
+ assignReactiveObject(state.objectsInOrder, cloneDeep(parentState.objectsInOrder));
113
115
  return;
114
116
  }
115
117
  const { removedKeys, addedKeys } = keyDiff(Object.keys(parentState.objects), Object.keys(state.sortCriteria));
@@ -117,10 +119,12 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
117
119
  removeSortCriteria(removedKey);
118
120
  }
119
121
 
120
- for (const addedKey of addedKeys) {
121
- const object = parentState.objects[addedKey];
122
- addSortCriteria(object, addedKey);
123
- }
122
+ es.run(() => {
123
+ for (const addedKey of addedKeys) {
124
+ const object = toRef(() => parentState.objects[addedKey]);
125
+ addSortCriteria(object, addedKey);
126
+ }
127
+ });
124
128
  assignReactiveObject(
125
129
  state.orderByDesc,
126
130
  state.orderByRules.filter(identity).map((e) => e.desc || false)
@@ -129,8 +133,8 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
129
133
 
130
134
  function sortWatch() {
131
135
  if (!state.orderByRules || !state.orderByRules.length) {
132
- assignReactiveObject(state.order, Object.keys(parentState.objects));
133
- assignReactiveObject(state.objectsInOrder, Object.values(parentState.objects));
136
+ assignReactiveObject(state.order, cloneDeep(parentState.order));
137
+ assignReactiveObject(state.objectsInOrder, cloneDeep(parentState.objectsInOrder));
134
138
  return;
135
139
  }
136
140
 
@@ -197,20 +201,14 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
197
201
  }
198
202
  // we do not need two immediate watches to the same function.
199
203
  watch(() => Object.keys(parentState.objects), sortCriteriaWatch);
200
- watch(() => cloneDeep(state.orderByRules), sortCriteriaWatch, {
204
+ watch(toRef(state, "orderByRules"), sortCriteriaWatch, {
201
205
  deep: true,
202
206
  immediate: true,
203
207
  });
204
208
 
205
- // watching parentState.order triggers some out of order `computed`s, now that listInstance.order is a computed.
206
- watch([toRef(state, "orderByDesc"), () => state.sortCriteria], throttledSortWatch, {
209
+ watch([toRef(state, "orderByDesc"), toRef(state, "sortCriteria")], throttledSortWatch, {
207
210
  deep: true,
208
211
  });
209
- onScopeDispose(() => {
210
- Object.keys(state.sortCriteriaWatches).forEach((key) => {
211
- removeSortCriteria(key);
212
- });
213
- });
214
212
  });
215
213
 
216
214
  return {
@@ -230,7 +230,7 @@ export function useListSubscription({ listInstance, props, functions, keepOldPag
230
230
  state.subscriptionErrored = true;
231
231
  state.subscriptionError = err;
232
232
  });
233
- catchPromise.cancel = subscribePromise.cancel;
233
+ catchPromise.cancel = subscribePromise.cancel.bind(subscribePromise);
234
234
  return catchPromise;
235
235
  },
236
236
  watchArguments: reactive({
@@ -12,10 +12,13 @@ import isObject from "lodash-es/isObject.js";
12
12
  * Array indexes are wrapped in square brackets and object keys are prefixed with a period.
13
13
  * @function flattenPaths
14
14
  * @param {Array | object} arrayOrObject array or object to flatten
15
- * @param {string} currentPath current path, for recursion or as a starting point
15
+ * @param {object} options options
16
+ * @param {string} options.currentPath current path, for recursion or as a starting point
17
+ * @param {number} options.depth current depth, for recursion
18
+ * @param {number} options.limit limit the depth of recursion
16
19
  * @returns {string[]} paths
17
20
  */
18
- export function flattenPaths(arrayOrObject, currentPath = "") {
21
+ export function flattenPaths(arrayOrObject, { currentPath = "", depth = 0, limit = 0 } = {}) {
19
22
  // arrayOrObject keys or indexes values can be objects or arrays.
20
23
  // find all paths you could use lodash to "get()" to.
21
24
  // indexes use `[${index}]`, keys use `.${key}`
@@ -23,11 +26,16 @@ export function flattenPaths(arrayOrObject, currentPath = "") {
23
26
  const paths = [];
24
27
  const keysOrIndexes = isArray(arrayOrObject);
25
28
  const dotOrNot = currentPath ? "." : "";
29
+ if (limit && depth >= limit) {
30
+ return paths;
31
+ }
26
32
  if (isObject(arrayOrObject)) {
27
33
  for (const [key, value] of Object.entries(arrayOrObject)) {
28
34
  const keyPath = keysOrIndexes ? `[${key}]` : `${dotOrNot}${key}`;
29
35
  if (isObject(value) || isArray(value)) {
30
- paths.push(...flattenPaths(value, `${currentPath}${keyPath}`));
36
+ paths.push(
37
+ ...flattenPaths(value, { currentPath: `${currentPath}${keyPath}`, depth: depth + 1, limit })
38
+ );
31
39
  } else {
32
40
  paths.push(`${currentPath}${keyPath}`);
33
41
  }
package/utils/keyDiff.js CHANGED
@@ -55,11 +55,18 @@ export function keyDiff(newKeys, oldKeys, { sameKeys = true, removedKeys = true,
55
55
  * @param {object} newObj - the new version of the object
56
56
  * @param {object} oldObj - the old version of the object
57
57
  * @param {KeyDiffOptions} [options] - which differences are returned
58
+ * @param {boolean} [options.sameKeys] - if true, return keys that are the same
59
+ * @param {boolean} [options.removedKeys] - if true, return keys that are removed
60
+ * @param {boolean} [options.addedKeys] - if true, return keys that are added
61
+ * @param {number} [options.limit] - limit the depth of recursion
58
62
  * @returns {KeyDiffResult} - the differences
59
63
  */
60
64
  export function keyDiffDeep(newObj, oldObj, options = {}) {
61
- const newPaths = flattenPaths(newObj);
62
- const oldPaths = flattenPaths(oldObj);
63
- const result = keyDiff(newPaths, oldPaths, options);
64
- return result;
65
+ const additionalFlattenArgs = [];
66
+ if (options.limit) {
67
+ additionalFlattenArgs.push({ limit: options.limit });
68
+ }
69
+ const newPaths = flattenPaths(newObj, ...additionalFlattenArgs);
70
+ const oldPaths = flattenPaths(oldObj, ...additionalFlattenArgs);
71
+ return keyDiff(newPaths, oldPaths, options);
65
72
  }