@arrai-innovations/reactive-helpers 1.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.
- package/.circleci/config.yml +91 -0
- package/.commitlintrc.json +3 -0
- package/.editorconfig +5 -0
- package/.eslintignore +2 -0
- package/.eslintrc.js +26 -0
- package/.husky/commit-msg +2 -0
- package/.husky/pre-commit +2 -0
- package/.idea/encodings.xml +6 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +8 -0
- package/.idea/reactive-helpers.iml +15 -0
- package/.lintstagedrc +11 -0
- package/.prettierignore +4 -0
- package/.prettierrc.js +5 -0
- package/README.md +598 -0
- package/babel.config.js +15 -0
- package/index.js +2 -0
- package/jest.config.mjs +27 -0
- package/package.json +59 -0
- package/tests/unit/.eslintrc.js +10 -0
- package/tests/unit/expectHelpers.js +6 -0
- package/tests/unit/mockOnUnmounted.js +9 -0
- package/tests/unit/use/index.spec.js +18 -0
- package/tests/unit/use/listFilter.spec.js +332 -0
- package/tests/unit/use/listInstance.spec.js +424 -0
- package/tests/unit/use/listRelated.spec.js +73 -0
- package/tests/unit/use/listSort.spec.js +220 -0
- package/tests/unit/use/listSubscription.spec.js +540 -0
- package/tests/unit/use/objectInstance.spec.js +897 -0
- package/tests/unit/use/objectSubscription.spec.js +671 -0
- package/tests/unit/use/search.spec.js +67 -0
- package/tests/unit/use/watches.spec.js +252 -0
- package/tests/unit/utils/assignReactiveObject.spec.js +127 -0
- package/tests/unit/utils/index.spec.js +18 -0
- package/tests/unit/utils/keyDiff.spec.js +67 -0
- package/tests/unit/utils/set.spec.js +68 -0
- package/tests/unit/utils/unrefAndToRawDeep.spec.js +170 -0
- package/use/index.js +8 -0
- package/use/listFilter.js +157 -0
- package/use/listInstance.js +144 -0
- package/use/listRelated.js +135 -0
- package/use/listSort.js +190 -0
- package/use/listSubscription.js +143 -0
- package/use/objectInstance.js +222 -0
- package/use/objectSubscription.js +188 -0
- package/use/search.js +104 -0
- package/utils/assignReactiveObject.js +62 -0
- package/utils/index.js +5 -0
- package/utils/keyDiff.js +10 -0
- package/utils/set.js +48 -0
- package/utils/unrefAndToRawDeep.js +49 -0
- package/utils/watches.js +170 -0
package/README.md
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# reactive-helpers
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://docs.arrai-dev.com/reactive-helpers/artifacts/main/coverage_tests/)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
VueJS 3 utility composition functions to help manipulate objects and lists.
|
|
9
|
+
|
|
10
|
+
<!-- prettier-ignore-start -->
|
|
11
|
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
12
|
+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
13
|
+
|
|
14
|
+
- [Install](#install)
|
|
15
|
+
- [Usage](#usage)
|
|
16
|
+
- [Import](#import)
|
|
17
|
+
- [List](#list)
|
|
18
|
+
- [Instance](#instance)
|
|
19
|
+
- [Subscription](#subscription)
|
|
20
|
+
- [Related](#related)
|
|
21
|
+
- [Sort](#sort)
|
|
22
|
+
- [Filter](#filter)
|
|
23
|
+
- [All](#all)
|
|
24
|
+
- [Object](#object)
|
|
25
|
+
- [Search](#search)
|
|
26
|
+
- [Utils](#utils)
|
|
27
|
+
- [addOrUpdateReactiveObject & assignReactiveObject](#addorupdatereactiveobject--assignreactiveobject)
|
|
28
|
+
- [keyDiff](#keydiff)
|
|
29
|
+
- [set](#set)
|
|
30
|
+
- [watches](#watches)
|
|
31
|
+
- [Development](#development)
|
|
32
|
+
|
|
33
|
+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
34
|
+
<!-- prettier-ignore-end -->
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
$ npm install @arrai-innovations/reactive-helpers
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Import
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// base import contains all exports
|
|
48
|
+
import { useListInstance, useObjectInstance } from "@arrai-innovations/reactive-helpers";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### List
|
|
52
|
+
|
|
53
|
+
#### Instance
|
|
54
|
+
|
|
55
|
+
The container for your list of objects, providing loading or error status.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// do this in your main.js
|
|
59
|
+
import { setListInstanceCrud } from "@arrai-innovations/reactive-helpers";
|
|
60
|
+
setListInstanceCrud({
|
|
61
|
+
list: async function listCrudAdaptor({ crudArgs, retrieveArgs, listArgs, pageCallback }) {
|
|
62
|
+
// todo: your implemenation here.
|
|
63
|
+
const listOfObjects = await talkToServer(crudArgs, retrieveArgs, listArgs);
|
|
64
|
+
pageCallback(listOfObjects);
|
|
65
|
+
const nextListOfObjects = await talkToServerAgain(crudArgs, retrieveArgs, listArgs);
|
|
66
|
+
pageCallback(nextListOfObjects);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// then use in your component
|
|
71
|
+
import { useListInstance } from "@arrai-innovations/reactive-helpers";
|
|
72
|
+
const contacts = useListInstance({
|
|
73
|
+
crudArgs: {
|
|
74
|
+
stream: "contacts",
|
|
75
|
+
},
|
|
76
|
+
defaultRetrieveArgs: {
|
|
77
|
+
fields: ["id", "has_name", "lexical_name", "organization", "phone"],
|
|
78
|
+
},
|
|
79
|
+
defaultListArgs: {
|
|
80
|
+
has_organization: true,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await contacts.list();
|
|
85
|
+
console.log(contacts.loading);
|
|
86
|
+
// False
|
|
87
|
+
console.log(contacts.errored);
|
|
88
|
+
// False
|
|
89
|
+
console.log(contacts.error);
|
|
90
|
+
// null
|
|
91
|
+
console.log(contacts.objects);
|
|
92
|
+
// { contacts keyed by 'id' }
|
|
93
|
+
contacts.defaultRetrieveArgs.fields.push("message_count");
|
|
94
|
+
await contacts.list();
|
|
95
|
+
console.log(contacts.objects);
|
|
96
|
+
// { contacts keyed by 'id' with message_count }
|
|
97
|
+
await contacts.list({ listArgs: {} });
|
|
98
|
+
console.log(contacts.objects);
|
|
99
|
+
// { contacts keyed by 'id' with organizationless contacts }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Subscription
|
|
103
|
+
|
|
104
|
+
Adds functionality to a list instance to receive updates from the server.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// do this in your main.js
|
|
108
|
+
import { setListSubscriptionCrud } from "@arrai-innovations/reactive-helpers";
|
|
109
|
+
setListSubscriptionCrud({
|
|
110
|
+
subscribe: function subscribeCrudAdaptor({ crudArgs, retrieveArgs, listArgs, eventCallback }) {
|
|
111
|
+
// todo: your implemenation here.
|
|
112
|
+
const subscription = talkToServer(function (data, action) {
|
|
113
|
+
eventCallback(data, action);
|
|
114
|
+
});
|
|
115
|
+
// return a promise with a cancel action
|
|
116
|
+
subscription.cancel = async () => {
|
|
117
|
+
await cancelSubOnServer();
|
|
118
|
+
};
|
|
119
|
+
return subscription;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// then use in your component
|
|
124
|
+
import { useListInstance, useListSubscription } from "@arrai-innovations/reactive-helpers";
|
|
125
|
+
const contacts = useListInstance({
|
|
126
|
+
crudArgs: {
|
|
127
|
+
stream: "contacts",
|
|
128
|
+
},
|
|
129
|
+
defaultRetrieveArgs: {
|
|
130
|
+
fields: ["id", "has_name", "lexical_name", "organization", "phone"],
|
|
131
|
+
},
|
|
132
|
+
defaultListArgs: {
|
|
133
|
+
has_organization: true,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const contactsSubscription = useListSubscription({
|
|
137
|
+
listInstance: contacts,
|
|
138
|
+
crudArgs: {
|
|
139
|
+
stream: "contacts",
|
|
140
|
+
includeCreateEvents: true,
|
|
141
|
+
subscribeAction: "subscribe_contacts",
|
|
142
|
+
unsubscribeAction: "unsubscribe_contacts",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// only get new or updated contacts, not existing.
|
|
147
|
+
contactsSubscription.subscribe({ list: false });
|
|
148
|
+
// or, subscribe and get the existing list.
|
|
149
|
+
contactsSubscription.subscribe();
|
|
150
|
+
// stop getting updates.
|
|
151
|
+
contactsSubscription.unsubscribe();
|
|
152
|
+
// re-retreive the list of existing contacts including another field.
|
|
153
|
+
contacts.defaultRetrieveArgs.fields.push("message_count");
|
|
154
|
+
// re-retreive the list of all existing contacts.
|
|
155
|
+
delete contacts.defaultListArgs.has_organization;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Related
|
|
159
|
+
|
|
160
|
+
Lookup foreign keys between list instances via watch, for using dot notation in templates to cross object relations.
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
// no main.js setup required.
|
|
164
|
+
|
|
165
|
+
// used in example below.
|
|
166
|
+
import { nextTick } from "vue";
|
|
167
|
+
|
|
168
|
+
// use in your component
|
|
169
|
+
import { useListInstance, useListRelated } from "@arrai-innovations/reactive-helpers";
|
|
170
|
+
const organizations = useListInstance({});
|
|
171
|
+
const contacts = useListInstance({
|
|
172
|
+
defaultRetrieveArgs: {
|
|
173
|
+
fields: ["id", "lexical_name", "organization"],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const contactsRelated = useListRelated({
|
|
177
|
+
listInstance: contacts,
|
|
178
|
+
relatedObjectsRules: {
|
|
179
|
+
organization: {
|
|
180
|
+
// desired key on relatedObjects
|
|
181
|
+
objects: toRef(organizations.state, "objects"), // organizations by id
|
|
182
|
+
pkKey: "organization", // reference key on contact for org id.
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
relatedObjectsPropertyName: "myRelatedObjects",
|
|
186
|
+
});
|
|
187
|
+
await organizations.list();
|
|
188
|
+
await contacts.list();
|
|
189
|
+
console.log(organizations.objects);
|
|
190
|
+
/*
|
|
191
|
+
{
|
|
192
|
+
"24": { "id": 24, "name": "org 24" },
|
|
193
|
+
"42": { "id": 42, "name": "org 42" },
|
|
194
|
+
}
|
|
195
|
+
*/
|
|
196
|
+
console.log(contacts.objects);
|
|
197
|
+
/*
|
|
198
|
+
{
|
|
199
|
+
"15": {
|
|
200
|
+
"id": 15,
|
|
201
|
+
"lexical_name": "one, contact",
|
|
202
|
+
"organization": 42,
|
|
203
|
+
"relatedObjects": {
|
|
204
|
+
"organization": { "id": 42, "name": "org 42" }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
*/
|
|
209
|
+
contacts.objects["15"].organization = 24;
|
|
210
|
+
await nextTick();
|
|
211
|
+
console.log(contacts.objects["15"].myRelatedObjects);
|
|
212
|
+
/*
|
|
213
|
+
{
|
|
214
|
+
"organization": { "id": 24, "name": "org 24" }
|
|
215
|
+
}
|
|
216
|
+
*/
|
|
217
|
+
delete contactsRelated.relatedObjectRules.organization;
|
|
218
|
+
await nextTick();
|
|
219
|
+
console.log(contacts.objects["15"].myRelatedObjects);
|
|
220
|
+
/* {} */
|
|
221
|
+
// manual stopage, inside a setup or another effect scope, there isnt a need to manually call this.
|
|
222
|
+
contactsRelated.effectScope.stop();
|
|
223
|
+
await nextTick();
|
|
224
|
+
console.log(contacts.objects["15"].myRelatedObjects);
|
|
225
|
+
/* undefined */
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Sort
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
// no main.js setup required.
|
|
232
|
+
|
|
233
|
+
// used in example below.
|
|
234
|
+
import { nextTick } from "vue";
|
|
235
|
+
|
|
236
|
+
// use in your component
|
|
237
|
+
import { useListInstance, useListSort } from "@arrai-innovations/reactive-helpers";
|
|
238
|
+
const contacts = useListInstance({
|
|
239
|
+
defaultRetrieveArgs: {
|
|
240
|
+
fields: ["id", "has_name", "lexical_name", "organization"],
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
const contactsSort = useListSort({
|
|
244
|
+
listInstance: contacts,
|
|
245
|
+
orderByRules: [
|
|
246
|
+
{ key: "has_name", desc: true, localeCompare: false },
|
|
247
|
+
{ key: "lexical_name", desc: false, localeCompare: true },
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
await contacts.list();
|
|
251
|
+
console.log(contactsSort.state.order);
|
|
252
|
+
// array of ids in order, based on the specified rules.
|
|
253
|
+
console.log(contactsSort.state.objectsInOrder);
|
|
254
|
+
// computed array of the previous that also looks up the object ids in .objects
|
|
255
|
+
contactsSort.state.orderByRules[0].desc = false;
|
|
256
|
+
await nextTick();
|
|
257
|
+
console.log(contactsSort.state.order);
|
|
258
|
+
// array of ids in order, based on updated rules.
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Filter
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
// no main.js setup required.
|
|
265
|
+
|
|
266
|
+
// used in example below.
|
|
267
|
+
import { nextTick } from "vue";
|
|
268
|
+
|
|
269
|
+
// use in your component
|
|
270
|
+
import { useListInstance, useListFilter } from "@arrai-innovations/reactive-helpers";
|
|
271
|
+
const contacts = useListInstance({
|
|
272
|
+
defaultRetrieveArgs: {
|
|
273
|
+
fields: ["id", "has_name", "lexical_name", "organization"],
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const myAllowedValues = reactive({ 1: true, 2: true, 3: true });
|
|
277
|
+
const contactsFilter = useListFilter({
|
|
278
|
+
listInstance: contacts,
|
|
279
|
+
allowedValues: myAllowedValues,
|
|
280
|
+
allowedFilter: function (object) {
|
|
281
|
+
return object.has_name === true;
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
await contacts.list();
|
|
285
|
+
console.log(contactsFilter.state.objects);
|
|
286
|
+
// only contains the objects passing the filter
|
|
287
|
+
delete myAllowedValues[3];
|
|
288
|
+
myAllowedValues[4] = True;
|
|
289
|
+
await nextTick();
|
|
290
|
+
console.log(contactsFilter.state.objects);
|
|
291
|
+
// array of ids in order, based on updated rules.
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### All
|
|
295
|
+
|
|
296
|
+
Example using all of the above.
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
import {
|
|
300
|
+
useListInstance,
|
|
301
|
+
useListSubscription,
|
|
302
|
+
useListRelated,
|
|
303
|
+
useListFilter,
|
|
304
|
+
useListSort,
|
|
305
|
+
} from "@arrai-innovations/reactive-helpers";
|
|
306
|
+
const organizationNameSearch = ref("");
|
|
307
|
+
const organizations = useListInstance({
|
|
308
|
+
crudArgs: {
|
|
309
|
+
stream: "organizations",
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
const contacts = useListInstance({
|
|
313
|
+
crudArgs: {
|
|
314
|
+
stream: "contacts",
|
|
315
|
+
},
|
|
316
|
+
defaultRetrieveArgs: {
|
|
317
|
+
fields: ["id", "has_name", "lexical_name", "organization", "phone"],
|
|
318
|
+
},
|
|
319
|
+
defaultListArgs: {
|
|
320
|
+
has_organization: true,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const contactsSubscription = useListSubscription({
|
|
324
|
+
crudArgs: {
|
|
325
|
+
stream: "contacts",
|
|
326
|
+
includeCreateEvents: true,
|
|
327
|
+
},
|
|
328
|
+
listInstance: contacts,
|
|
329
|
+
});
|
|
330
|
+
const contactsRelated = useListRelated({
|
|
331
|
+
parentState: contactsSubscription.state,
|
|
332
|
+
relatedObjectsRules: {
|
|
333
|
+
organization: {
|
|
334
|
+
// desired key on relatedObjects
|
|
335
|
+
objects: toRef(organizations.state, "objects"), // organizations by id
|
|
336
|
+
pkKey: "organization", // reference key on contact for org id.
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
const contactsFiltered = useListFilter({
|
|
341
|
+
parentState: contactsRelated.state,
|
|
342
|
+
useTextSearch: true,
|
|
343
|
+
textSearchRules: ["relatedObjects.organization.name"],
|
|
344
|
+
textSearchValue: organizationNameSearch,
|
|
345
|
+
});
|
|
346
|
+
const contactsSorted = useListSort({
|
|
347
|
+
parentState: contactsFiltered.state,
|
|
348
|
+
orderByRules: [
|
|
349
|
+
{ key: "relatedObjects.organization.name", desc: false, localeCompare: true },
|
|
350
|
+
{ key: "lexical_name", desc: false, localeCompare: true },
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
console.log(contactsSorted.state.value.objects);
|
|
354
|
+
console.log(contactsSorted.state.value.order);
|
|
355
|
+
console.log(contactsSorted.state.value.objectsInOrder);
|
|
356
|
+
// array of contacts, updating as new ones are created, related to organization, filtered by organziation name, sort organization name & lexical name.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Object
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
import { useObjectInstance } from "@arrai-innovations/reactive-helpers";
|
|
363
|
+
const contact = useObjectInstance({});
|
|
364
|
+
// or
|
|
365
|
+
import { useObjectSubscription } from "@arrai-innovations/reactive-helpers";
|
|
366
|
+
const contact = useObjectSubscription({});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Search
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
// no main.js setup required.
|
|
373
|
+
import { useSearch } from "@arrai-innovations/reactive-helpers";
|
|
374
|
+
const search = useSearch({});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Utils
|
|
378
|
+
|
|
379
|
+
#### addOrUpdateReactiveObject & assignReactiveObject
|
|
380
|
+
|
|
381
|
+
`addOrUpdateReactiveObject` - Assigns properties of a source object onto a target object, using refs if both source and
|
|
382
|
+
target are reactive.
|
|
383
|
+
|
|
384
|
+
`assignReactiveObject` - same as `addOrUpdateReactiveObject`, but deletes keys from target that are not in source.
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
import { reactive, toRef, computed } from "vue";
|
|
388
|
+
import { assignReactiveObject, addOrUpdateReactiveObject } from "@arrai-innovations/reactive-helpers";
|
|
389
|
+
|
|
390
|
+
const target = reactive({ a: 1 });
|
|
391
|
+
const source = { a: 3, b: 4 };
|
|
392
|
+
const source2 = reactive({ b: 5 });
|
|
393
|
+
|
|
394
|
+
const a = toRef(target, "a");
|
|
395
|
+
const b = toRef(target, "b");
|
|
396
|
+
const mySum = computed(() => (a.value || 0) + (b.value || 0));
|
|
397
|
+
|
|
398
|
+
console.log(mySum.value); // 1
|
|
399
|
+
assignReactiveObject(target, source);
|
|
400
|
+
console.log(mySum.value); // 7
|
|
401
|
+
addOrUpdateReactiveObject(target, source2);
|
|
402
|
+
console.log({ ...target }); // { a: 3, b: 5 }
|
|
403
|
+
console.log(mySum.value); // 8
|
|
404
|
+
source2.b = 6;
|
|
405
|
+
console.log(mySum.value); // 9
|
|
406
|
+
assignReactiveObject(target, source2);
|
|
407
|
+
console.log({ ...target }); // { b: 6 }
|
|
408
|
+
console.log(mySum.value); // 6
|
|
409
|
+
source2.b = 10;
|
|
410
|
+
console.log(mySum.value); // 10
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### keyDiff
|
|
414
|
+
|
|
415
|
+
`keyDiff` - returns the various set results related to figuring out the changes in keys over time on an object or related objects. Returns the intersection as sameKeys, the difference of old and new as removedKeys, and the difference of new and old as addedKeys.
|
|
416
|
+
|
|
417
|
+
```js
|
|
418
|
+
import { keyDiff } from "@arrai-innovations/reactive-helpers";
|
|
419
|
+
const newKeys = Object.keys({ a: 1, b: 2 });
|
|
420
|
+
const oldKeys = Object.keys({ a: 1, c: 3 });
|
|
421
|
+
const { addedKeys, removedKeys, sameKeys } = keyDiff(newKeys, oldKeys);
|
|
422
|
+
console.log({ addedKeys, removedKeys, sameKeys });
|
|
423
|
+
// { addedKeys: Set(1) { 'b' }, removedKeys: Set(1) { 'c' }, sameKeys: Set(1) { 'a' } }
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### set
|
|
427
|
+
|
|
428
|
+
We make use of the basic set operations provided as example on mdn:
|
|
429
|
+
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#implementing_basic_set_operations
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
import { isSuperset, union, intersection, symmetricDifference, difference } from "@arrai-innovations/reactive-helpers";
|
|
433
|
+
isSuperset(new Set([1, 2, 3, 4]), new Set([1, 2, 3])); // true
|
|
434
|
+
union(new Set([1, 2, 3, 4]), new Set([1, 2, 3])); // Set { 1, 2, 3, 4 }
|
|
435
|
+
intersection(new Set([1, 2, 3, 4]), new Set([1, 2, 3])); // Set { 1, 2, 3 }
|
|
436
|
+
symmetricDifference(new Set([1, 2, 3, 4]), new Set([1, 2, 3, 5])); // Set { 4, 5 }
|
|
437
|
+
difference(new Set([1, 2, 3, 4]), new Set([1, 2, 3, 5])); // Set { 4 }
|
|
438
|
+
difference(new Set([1, 2, 3, 5]), new Set([1, 2, 3, 4])); // Set { 5 }
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### watches
|
|
442
|
+
|
|
443
|
+
`ImmediateStopWatch` - a vue.js watch that can be stopped in the first iteration
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
import { ImmediateStopWatch } from "@arrai-innovations/reactive-helpers";
|
|
447
|
+
const reactiveObject = reactive({ a: 1 });
|
|
448
|
+
const watch = new ImmediateStopWatch();
|
|
449
|
+
const watch2 = new ImmediateStopWatch();
|
|
450
|
+
watch.start(
|
|
451
|
+
() => reactiveObject.a,
|
|
452
|
+
(newA) => {
|
|
453
|
+
if (newA === 2) {
|
|
454
|
+
console.log("a is now 2!");
|
|
455
|
+
watch.stop();
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
[reactiveObject.a]
|
|
459
|
+
);
|
|
460
|
+
watch2.start(
|
|
461
|
+
() => reactiveObject.a,
|
|
462
|
+
(newA) => {
|
|
463
|
+
if (newA === 1) {
|
|
464
|
+
console.log("a is now 1!");
|
|
465
|
+
watch2.stop();
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
[reactiveObject.a]
|
|
469
|
+
);
|
|
470
|
+
// a is now 1!
|
|
471
|
+
await nextTick();
|
|
472
|
+
reactiveObject.a = 2;
|
|
473
|
+
await nextTick();
|
|
474
|
+
// a is now 2!
|
|
475
|
+
await nextTick();
|
|
476
|
+
reactiveObject.a = 1;
|
|
477
|
+
await nextTick();
|
|
478
|
+
// no output;
|
|
479
|
+
reactiveObject.a = 2;
|
|
480
|
+
await nextTick();
|
|
481
|
+
// no output;
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`AwaitTimeout` - a class that provides a promise that resolves after a timeout.
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
import { AwaitTimeout } from "@arrai-innovations/reactive-helpers";
|
|
488
|
+
const awaitTimeout = new AwaitTimeout({ timeout: 1000 });
|
|
489
|
+
awaitTimeout.start();
|
|
490
|
+
await awaitTimeout.promise; // waits 1 second
|
|
491
|
+
const awaitTimeout2 = new AwaitTimeout({ timeout: 1000 });
|
|
492
|
+
awaitTimeout2.start();
|
|
493
|
+
setTimeout(() => awaitTimeout2.stop(), 500);
|
|
494
|
+
await awaitTimeout2.promise; // waits 500 ms then rejects with new AwaitTimeoutError("Cancelled", "timeout_cancelled")
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`doAwaitTimeout` - a function that returns a promise that resolves after a timeout.
|
|
498
|
+
|
|
499
|
+
```js
|
|
500
|
+
import { doAwaitTimeout } from "@arrai-innovations/reactive-helpers";
|
|
501
|
+
// non cancellable AwaitTimeout helper
|
|
502
|
+
await doAwaitTimeout(1000); // waits 1 second
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`AwaitNot` - a class that provides a promise that resolves when a watched prop switches to true then to false.
|
|
506
|
+
|
|
507
|
+
```mermaid
|
|
508
|
+
flowchart TD
|
|
509
|
+
A["new AwaitNot"] --> B["AwaitNot.start()"];
|
|
510
|
+
B -- timeout --> G["AwaitNot.promise is rejected"]
|
|
511
|
+
B --> C{"prop is false"};
|
|
512
|
+
C -- false --> D["AwaitNot.waitForTrue()"];
|
|
513
|
+
C -- true --> H{"canAlreadyBeFalse"}
|
|
514
|
+
H -- true --> D;
|
|
515
|
+
H -- false --> E["AwaitNot.waitForFalse()"];
|
|
516
|
+
|
|
517
|
+
D -- timeout --> G
|
|
518
|
+
E -- timeout --> G
|
|
519
|
+
E -- prop is false --> F["AwaitNot.promise is resolved"]
|
|
520
|
+
D -- prop is true --> E
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
import { reactive } from "vue";
|
|
525
|
+
const reactiveObject = reactive({});
|
|
526
|
+
import { AwaitNot } from "@arrai-innovations/reactive-helpers";
|
|
527
|
+
const awaitNot = new AwaitNot({
|
|
528
|
+
obj: reactiveObject,
|
|
529
|
+
prop: "prop",
|
|
530
|
+
couldAlreadyBeFalse: true, // default
|
|
531
|
+
timeout: 1000, // default
|
|
532
|
+
});
|
|
533
|
+
awaitNot.promise
|
|
534
|
+
.then(() => {
|
|
535
|
+
console.log("resolved");
|
|
536
|
+
})
|
|
537
|
+
.catch(() => {
|
|
538
|
+
console.log("rejected");
|
|
539
|
+
});
|
|
540
|
+
awaitNot.start();
|
|
541
|
+
await nextTick();
|
|
542
|
+
reactiveObject.prop = true;
|
|
543
|
+
await nextTick();
|
|
544
|
+
reactiveObject.prop = false;
|
|
545
|
+
await nextTick();
|
|
546
|
+
// resolved
|
|
547
|
+
const awaitNot2 = new AwaitNot({
|
|
548
|
+
obj: reactiveObject,
|
|
549
|
+
prop: "prop",
|
|
550
|
+
couldAlreadyBeFalse: true, // default
|
|
551
|
+
timeout: 500,
|
|
552
|
+
});
|
|
553
|
+
awaitNot2.promise
|
|
554
|
+
.then(() => {
|
|
555
|
+
console.log("resolved");
|
|
556
|
+
})
|
|
557
|
+
.catch(() => {
|
|
558
|
+
console.log("rejected");
|
|
559
|
+
});
|
|
560
|
+
awaitNot2.start();
|
|
561
|
+
await awaitNot2.promise;
|
|
562
|
+
// rejected
|
|
563
|
+
const awaitNot3 = new AwaitNot({
|
|
564
|
+
obj: reactiveObject,
|
|
565
|
+
prop: "prop",
|
|
566
|
+
couldAlreadyBeFalse: false,
|
|
567
|
+
timeout: 1000, // default
|
|
568
|
+
});
|
|
569
|
+
awaitNot3.promise
|
|
570
|
+
.then(() => {
|
|
571
|
+
console.log("resolved");
|
|
572
|
+
})
|
|
573
|
+
.catch(() => {
|
|
574
|
+
console.log("rejected");
|
|
575
|
+
});
|
|
576
|
+
awaitNot3.start();
|
|
577
|
+
await awaitNot3.promise;
|
|
578
|
+
// resolved
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## Development
|
|
582
|
+
|
|
583
|
+
1. Checkout this repo:
|
|
584
|
+
```bash
|
|
585
|
+
$ git clone git@github.com:arrai-innovations/reactive-helpers.git
|
|
586
|
+
```
|
|
587
|
+
2. Install dependencies:
|
|
588
|
+
```bash
|
|
589
|
+
$ npm ci
|
|
590
|
+
```
|
|
591
|
+
3. Run tests via jest:
|
|
592
|
+
```bash
|
|
593
|
+
$ npm test
|
|
594
|
+
```
|
|
595
|
+
4. Run tests with coverage output:
|
|
596
|
+
```bash
|
|
597
|
+
$ npm run coverage
|
|
598
|
+
```
|
package/babel.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Only used by Jest
|
|
2
|
+
module.exports = {
|
|
3
|
+
presets: [["@babel/preset-env", { useBuiltIns: "entry", corejs: "2", targets: { node: "current" } }]],
|
|
4
|
+
plugins: [
|
|
5
|
+
function () {
|
|
6
|
+
return {
|
|
7
|
+
visitor: {
|
|
8
|
+
MetaProperty(path) {
|
|
9
|
+
path.replaceWithSourceString("process");
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
package/index.js
ADDED
package/jest.config.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path, { dirname } from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
rootDir: path.resolve(dirname(fileURLToPath(import.meta.url))),
|
|
6
|
+
clearMocks: true,
|
|
7
|
+
coverageDirectory: "coverage",
|
|
8
|
+
coverageProvider: "babel",
|
|
9
|
+
coverageReporters: ["json-summary", "html", "clover", "text"],
|
|
10
|
+
moduleFileExtensions: ["vue", "js", "json"],
|
|
11
|
+
moduleNameMapper: {
|
|
12
|
+
"@/(.*)$": "<rootDir>/src/$1",
|
|
13
|
+
"~/(.*)$": "<rootDir>/node_modules/$1",
|
|
14
|
+
},
|
|
15
|
+
testEnvironment: "jsdom",
|
|
16
|
+
testMatch: ["<rootDir>/tests/unit/**/*.spec.js"],
|
|
17
|
+
testPathIgnorePatterns: [],
|
|
18
|
+
transform: {
|
|
19
|
+
"^.+\\.vue$": "@vue/vue3-jest",
|
|
20
|
+
"^.+\\js$": "babel-jest",
|
|
21
|
+
},
|
|
22
|
+
globals: {
|
|
23
|
+
"@vue/vue3-jest": {
|
|
24
|
+
babelConfig: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arrai-innovations/reactive-helpers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "VueJS 3 utility composition functions to help manipulate objects and lists.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"directories": {
|
|
7
|
+
"use": "use",
|
|
8
|
+
"utils": "utils",
|
|
9
|
+
"tests": "tests"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"coverage": "npm run coverage:clean; npm test -- --coverage=true",
|
|
14
|
+
"coverage:clean": "rm -rf coverage",
|
|
15
|
+
"_postinstall": "if [ \"$NODE_ENV\" != \"production\" ] && [ ./node_modules/.bin/is-ci ]; then ./node_modules/.bin/husky install; fi",
|
|
16
|
+
"prepublishOnly": "pinst --disable",
|
|
17
|
+
"postpublish": "pinst --enable"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/arrai-innovations/reactive-helpers.git"
|
|
22
|
+
},
|
|
23
|
+
"author": "Arrai Innovations Inc. <developers@arrai.com>",
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/arrai-innovations/reactive-helpers/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/arrai-innovations/reactive-helpers#readme",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@arrai-innovations/commitlint-config": "^1.1.0",
|
|
31
|
+
"@babel/preset-env": "^7.17.10",
|
|
32
|
+
"@commitlint/cli": "^16.2.4",
|
|
33
|
+
"@types/jest": "^27.5.0",
|
|
34
|
+
"@vue/compiler-sfc": "^3.2.33",
|
|
35
|
+
"@vue/eslint-config-prettier": "^7.0.0",
|
|
36
|
+
"@vue/test-utils": "^2.0.0-rc.21",
|
|
37
|
+
"@vue/vue3-jest": "^27.0.0",
|
|
38
|
+
"babel-eslint": "^10.1.0",
|
|
39
|
+
"babel-jest": "^27.5.1",
|
|
40
|
+
"doctoc": "^2.1.0",
|
|
41
|
+
"eslint": "^7.32.0",
|
|
42
|
+
"eslint-plugin-jest": "^26.1.5",
|
|
43
|
+
"eslint-plugin-vue": "^8.7.1",
|
|
44
|
+
"flush-promises": "^1.0.2",
|
|
45
|
+
"lint-staged": "^11.1.2",
|
|
46
|
+
"prettier": "2.6.2"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"browser-util-inspect": "^0.2.0",
|
|
50
|
+
"flexsearch": "^0.7.21",
|
|
51
|
+
"husky": "^7.0.4",
|
|
52
|
+
"is-ci": "^3.0.1",
|
|
53
|
+
"jest": "^27.5.1",
|
|
54
|
+
"lodash": "^4.17.21",
|
|
55
|
+
"pinst": "^3.0.0",
|
|
56
|
+
"vue": "^3.2.33",
|
|
57
|
+
"vue-router": "^4.0.15"
|
|
58
|
+
}
|
|
59
|
+
}
|