@genesislcap/foundation-store 14.117.0 → 14.118.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. package/README.md +94 -36
  2. package/dist/dts/__test__/elements.d.ts +701 -0
  3. package/dist/dts/__test__/elements.d.ts.map +1 -0
  4. package/dist/dts/__test__/index.d.ts +5 -0
  5. package/dist/dts/__test__/index.d.ts.map +1 -0
  6. package/dist/dts/__test__/services.d.ts +40 -0
  7. package/dist/dts/__test__/services.d.ts.map +1 -0
  8. package/dist/dts/__test__/store.d.ts +145 -0
  9. package/dist/dts/__test__/store.d.ts.map +1 -0
  10. package/dist/dts/__test__/types.d.ts +15 -0
  11. package/dist/dts/__test__/types.d.ts.map +1 -0
  12. package/dist/dts/store/errorMap.d.ts +3 -0
  13. package/dist/dts/store/errorMap.d.ts.map +1 -1
  14. package/dist/dts/store/foundationStore.d.ts +108 -12
  15. package/dist/dts/store/foundationStore.d.ts.map +1 -1
  16. package/dist/esm/__test__/elements.js +118 -0
  17. package/dist/esm/__test__/index.js +4 -0
  18. package/dist/esm/__test__/services.js +37 -0
  19. package/dist/esm/__test__/store.js +154 -0
  20. package/dist/esm/__test__/types.js +1 -0
  21. package/dist/esm/store/foundationStore.js +145 -40
  22. package/dist/foundation-store.api.json +174 -13
  23. package/dist/foundation-store.d.ts +113 -12
  24. package/docs/api/foundation-store.abstractstore.createasynclistener.md +35 -1
  25. package/docs/api/foundation-store.abstractstore.createerrorlistener.md +1 -1
  26. package/docs/api/foundation-store.abstractstore.createlistener.md +1 -1
  27. package/docs/api/foundation-store.abstractstore.invokeasyncapi.md +46 -0
  28. package/docs/api/foundation-store.abstractstore.md +4 -3
  29. package/docs/api/foundation-store.errordetailmap.md +4 -0
  30. package/docs/api/foundation-store.errormap.md +4 -0
  31. package/docs/api/foundation-store.errormaplogger.md +4 -0
  32. package/docs/api/foundation-store.registerstore.md +1 -1
  33. package/docs/api-report.md +27 -6
  34. package/package.json +11 -7
