@thepassle/app-tools 0.9.3 → 0.9.4
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 +443 -0
- package/dialog/README.md +252 -0
- package/env/README.md +29 -0
- package/package.json +1 -1
- package/pwa/README.md +103 -0
- package/router/README.md +491 -0
- package/state/README.md +25 -0
- package/utils/README.md +139 -0
- package/utils/log.js +1 -1
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
|
+
```
|
package/dialog/README.md
ADDED
|
@@ -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
|
+
```
|
package/env/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Env
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm i -S @thepassle/app-tools
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { DEV, PROD } from '@thepassle/app-tools/env.js';
|
|
13
|
+
|
|
14
|
+
if (DEV) {
|
|
15
|
+
console.log('Running in dev mode');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (PROD) {
|
|
19
|
+
console.log('Running in prod mode');
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When resolving imports, Node will look for keys in the package export to figure out which file to use. For example "default", "import", or "require".
|
|
24
|
+
|
|
25
|
+
Tools however can use custom keys here as well, like "types", "browser", "development", or "production". Depending on what kind of import it is, and which environment (dev/prod) tools (like bundlers or dev servers) can resolve/distinguish between which file should actually be used.
|
|
26
|
+
|
|
27
|
+
## Acknowledgement
|
|
28
|
+
|
|
29
|
+
This was inspired by [`esm-env`](https://github.com/benmccann/esm-env) by Ben McCann. I've added to my own repo because I like to have useful utils like these centralized in one place for my personal projects, especially when they're as small and elegant as this.
|