@thepassle/app-tools 0.9.3 → 0.9.5

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/api/README.md ADDED
@@ -0,0 +1,443 @@
1
+ # Api
2
+
3
+ Small wrapper around the native [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm i -S @thepassle/app-tools
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { Api } from '@thepassle/app-tools/api.js';
15
+
16
+ /** Using defaults: */
17
+ const api = new Api();
18
+
19
+ /** Or with configuration: */
20
+ const api = new Api({
21
+ baseURL: 'https://api.foo.com',
22
+ responseType: 'text',
23
+ plugins: [
24
+ {
25
+ beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
26
+ afterFetch: (res) => res,
27
+ transform: (data) => data,
28
+ handleError: (e) => true
29
+ }
30
+ ]
31
+ });
32
+
33
+ const user = await api.get('/users/1');
34
+ await api.post('/form/submit', { name: 'John Doe', email: 'johndoe@internet.com' });
35
+
36
+ try {
37
+ await api.get('/foo');
38
+ } catch(e) {
39
+ console.log(e); // StatusError
40
+ e.message; // the `statusText` of the `response`
41
+ e.response; // access the `response`
42
+ }
43
+ ```
44
+
45
+ ### Error Handling
46
+
47
+ By default `api` will throw an error if a response is not ok (`!response.ok`). If this is the case, it will throw a `StatusError`. The `StatusError` is thrown with the `response.statusText` as message, and also has the actual `response` available on it: `e.response`.
48
+
49
+ ## Composable
50
+
51
+ Use plugins to customize your requests to fit your needs
52
+
53
+ ### `logger`
54
+
55
+ ```js
56
+ import { logger, loggerPlugin } from '@thepassle/app-tools/api/plugins/logger.js';
57
+
58
+ /** Logs metadata to the console */
59
+ api.get(url, {plugins: [logger]});
60
+
61
+ /** Or */
62
+ const logger = loggerPlugin({collapsed: false});
63
+ api.get(url, {plugins: [logger]});
64
+ ```
65
+
66
+ ### `cache`
67
+
68
+ ```js
69
+ import { cache, cachePlugin } from '@thepassle/app-tools/api/plugins/cache.js';
70
+
71
+ /** Caches the response for a default of 10 minutes */
72
+ api.get(url, {plugins: [cache]});
73
+
74
+ /** Or */
75
+ const cache = cachePlugin({maxAge: 1000});
76
+ api.get(url, {plugins: [cache]});
77
+ ```
78
+
79
+ ### `debounce`
80
+
81
+ ```js
82
+ import { debounce, debouncePlugin } from '@thepassle/app-tools/api/plugins/debounce.js';
83
+
84
+ /** Debounces the response for a default of 1000 ms */
85
+ api.get(url, {plugins: [debounce]});
86
+
87
+ /** Or */
88
+ const debounce = debouncePlugin({maxAge: 2000});
89
+ api.get(url, {plugins: [debounce]});
90
+ ```
91
+
92
+ **Note:** The `debounce` plugin wraps the `fetchFn` in a debouncer. `await`ing the call will cause the debounce to be awaited. E.g.:
93
+
94
+ ```js
95
+ api.get(url, {plugins: [debounce]}).then(() => { console.log(1) });
96
+ api.get(url, {plugins: [debounce]}).then(() => { console.log(2) });
97
+
98
+ // Output:
99
+ // 2
100
+ ```
101
+
102
+ But awaiting it will become:
103
+ ```js
104
+ await api.get(url, {plugins: [debounce]}).then(() => { console.log(1) });
105
+ await api.get(url, {plugins: [debounce]}).then(() => { console.log(2) });
106
+
107
+ // Output:
108
+ // 1
109
+ // 2
110
+ ```
111
+
112
+ ### `abort`
113
+
114
+ ```js
115
+ import { abort } from '@thepassle/app-tools/api/plugins/abort.js';
116
+
117
+ /** Aborts previous, unfinished requests via an AbortController if requests are fired in quick succession, like spammy clicks on buttons */
118
+ api.get(url, {plugins: [abort]});
119
+ ```
120
+
121
+ ### `mock`, `delay`
122
+
123
+ ```js
124
+ import { mock } from '@thepassle/app-tools/api/plugins/mock.js';
125
+ import { delay, delayPlugin } from '@thepassle/app-tools/api/plugins/delay.js';
126
+
127
+ /** Easily mock requests during development using the native `Response` object */
128
+ api.get(url, {
129
+ plugins: [
130
+ mock(() => new Response(JSON.stringify({foo: 'bar'}))),
131
+ delay // defaults to 1000ms
132
+ ]
133
+ });
134
+
135
+ /** Or */
136
+ const delay = delayPlugin(2000);
137
+ api.get(url, {plugins: [delay]});
138
+ ```
139
+
140
+ ### `jsonPrefix`
141
+
142
+ ```js
143
+ import { jsonPrefix, jsonPrefixPlugin } from '@thepassle/app-tools/api/plugins/jsonPrefix.js';
144
+
145
+ /** Add plugins to run on all requests */
146
+ const api = new Api({ plugins: [jsonPrefix] });
147
+
148
+ /** Or */
149
+ const jsonPrefix = jsonPrefixPlugin('<prefix>');
150
+ const api = new Api({ plugins: [jsonPRefix] });
151
+ ```
152
+
153
+ ### `xsrf`
154
+
155
+ ```js
156
+ import { xsrf, xsrfPlugin } from '@thepassle/app-tools/api/plugins/xsrf.js';
157
+
158
+ /** Add plugins to run on all requests */
159
+ const api = new Api({ plugins: [xsrf] });
160
+
161
+ /** Or */
162
+ const xsrf = xsrfPlugin({
163
+ xsrfCookieName: '',
164
+ xsrfHeaderName: ''
165
+ });
166
+ const api = new Api({ plugins: [xsrf] });
167
+ ```
168
+
169
+ ### Other
170
+
171
+ ```js
172
+ import { logger } from '@thepassle/app-tools/api/plugins/logger.js';
173
+
174
+ /** Add plugins to run on all requests */
175
+ const api = new Api({ plugins: [logoutOnUnauthorized, logger] });
176
+ ```
177
+
178
+ ## Methods
179
+
180
+ ```js
181
+ api.get(url, opts);
182
+ api.options(url, opts);
183
+ api.delete(url, opts);
184
+ api.head(url, opts);
185
+ api.post(url, data, opts);
186
+ api.put(url, data, opts);
187
+ api.patch(url, data, opts);
188
+
189
+ api.addPlugin({
190
+ beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
191
+ afterFetch: (res) => res,
192
+ transform: (data) => data,
193
+ handleError: (e) => true
194
+ });
195
+ ```
196
+
197
+ ## Big List of Options
198
+
199
+ ```js
200
+ api.get(url, {
201
+ baseURL: 'https://api.foo.com',
202
+ responseType: 'text',
203
+ params: { foo: 'bar' },
204
+ plugins: [
205
+ {
206
+ beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
207
+ afterFetch: (res) => res,
208
+ transform: (data) => data,
209
+ handleError: (e) => true
210
+ }
211
+ ],
212
+
213
+ // Also supports all the options of the native fetch API
214
+ // mode, credentials, cache, redirect, referrer, integrity, keepalive, signal, referrerPolicy, headers, method
215
+ });
216
+ ```
217
+
218
+ ## Options
219
+
220
+ ### `baseURL`
221
+
222
+ BaseURL to resolve all requests from. Can be set globally when instantiating a new `Api` instance, or on a per request basis. When set on a per request basis, will override the globally set baseURL (if set)
223
+
224
+ ```js
225
+ api.get(url, { baseURL: 'https://api.foo.com' });
226
+ ```
227
+
228
+ ### `responseType`
229
+
230
+ Overwrite the default responseType (`'json'`)
231
+
232
+ ```js
233
+ api.get(url, { responseType: 'text' });
234
+ ```
235
+
236
+ ### `params`
237
+
238
+ An object to be queryParam-ified and added to the request url
239
+
240
+ ```js
241
+ api.get(url, { params: { foo: 'bar' } });
242
+ ```
243
+
244
+ ### `plugins`
245
+
246
+ An array of plugins.
247
+
248
+ ```js
249
+ api.get(url, {
250
+ plugins: [
251
+ {
252
+ beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
253
+ afterFetch: (res) => res,
254
+ transform: (data) => data,
255
+ handleError: (e) => true
256
+ }
257
+ ]
258
+ })
259
+ ```
260
+
261
+ ## Plugins
262
+
263
+ You can also use plugins. You can add plugins on a per-request basis, or you can globally add them to your `api` instance:
264
+
265
+ ```js
266
+ const api = new Api({
267
+ plugins: [
268
+ {
269
+ beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
270
+ afterFetch: (res) => res,
271
+ transform: (data) => data,
272
+ handleError: (e) => true
273
+ }
274
+ ]
275
+ });
276
+ ```
277
+
278
+ You can also dynamically add plugins:
279
+
280
+ ```js
281
+ api.addPlugin({
282
+ afterFetch: (res) => res
283
+ });
284
+ ```
285
+
286
+ Or you can add them on a per request basis:
287
+
288
+ ```js
289
+ api.get(url, {
290
+ plugins: [
291
+ {
292
+ beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
293
+ afterFetch: (res) => res,
294
+ transform: (data) => data,
295
+ handleError: (e) => true
296
+ }
297
+ ]
298
+ });
299
+ ```
300
+
301
+ ### `beforeFetch`
302
+
303
+ Run logic before the actual `fetch` call happens, or alter/modify the meta information of a request.
304
+ If you want to alter or modify the meta information of a request, make sure to return the value.
305
+
306
+ ```js
307
+ api.get('/foo', {
308
+ plugins: [{
309
+ beforeFetch: (meta) => ({...meta, url: '/bar'})
310
+ }]
311
+ });
312
+
313
+ // RESULT: url `/bar` gets called instead of `/foo`
314
+ ```
315
+
316
+ If you dont want to alter or modify any meta information of the request, you dont have to return anything.
317
+ ```js
318
+ {
319
+ beforeFetch: ({url}) => {
320
+ console.log(url)
321
+ }
322
+ }
323
+ ```
324
+
325
+ ### `afterFetch`
326
+
327
+ Runs immediately after the `fetch` call happened. `afterFetch` should always return a `Response`:
328
+ ```js
329
+ { afterFetch: (res) => res; }
330
+ { afterFetch: (res) => new Response(JSON.stringify({foo: 'bar'}), res); }
331
+ ```
332
+
333
+ ### `transform`
334
+
335
+ Runs after the `Response` object has been handled according to the `responseType`, (e.g.: `res.json()`). Can be used to transform the returned data:
336
+ Should always return the data.
337
+
338
+ ```js
339
+ { transform: (data) => data }
340
+ ```
341
+
342
+ ### `handleError`
343
+
344
+ Whether or not an error should throw. Return `true` if an error should throw, return `false` if an error should be ignored.
345
+
346
+ ```js
347
+ { handleError: (e) => e.message !== 'AbortError' }
348
+ ```
349
+
350
+ ### Plugin Examples
351
+
352
+ #### Request logger
353
+
354
+ ```js
355
+ function requestLogger() {
356
+ let start;
357
+ return {
358
+ beforeFetch: () => {
359
+ start = Date.now();
360
+ },
361
+ afterFetch: () => {
362
+ console.log(`Request took ${Date.now() - start}ms`);
363
+ }
364
+ }
365
+ }
366
+
367
+ api.addPlugin(requestLogger());
368
+ ```
369
+
370
+ #### Automatic logout on 401 or 403
371
+
372
+ ```js
373
+ api.get(url, {
374
+ plugins: [
375
+ {
376
+ beforeFetch: ({url, headers, fetchFn, responseType, baseURL, method, opts, data}) => {},
377
+ afterFetch: (res) => {
378
+ if(res.status === 401 || res.status === 403) {
379
+ logout();
380
+ }
381
+ return res;
382
+ }
383
+ }
384
+ ]
385
+ });
386
+ ```
387
+
388
+ #### Accessing the response body in `afterFetch`
389
+
390
+ If you want to access the response body of your response in a plugin, make sure to clone the response:
391
+
392
+ ```js
393
+ const myPlugin = {
394
+ afterFetch: async (originalResponse) => {
395
+ const clone = originalResponse.clone();
396
+ let data = await clone.text(); // or `.json()` etc
397
+
398
+ data = data.replaceAll('foo', 'bar');
399
+
400
+ // Always make sure to return a `Response`
401
+ return new Response(data, originalResponse);
402
+ }
403
+ }
404
+
405
+ api.addPlugin(myPlugin);
406
+ ```
407
+
408
+ #### Returning a new response entirely
409
+
410
+ You can also overwrite the response entirely by returning a new `Response`
411
+
412
+ ```js
413
+ api.addPlugin({
414
+ afterFetch: async (res) => new Response(JSON.stringify({foo: 'bar'}), res);
415
+ });
416
+ ```
417
+
418
+ #### Overwriting the `fetch` implementation
419
+
420
+ You can also overwrite the `fetch` implementation to use:
421
+
422
+ ```js
423
+ api.addPlugin({
424
+ beforeFetch: (meta) => ({
425
+ ...meta,
426
+ fetchFn: () => Promise.resolve(new Response('{}'))
427
+ })
428
+ })
429
+ ```
430
+
431
+ Do note that if you use multiple plugins that overwrite the `fetchFn`, the last plugin to overwrite the `fetchFn` will win, there can only be one `fetchFn`.
432
+
433
+ #### Transforming data
434
+
435
+ ```js
436
+ api.addPlugin({
437
+ // Adds a `.foo` property to all of your response data
438
+ transform: (data) => {
439
+ data.foo = 'bar';
440
+ return data;
441
+ }
442
+ })
443
+ ```
@@ -0,0 +1,252 @@
1
+ # Pwa
2
+
3
+ ## Install
4
+
5
+ ```
6
+ npm i -S @thepassle/app-tools
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ import { Dialog } from '@thepassle/app-tools/dialog.js';
13
+
14
+ const dialog = new Dialog({
15
+ foo: {
16
+ opening: (context) => {context.dialog.querySelector('form').innerHTML = 'hello world';},
17
+ opened: (context) => {},
18
+ closing: (context) => {},
19
+ closed: (context) => {}
20
+ },
21
+ bar: someAbstraction({
22
+ title: 'foo',
23
+ import: () => import('./my-component.js'),
24
+ render: () => html`<my-dialog></my-dialog>`
25
+ }),
26
+ });
27
+
28
+ dialog.open({id: 'foo'});
29
+ await dialog.opened;
30
+ dialog.isOpen; // true
31
+ /** Or */
32
+ dialog.opened.then((context) => {});
33
+
34
+ dialog.close();
35
+ await dialog.closed;
36
+ dialog.isOpen; // false
37
+ /** Or */
38
+ dialog.closed.then((context) => {});
39
+
40
+ dialog.addEventListener('opening', ({context}) => {});
41
+ dialog.addEventListener('opened', ({context}) => {});
42
+ dialog.addEventListener('closing', ({context}) => {
43
+ console.log(dialog.returnValue);
44
+ });
45
+ dialog.addEventListener('closed', ({context}) => {
46
+ const { id, dialog } = context;
47
+
48
+ if (id === 'foo') {
49
+ console.log(dialog.returnValue);
50
+ }
51
+
52
+ if(dialog.returnValue === 'dismiss') {
53
+ console.log('Dialog was closed via light dismiss');
54
+ }
55
+
56
+ if(dialog.returnValue === 'programmatic') {
57
+ console.log('Dialog was closed via `dialog.close()`');
58
+ }
59
+ });
60
+
61
+ dialog.modify((dialogNode) => {
62
+ dialogNode.classList.add('foo');
63
+ });
64
+
65
+ /** You can also pass parameters to the dialog renderer */
66
+ dialog.open({
67
+ id: 'foo',
68
+ parameters: {
69
+ foo: 'bar'
70
+ }
71
+ });
72
+ ```
73
+
74
+ ## Callbacks
75
+
76
+ ```js
77
+ import { Dialog } from '@thepassle/app-tools/dialog.js';
78
+
79
+ const dialog = new Dialog({
80
+ foo: {
81
+ /**
82
+ * Executed right after the dialog has been created and added to the DOM, before animations have run
83
+ * Can be used for setup work, like adding `id`s to the dialog, lazy loading,
84
+ * and rendering to the dialog's DOM
85
+ */
86
+ opening: (context) => {
87
+ context.dialog; // dialog node
88
+ context.id; // 'foo';
89
+ context.parameters; // { bar: 'bar' }
90
+ },
91
+
92
+ /**
93
+ * Executed after animations for the dialog element have run
94
+ */
95
+ opened: (context) => {},
96
+
97
+ /**
98
+ * Executed when the native <dialog>'s `close` event has fired, on "light dismiss",
99
+ * escape was pressed, or `dialog.close` was called
100
+ * Executed before animations
101
+ *
102
+ * Has access to `dialog.returnValue`
103
+ */
104
+ closing: (context) => {},
105
+
106
+ /**
107
+ * Executed after the dialog's close animations have run and right before the dialog node is removed from the DOM
108
+ *
109
+ * Has access to `dialog.returnValue`
110
+ */
111
+ closed: (context) => {}
112
+ },
113
+ });
114
+
115
+ dialog.open({id: 'foo', parameters: {bar: 'bar'}})
116
+ ```
117
+
118
+ ## Styling the dialog
119
+
120
+ It's recommended to provide a unique ID for the kind of dialog you want to show. For example:
121
+
122
+ ```js
123
+ import { Dialog } from '@thepassle/app-tools/dialog.js';
124
+
125
+ const dialog = new Dialog({
126
+ foo: {
127
+ opening: ({dialog}) => {
128
+ dialog.id = 'foo';
129
+ },
130
+ },
131
+ });
132
+ ```
133
+
134
+ You can then, in your global stylesheet, select the dialog like so:
135
+ ```css
136
+ dialog[app-tools]#foo {
137
+ border-radius: 10px;
138
+ background: lightgrey;
139
+ /* etc */
140
+ }
141
+
142
+ @media (max-width: 600px) {
143
+ dialog[app-tools]#foo {
144
+ width: 90%;
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## Animating the dialog
150
+
151
+ ```js
152
+ import { Dialog } from '@thepassle/app-tools/dialog.js';
153
+
154
+ const dialog = new Dialog({
155
+ foo: {
156
+ opening: ({dialog}) => {
157
+ dialog.id = 'foo';
158
+ dialog.form.innerHTML = 'hello world';
159
+ },
160
+ },
161
+ });
162
+
163
+ dialog.open({id: 'foo'});
164
+ ```
165
+
166
+ ```css
167
+ dialog[app-tools]#foo {
168
+ opacity: 0;
169
+ transform: translateY(40px);
170
+ transition: opacity .3s ease-out, transform .3s ease-out;
171
+ }
172
+
173
+ dialog[app-tools][open]#foo {
174
+ opacity: 1;
175
+ transform: translateY(0);
176
+ }
177
+ ```
178
+
179
+ ## Abstractions
180
+
181
+ It can be useful to declare some abstractions for the different kinds of dialogs you want to use in your app. Here's an example using Lit:
182
+
183
+ ```js
184
+ import { html, render } from 'lit';
185
+ import { Dialog } from '@thepassle/app-tools/dialog.js';
186
+
187
+ function modal(config) {
188
+ return {
189
+ opening: ({dialog, parameters}) => {
190
+ config.import();
191
+ render(config.render({parameters, title: config.title}), dialog.form);
192
+ },
193
+ closing: ({dialog}) => {
194
+ console.log(dialog.returnValue); // "bar"
195
+ }
196
+ }
197
+ }
198
+
199
+ const dialog = new Dialog({
200
+ foo: modal({
201
+ title: 'Cart',
202
+ import: () => import('./shopping-cart.js'),
203
+ render: ({title, parameters}) => html`
204
+ <h1>${title}</h1>
205
+ <shopping-cart foo=${parameters.foo}></shopping-cart>
206
+ <button value="bar">Close</button>
207
+ `
208
+ })
209
+ });
210
+
211
+ dialog.open({id: 'foo', parameters: { foo: 'bar' }});
212
+ ```
213
+
214
+ ### Context menu
215
+
216
+ ```js
217
+ import { computePosition } from '@floating-ui/dom';
218
+ import { Dialog } from '@thepassle/app-tools';
219
+
220
+ export const dialog = new Dialog({
221
+ context: context()
222
+ });
223
+
224
+ function context() {
225
+ return {
226
+ opening: async ({dialog, parameters, id}) => {
227
+ dialog.id = 'context';
228
+ render(parameters.template(), dialog.form);
229
+
230
+ if (!media.MAX.XS()) {
231
+ const { x, y } = await computePosition(
232
+ parameters.target,
233
+ dialog,
234
+ { placement: 'bottom-end'}
235
+ );
236
+ Object.assign(dialog.style, {
237
+ marginLeft: `${x}px`,
238
+ marginTop: `${y}px`,
239
+ });
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ dialog.open({
246
+ id: 'context',
247
+ parameters: {
248
+ target: e.target,
249
+ template: () => html`<h1>hello world</h1>`
250
+ }
251
+ });
252
+ ```