@@ -0,0 +1,154 @@
1
+ import { __awaiter, __decorate, __param } from "tslib";
2
+ import { observable, volatile } from '@microsoft/fast-element';
3
+ import { DI } from '@microsoft/fast-foundation';
4
+ import { classNames } from '@microsoft/fast-web-utilities';
5
+ import { AbstractStore, AbstractStoreRoot, } from '../store';
6
+ import { ReportService, SubService } from './services';
7
+ /**
8
+ * @internal
9
+ */
10
+ let DefaultSubStore = class DefaultSubStore extends AbstractStore {
11
+ constructor(service) {
12
+ super();
13
+ this.service = service;
14
+ this.x = '';
15
+ this.y = 0;
16
+ this.z = 0;
17
+ this.onXChanged = this.createListener('x-changed', (detail) => (this.commit.x = detail));
18
+ this.onYChanged = this.createListener('y-changed', (detail) => (this.commit.y = detail));
19
+ this.onZChanged = this.createListener('z-changed', (detail) => (this.commit.z = detail));
20
+ this.onSelectSubEntity = this.createListener('select-sub-entity', (detail) => (this.commit.selectedEntity = detail));
21
+ this.onRemoveSubEntity = this.createListener('remove-sub-entity', (detail) => {
22
+ if (this.entities === undefined || this.entities.length === 0) {
23
+ return;
24
+ }
25
+ this.commit.entities = this.entities.filter(entity => entity !== detail);
26
+ });
27
+ /**
28
+ * Some serialised store state.
29
+ */
30
+ this.toEntity = () => ({
31
+ label: this.x,
32
+ quantity: this.derived,
33
+ });
34
+ /**
35
+ * Listeners not on the public interface can be created anonymously.
36
+ */
37
+ this.createAsyncListener('sub-load', (type) => __awaiter(this, void 0, void 0, function* () {
38
+ return this.invokeAsyncAPI(() => __awaiter(this, void 0, void 0, function* () {
39
+ this.commit.lastChangedProperty = type;
40
+ return this.service.list();
41
+ }), 'sub-load-error', 'sub-load-success');
42
+ }));
43
+ this.createListener('sub-load-success', entities => this.commit.entities = entities);
44
+ this.createErrorListener('sub-load-error', () => this.commit.entities = undefined);
45
+ /**
46
+ * Stores support creating multiple listeners for the same key, so we can piggyback on events to drive other logic.
47
+ */
48
+ this.createListener([
49
+ 'y-changed',
50
+ 'z-changed',
51
+ ], (_, { type }) => this.emit('sub-load', type));
52
+ }
53
+ /**
54
+ * Does not need any special code as it's computing based on properties that are already observable.
55
+ */
56
+ get derived() {
57
+ return this.y + this.z;
58
+ }
59
+ };
60
+ __decorate([
61
+ observable
62
+ ], DefaultSubStore.prototype, "x", void 0);
63
+ __decorate([
64
+ observable
65
+ ], DefaultSubStore.prototype, "y", void 0);
66
+ __decorate([
67
+ observable
68
+ ], DefaultSubStore.prototype, "z", void 0);
69
+ __decorate([
70
+ observable
71
+ ], DefaultSubStore.prototype, "lastChangedProperty", void 0);
72
+ __decorate([
73
+ observable
74
+ ], DefaultSubStore.prototype, "entities", void 0);
75
+ __decorate([
76
+ observable
77
+ ], DefaultSubStore.prototype, "selectedEntity", void 0);
78
+ DefaultSubStore = __decorate([
79
+ __param(0, SubService)
80
+ ], DefaultSubStore);
81
+ export { DefaultSubStore };
82
+ /**
83
+ * Registering as `transient` with the DI directly instead of via `registerStore` given test suite create/destroy cycle.
84
+ * Normally store fragments are singletons, use `registerStore` in 99% of cases.
85
+ * @internal
86
+ */
87
+ export const SubStore = DI.createInterface(x => x.transient(DefaultSubStore));
88
+ /**
89
+ * @internal
90
+ */
91
+ let DefaultTestStore = class DefaultTestStore extends AbstractStoreRoot {
92
+ constructor(subStore, service) {
93
+ /**
94
+ * Connect the sub store node to the tree by passing it.
95
+ */
96
+ super(subStore);
97
+ this.subStore = subStore;
98
+ this.service = service;
99
+ this.a = '';
100
+ this.b = 0;
101
+ this.c = 0;
102
+ this.onAChanged = this.createListener('a-changed', (detail) => (this.commit.a = detail));
103
+ this.onBChanged = this.createListener('b-changed', (detail) => (this.commit.b = detail));
104
+ this.onCChanged = this.createListener('c-changed', (detail) => (this.commit.c = detail));
105
+ /**
106
+ * Listeners not on the public interface can be created anonymously.
107
+ */
108
+ this.createAsyncListener('post-report', (detail) => __awaiter(this, void 0, void 0, function* () {
109
+ yield this.service.send(detail);
110
+ }));
111
+ /**
112
+ * Stores support creating multiple listeners for the same key, so we can piggyback on events to drive other logic.
113
+ */
114
+ this.createListener([
115
+ 'a-changed',
116
+ 'b-changed',
117
+ ], (_, { type, detail }) => this.emit('post-report', { trigger: type, value: detail }));
118
+ }
119
+ /**
120
+ * Marked as volatile as it contains branching code paths
121
+ */
122
+ get volatileDerived() {
123
+ return this.a === 'multiply' ? this.b * this.c : this.b + this.c;
124
+ }
125
+ /**
126
+ * Generates css classnames based on store state
127
+ */
128
+ get classNames() {
129
+ return classNames(['warning', this.volatileDerived > 100]);
130
+ }
131
+ };
132
+ __decorate([
133
+ observable
134
+ ], DefaultTestStore.prototype, "a", void 0);
135
+ __decorate([
136
+ observable
137
+ ], DefaultTestStore.prototype, "b", void 0);
138
+ __decorate([
139
+ observable
140
+ ], DefaultTestStore.prototype, "c", void 0);
141
+ __decorate([
142
+ volatile
143
+ ], DefaultTestStore.prototype, "volatileDerived", null);
144
+ DefaultTestStore = __decorate([
145
+ __param(0, SubStore),
146
+ __param(1, ReportService)
147
+ ], DefaultTestStore);
148
+ export { DefaultTestStore };
149
+ /**
150
+ * Registering as `transient` with the DI directly instead of via `registerStore` given test suite create/destroy cycle.
151
+ * Normally store fragments are singletons, use `registerStore` in 99% of cases.
152
+ * @internal
153
+ */
154
+ export const TestStore = DI.createInterface(x => x.transient(DefaultTestStore));
@@ -0,0 +1 @@
1
+ export {};
@@ -17,6 +17,15 @@ const getBindArgs = (...args) => {
17
17
  }
18
18
  return binding;
19
19
  };
20
+ /**
21
+ * @internal
22
+ */
23
+ export var EventListenerType;
24
+ (function (EventListenerType) {
25
+ EventListenerType["sync"] = "sync";
26
+ EventListenerType["async"] = "async";
27
+ EventListenerType["error"] = "error";
28
+ })(EventListenerType || (EventListenerType = {}));
20
29
  /**
21
30
  * Creates a dependency injection key for the store being registered.
22
31
  * @param Base - The store fragment class.
@@ -25,7 +34,7 @@ const getBindArgs = (...args) => {
25
34
  *
26
35
  * @example
27
36
  * ```ts
28
- * export const TradeEntry = registerStore(DefaultTradeEntry, 'TradeEntry');
37
+ * export const TradeEntry = registerStore<TradeEntry>(DefaultTradeEntry, 'TradeEntry');
29
38
  * ```
30
39
  *
31
40
  * @public
@@ -64,6 +73,10 @@ export class AbstractStore {
64
73
  /**
65
74
  * The store fragment's event listener map.
66
75
  *
76
+ * @remarks
77
+ * Keyed by listener to allow events to have multiple listeners for cleaner logic flows.
78
+ * See multiple event keys example in `createAsyncListener`.
79
+ *
67
80
  * @internal
68
81
  */
69
82
  this.eventListenerMap = new Map();
@@ -112,10 +125,64 @@ export class AbstractStore {
112
125
  this.commit = new Proxy(this, this.commitHandler);
113
126
  /** {@inheritDoc Store.errors} */
114
127
  this.errors = createErrorMap(logger.error);
128
+ /**
129
+ * Creates an event listener by type.
130
+ *
131
+ * @typeParam TDetail - The CustomEvent detail.
132
+ * @param type - The type of event listener to create. See {@link EventListenerType}.
133
+ * @param keys - The event key or keys from the store fragment's event detail map.
134
+ * @param token - The function handling the event.
135
+ * @returns The event listener when one key is passed or undefined.
136
+ *
137
+ * @internal
138
+ */
139
+ this.createListenerType = (type, keys, token) => {
140
+ let listener;
141
+ const listenerKeys = Array.isArray(keys) ? keys : [keys];
142
+ listenerKeys.forEach((key) => {
143
+ listener = this.createListenerEnvelope(type, token);
144
+ this.eventListenerMap.set(listener, key);
145
+ });
146
+ return listenerKeys.length === 1 ? listener : undefined;
147
+ };
148
+ /**
149
+ * Creates an event listener envelope.
150
+ *
151
+ * @remarks
152
+ * These wrap the provided token.
153
+ *
154
+ * @typeParam TDetail - The CustomEvent detail.
155
+ * @param listenerType - The type of event listener to create. See {@link EventListenerType}.
156
+ * @param token - The function handling the event.
157
+ * @returns An event listener.
158
+ *
159
+ * @internal
160
+ */
161
+ this.createListenerEnvelope = (listenerType, token) => {
162
+ if (listenerType === EventListenerType.async) {
163
+ return (event) => __awaiter(this, void 0, void 0, function* () {
164
+ const { detail, type } = event;
165
+ this.logEvent(type, true);
166
+ return token(detail, event);
167
+ });
168
+ }
169
+ return (event) => {
170
+ const { detail, type } = event;
171
+ this.logEvent(type);
172
+ if (listenerType === EventListenerType.error) {
173
+ this.errors.set(type, detail);
174
+ }
175
+ return token && token(detail, event);
176
+ };
177
+ };
115
178
  /**
116
179
  * Creates an event listener.
180
+ *
181
+ * @remarks
182
+ * See the `createAsyncListener` for usage examples.
183
+ *
117
184
  * @typeParam TDetail - The CustomEvent detail.
118
- * @param key - The event key from the store fragment's event detail map.
185
+ * @param keys - The event key or keys from the store fragment's event detail map.
119
186
  * @param token - The function handling the event.
120
187
  * @returns The event listener.
121
188
  *
@@ -124,17 +191,42 @@ export class AbstractStore {
124
191
  * You can think of this like a `reducer` in the redux sense. You are allowed to commit values to the store in these
125
192
  * synchronous handlers.
126
193
  */
127
- this.createListener = (key, token) => {
128
- const listener = ({ detail }) => {
129
- this.logEvent(key);
130
- return token(detail);
131
- };
132
- return this.mappedListener(key, listener);
133
- };
194
+ this.createListener = (keys, token) => this.createListenerType(EventListenerType.sync, keys, token);
134
195
  /**
135
196
  * Creates an async event listener.
197
+ *
198
+ * @example
199
+ * Creating an interface defined handler for a single event key.
200
+ * ```ts
201
+ * onDomainAction = this.createAsyncListener<DomainActionDetail>(
202
+ * 'domain-action',
203
+ * async ({ id, message }) =>
204
+ * this.invokeAsyncAPI(
205
+ * async () => this.domainService.action(id, message),
206
+ * 'domain-action-error',
207
+ * 'domain-action-success'
208
+ * )
209
+ * );
210
+ * ```
211
+ *
212
+ * @example
213
+ * Creating an anonymous handler in the constructor for multiple event keys.
214
+ * ```ts
215
+ * this.createAsyncListener(
216
+ * [
217
+ * 'columns-changed',
218
+ * 'types-changed',
219
+ * 'max-rows-changed',
220
+ * 'max-view-changed',
221
+ * 'order-by-changed',
222
+ * 'reverse-changed',
223
+ * ],
224
+ * async (_, { type }) => this.emit('domain-load')
225
+ * );
226
+ * ```
227
+ *
136
228
  * @typeParam TDetail - The CustomEvent detail.
137
- * @param key - The event key from the store fragment's event detail map.
229
+ * @param keys - The event key or keys from the store fragment's event detail map.
138
230
  * @param token - The async function handling the event.
139
231
  * @returns The event listener.
140
232
  *
@@ -143,17 +235,11 @@ export class AbstractStore {
143
235
  * You can think of this like an `effect` in the redux sense. You should not commit values to the store in these,
144
236
  * instead raise subsequent events to be handled synchronously, where commits are allowed.
145
237
  */
146
- this.createAsyncListener = (key, token) => {
147
- const listener = ({ detail }) => __awaiter(this, void 0, void 0, function* () {
148
- this.logEvent(key, true);
149
- return token(detail);
150
- });
151
- return this.mappedListener(key, listener);
152
- };
238
+ this.createAsyncListener = (keys, token) => this.createListenerType(EventListenerType.async, keys, token);
153
239
  /**
154
240
  * Creates an error event listener.
155
241
  * @typeParam TDetail - The CustomEvent detail.
156
- * @param key - The event key from the store fragment's event detail map.
242
+ * @param keys - The event key or keys from the store fragment's event detail map.
157
243
  * @param token - The function handling the event.
158
244
  * @returns The event listener.
159
245
  *
@@ -162,14 +248,7 @@ export class AbstractStore {
162
248
  * This logs and stores errors by event key in the store fragment's {@link ErrorMap}, allowing multiple errors to
163
249
  * co-exist, and be presentable to the user via the UI for further action or dismissal.
164
250
  */
165
- this.createErrorListener = (key, token) => {
166
- const listener = ({ detail }) => {
167
- this.logEvent(key);
168
- this.errors.set(key, detail);
169
- return token && token(detail);
170
- };
171
- return this.mappedListener(key, listener);
172
- };
251
+ this.createErrorListener = (keys, token) => this.createListenerType(EventListenerType.error, keys, token);
173
252
  this.storeFragments = storeFragments;
174
253
  }
175
254
  /**
@@ -259,18 +338,6 @@ export class AbstractStore {
259
338
  logger.debug(`'${this.name}': Handle '${String(key)}' store event ${isAsyncHandler ? '[async]' : ''}`);
260
339
  }
261
340
  }
262
- /**
263
- * Sets event listeners on the store fragment's internal map.
264
- * @param key - The event key from the store fragment's event detail map.
265
- * @param listener - The event listener.
266
- * @returns The event listener.
267
- *
268
- * @internal
269
- */
270
- mappedListener(key, listener) {
271
- this.eventListenerMap.set(key, listener);
272
- return listener;
273
- }
274
341
  /**
275
342
  * Add the store fragment's event listeners to the root element.
276
343
  * @param element - The store root element.
@@ -280,7 +347,7 @@ export class AbstractStore {
280
347
  * Called via connect() when the root element changes.
281
348
  */
282
349
  addEventListeners(element) {
283
- for (const [key, listener] of this.eventListenerMap) {
350
+ for (const [listener, key] of this.eventListenerMap) {
284
351
  element.addEventListener(key, listener);
285
352
  }
286
353
  }
@@ -293,7 +360,7 @@ export class AbstractStore {
293
360
  * Called via disconnect() when the root element changes.
294
361
  */
295
362
  removeEventListeners(element) {
296
- for (const [key, listener] of this.eventListenerMap) {
363
+ for (const [listener, key] of this.eventListenerMap) {
297
364
  element.removeEventListener(key, listener);
298
365
  }
299
366
  }
@@ -317,6 +384,44 @@ export class AbstractStore {
317
384
  }
318
385
  this.root.element.dispatchEvent(new CustomEvent(key, Object.assign({ detail }, internalEmitOptions)));
319
386
  }
387
+ /**
388
+ * A convenience method to invoke an async api and emit success and error events.
389
+ *
390
+ * @remarks
391
+ * The async api function should throw when it encounters an error.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * onLoad = this.createAsyncListener('domain-load', async () =>
396
+ * this.invokeAsyncAPI(
397
+ * async () => {
398
+ * !this.domainService.initialized && (await this.domainService.initialize(this.asDomainServiceInit()));
399
+ * return this.domainService.list();
400
+ * },
401
+ * 'domain-load-error',
402
+ * 'domain-load-success'
403
+ * )
404
+ * );
405
+ * ```
406
+ *
407
+ * @param api - The async service api function.
408
+ * @param error - The event key from the store fragment's event detail map.
409
+ * @param success - The event key from the store fragment's event detail map.
410
+ * @public
411
+ */
412
+ invokeAsyncAPI(api, error, success) {
413
+ return __awaiter(this, void 0, void 0, function* () {
414
+ try {
415
+ const result = yield api();
416
+ // @ts-ignore (we know more about this than ts)
417
+ success && this.emit(success, result);
418
+ }
419
+ catch (e) {
420
+ // @ts-ignore (we know more about this than ts)
421
+ this.emit(error, e);
422
+ }
423
+ });
424
+ }
320
425
  }
321
426
  /**
322
427
  * The abstract store root that concrete store roots must extend.