@webqit/observer 2.0.7 → 2.1.1

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/README.md CHANGED
@@ -1,202 +1,425 @@
1
- # The Observer API
2
-
3
- <!-- BADGES/ -->
4
-
5
- <span class="badge-npmversion"><a href="https://npmjs.org/package/@webqit/observer" title="View this project on NPM"><img src="https://img.shields.io/npm/v/@webqit/observer.svg" alt="NPM version" /></a></span> <span class="badge-npmdownloads"><a href="https://npmjs.org/package/@webqit/observer" title="View this project on NPM"><img src="https://img.shields.io/npm/dm/@webqit/observer.svg" alt="NPM downloads" /></a></span>
6
-
7
- <!-- /BADGES -->
8
-
9
- # Overview
10
-
11
- > **Note**
12
- > <br>Major updates coming in this branch!
13
-
14
- Take a one-minute rundown of the Observer API.
15
-
16
- ## Observe
17
-
18
- Observe operations on any object or array...
19
-
20
- ```js
21
- let obj = {};
22
- ```
23
-
24
- ...using the [`Observer.observe()`](https://webqit.io/tooling/observer/docs/api/reactions/observe) method.
25
-
26
- ```js
27
- Observer.observe(obj, mutations => {
28
- mutations.forEach(mutation => {
29
- console.log(mutation.type, mutation.name, mutation.path, mutation.value, mutation.oldValue);
30
- });
31
- });
32
- ```
33
-
34
- Now changes will be delivered *synchronously* - as they happen.
35
-
36
- ## Mutate
37
-
38
- Programmatically make *reactive* changes using the *Reflect-like* [set of operators](https://webqit.io/tooling/observer/docs/api/actions)...
39
-
40
- ```js
41
- // A single set operation
42
- Observer.set(obj, 'prop1', 'value1');
43
- ```
44
- ```js
45
- // A batch set operation
46
- Observer.set(obj, {
47
- prop2: 'value2',
48
- prop3: 'value3',
49
- });
50
- ```
51
-
52
- ...or switch to using *object accessors* - using the [`Observer.accessorize()`](https://webqit.io/tooling/observer/docs/api/actors/accessorize) method...
53
-
54
- ```js
55
- // Accessorize all (existing) properties
56
- Observer.accessorize(obj);
57
- // Accessorize specific properties (existing or new)
58
- Observer.accessorize(obj, ['prop1', 'prop5', 'prop9']);
59
- ```
60
- ```js
61
- // Make reactive operations
62
- obj.prop1 = 'value1';
63
- obj.prop5 = 'value5';
64
- obj.prop9 = 'value9';
65
- ```
66
-
67
- ...or even go with a *reactive Proxy* of your object, and imply properties on the fly.
68
-
69
- ```js
70
- // Obtain a reactive Proxy
71
- let _obj = Observer.proxy(obj);
72
- ```
73
- ```js
74
- // Make reactive operations
75
- _obj.prop1 = 'value1';
76
- _obj.prop4 = 'value4';
77
- _obj.prop8 = 'value8';
78
- ```
79
-
80
- And no problem if you inadvertently cascade the approaches. No bizzare behaviours.
81
-
82
- ```js
83
- // Accessorized properties are already reactive
84
- Observer.accessorize(obj, ['prop1', 'prop6', 'prop10']);
85
-
86
- // But no problem if you inadvertently proxy an accessorized object
87
- let _obj = Observer.proxy(obj);
88
-
89
- // And yet no problem if you inadvertently made a programmatic call over an already reactive Proxy
90
- Observer.set(_obj, 'prop1', 'value1');
91
- ```
92
-
93
- ## Intercept
94
-
95
- How about some level of indirection - the ability to hook into operators like `Observer.set()` and `Observer.deleteProperty()` to repurpose their operation? That's all possible using the [`Observer.intercept()`](https://webqit.io/tooling/observer/docs/api/reactions/intercept) method!
96
-
97
- Below, we catch any attempt to set an HTTP URL and force it to an HTTPS URL.
98
-
99
- ```js
100
- Observer.intercept(obj, 'set', (action, previous, next) => {
101
- if (action.name === 'url' && action.value.startsWith('http:')) {
102
- return next(action.value.replace('http:', 'https:'));
103
- }
104
- return next();
105
- });
106
- ```
107
-
108
- Now, only the first of the following will fly as-is.
109
-
110
- ```js
111
- Observer.set(obj, 'url', 'https://webqit.io');
112
- ```
113
- ```js
114
- Observer.set(obj, 'url', 'http://webqit.io');
115
- ```
116
-
117
- ## Pass Some Detail to Observers
118
-
119
- Operators, like `Observer.set()`, can pass arbitrary value to observers via a `params.detail` property.
120
-
121
- ```js
122
- // A set operation with detail
123
- Observer.set(obj, {
124
- prop2: 'value2',
125
- prop3: 'value3',
126
- }, { detail: 'Certain detail' });
127
- ```
128
-
129
- Observers will recieve this value in a `mutation.detail` property.
130
-
131
- ```js
132
- // An observer with detail
133
- Observer.observe(obj, 'prop1', mutation => {
134
- console.log('An operation has been made with detail:' + mutation.detail);
135
- });
136
- ```
137
-
138
- ## Negotiate with Observers
139
-
140
- Observers can access and *act* on a special object called the *Response Object*.
141
-
142
- ```js
143
- // An observer and the response object
144
- Observer.observe(obj, 'prop1', (mutation, event) => {
145
- if (1) {
146
- event.preventDefault(); // Or return false
147
- } else if (2) {
148
- event.stopPropagation(); // Or return false
149
- } else if (3) {
150
- event.waitUntil(new Promise); // Or return new Promise
151
- }
152
- });
153
- ```
154
-
155
- Operators can access and honour the event's state.
156
-
157
- ```js
158
- // A set operation that returns the eventTypeReturn
159
- let event = Observer.set(obj, {
160
- prop2: 'value2',
161
- prop3: 'value3',
162
- }, { eventTypeReturn: true });
163
- ```
164
-
165
- ```js
166
- if (event.defaultPrevented) {
167
- // event.preventDefault() was called
168
- } else if (event.propagationStopped) {
169
- // event.stopPropagation() was called
170
- } else if (event.promises) {
171
- // event.waitUntil() was called
172
- event.promises.then(() => {
173
-
174
- });
175
- }
176
- ```
177
-
178
- *Learn more about negotiation [here](https://webqit.io/tooling/observer/docs/api/core/Event#negotiating-with-operators)*
179
-
180
- ## Clean Up Anytime
181
-
182
- Need to undo certain bindings? There are the methods for that!
183
-
184
- + [`Observer.unobserve()`](https://webqit.io/tooling/observer/docs/api/reactions/unobserve)
185
- + [`Observer.unintercept()`](https://webqit.io/tooling/observer/docs/api/reactions/unintercept)
186
- + [`Observer.unproxy()`](https://webqit.io/tooling/observer/docs/api/actors/unproxy)
187
- + [`Observer.unaccessorize()`](https://webqit.io/tooling/observer/docs/api/actors/unaccessorize)
188
-
189
- ## The End?
190
-
191
- Certainly not! But this rundown should be a good start. Next:
192
-
193
- + Visit the [download](https://webqit.io/tooling/observer/docs/getting-started/download) page to obtain the Observer API.
194
- + Explore the [API Reference](https://webqit.io/tooling/observer/docs/api).
195
-
196
- ## Issues
197
-
198
- To report bugs or request features, please submit an [issue](https://github.com/webqit/observer/issues).
199
-
200
- ## License
201
-
202
- MIT.
1
+ # The Observer API
2
+
3
+ <!-- BADGES/ -->
4
+
5
+ <span class="badge-npmversion"><a href="https://npmjs.org/package/@webqit/observer" title="View this project on NPM"><img src="https://img.shields.io/npm/v/@webqit/observer.svg" alt="NPM version" /></a></span> <span class="badge-npmdownloads"><a href="https://npmjs.org/package/@webqit/observer" title="View this project on NPM"><img src="https://img.shields.io/npm/dm/@webqit/observer.svg" alt="NPM downloads" /></a></span>
6
+
7
+ <!-- /BADGES -->
8
+
9
+ A web-native object observability API!
10
+
11
+ Observe and intercept operations on any type of JavaScript objects and arrays, using a notably lightweight and predictable utility-first reactivity API!
12
+
13
+ ## Motivation
14
+
15
+
16
+
17
+ ## Table of Contents
18
+
19
+ + [Motivation](#motivation)
20
+ + [Download Options](#download-options)
21
+ + [An Overview](#an-overview)
22
+ + [Method: `Observer.observe()`](#method-observerobserve)
23
+ + [Concept: *Observers*](#concept-observers)
24
+ + [Concept: *Mutations*](#concept-mutations)
25
+ + [Concept: *Batch Mutations*](#concept-batch-mutations)
26
+ + [Concept: *Custom Details*](#concept-custom-details)
27
+ + [Concept: *Diffing*](#concept-diffing)
28
+ + [Method: `Observer.intercept()`](#method-observerintercept)
29
+ + [Concept: *Traps*](#concept-traps)
30
+ + [Issues](#issues)
31
+ + [License](#license)
32
+
33
+ ## Download Options
34
+
35
+ **_Use as an npm package:_**
36
+
37
+ ```bash
38
+ npm i @webqit/observer
39
+ ```
40
+
41
+ ```js
42
+ // Import
43
+ import Observer from '@webqit/observer';;
44
+ ```
45
+
46
+ **_Use as a script:_**
47
+
48
+ ```html
49
+ <script src="https://unpkg.com/@webqit/observer/dist/main.js"></script>
50
+ ```
51
+
52
+ ```js
53
+ // Obtain the APIs
54
+ const Observer = window.webqit.Observer;
55
+ ```
56
+
57
+ ## An Overview
58
+
59
+ > **Note**
60
+ > <br>This is documentation for `Observer@2.x`. (Looking for [`Observer@1.x`](https://github.com/webqit/observer/tree/v1.7.6)?)
61
+
62
+ ### Method: `Observer.observe()`
63
+
64
+ Observe mutations on any object or array!
65
+
66
+ ```js
67
+ // Signature 1
68
+ Observer.observe( obj, callback[, options = {} ]);
69
+ ```
70
+
71
+ ```js
72
+ // Signature 2
73
+ Observer.observe( obj, props, callback[, options = {} ]);
74
+ ```
75
+
76
+ #### Concept: *Observers*
77
+
78
+ Observe arbitrary objects and arrays:
79
+
80
+ ```js
81
+ // An object
82
+ const obj = {};
83
+ // Mtation observer on an object
84
+ const abortController = Observer.observe( obj, handleChanges );
85
+ ```
86
+
87
+ ```js
88
+ // An array
89
+ const arr = [];
90
+ // Mtation observer on an array
91
+ const abortController = Observer.observe( arr, handleChanges );
92
+ ```
93
+
94
+ *Now changes will be delivered **synchronously** - as they happen. (The *sync* design is discussed shortly.)*
95
+
96
+ ```js
97
+ // The change handler
98
+ function handleChanges( mutations ) {
99
+ mutations.forEach( mutation => {
100
+ console.log( mutation.type, mutation.key, mutation.value, mutation.oldValue );
101
+ } );
102
+ }
103
+ ```
104
+
105
+ **-->** Stop observing at any time by calling `abort()` on the returned *abortController*...
106
+
107
+ ```js
108
+ // Remove listener
109
+ abortController.abort();
110
+ ```
111
+
112
+ ...or by using an [Abort Signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) instance:
113
+
114
+ ```js
115
+ // Providing an AbortSignal
116
+ const abortController = new AbortController;
117
+ Observer.observe( obj, mutations => {
118
+ // Handle...
119
+ }, { signal: abortController.signal } );
120
+ ```
121
+
122
+ ```js
123
+ // Abort at any time
124
+ abortController.abort();
125
+ ```
126
+
127
+ #### Concept: *Mutations*
128
+
129
+ Programmatically mutate properties of an object using the *[Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect#static_methods)-like* set of operators; each operation will be reported by observers:
130
+
131
+ ```js
132
+ // A single "set" operation on an object
133
+ Observer.set( obj, 'prop0', 'value0' );
134
+ Observer.defineProperty( obj, 'prop1', { get: () => 'value1' } );
135
+ Observer.deleteProperty( obj, 'prop2' );
136
+ ```
137
+
138
+ ```js
139
+ // A single "set" operation on an array
140
+ Observer.set( arr, 0, 'item0' ); // Array [ 'item0' ]
141
+ Observer.deleteProperty( arr, 0 ); // Array [ <1 empty slot> ]
142
+ ```
143
+
144
+ *Beware non-reactive operations*:
145
+
146
+ ```js
147
+ // Literal object accessors
148
+ delete obj.prop0;
149
+ obj.prop3 = 'value3';
150
+ ```
151
+
152
+ ```js
153
+ // Array methods
154
+ arr.push( 'item3' );
155
+ arr.pop();
156
+ ```
157
+
158
+ **-->** Enable reactivity on *specific* properties with literal *object accessors* - using the `Observer.accessorize()` method:
159
+
160
+ ```js
161
+ // Accessorize all (existing) properties
162
+ Observer.accessorize( obj );
163
+ // Accessorize specific properties (existing or new)
164
+ Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2' ] );
165
+
166
+ // Make reactive UPDATES
167
+ obj.prop0 = 'value0';
168
+ obj.prop1 = 'value1';
169
+ obj.prop2 = 'value2';
170
+ ```
171
+
172
+ ```js
173
+ // Accessorize all (existing) indexes
174
+ Observer.accessorize( arr );
175
+ // Accessorize specific indexes (existing or new)
176
+ Observer.accessorize( arr, [ 0, 1, 2 ] );
177
+
178
+ // Make reactive UPDATES
179
+ arr[ 0 ] = 'item0';
180
+ arr[ 1 ] = 'item1';
181
+ arr[ 2 ] = 'item2';
182
+
183
+ // Bonus reactivity: array methods that re-index existing items
184
+ arr.unshift( 'new-item0' );
185
+ arr.shift();
186
+ ```
187
+
188
+ *Beware non-reactive operations*:
189
+
190
+ ```js
191
+ // The delete operator and object properties that haven't been accessorized
192
+ delete obj.prop0;
193
+ obj.prop3 = 'value3';
194
+ ```
195
+
196
+ ```js
197
+ // Array methods that do not re-index existing items
198
+ arr.push( 'item0' );
199
+ arr.pop();
200
+ ```
201
+
202
+ **-->** Enable reactivity on *arbitray* properties with *Proxies* - using the `Observer.proxy()` method:
203
+
204
+ ```js
205
+ // Obtain a reactive Proxy for an object
206
+ const _obj = Observer.proxy( obj );
207
+
208
+ // Make reactive operations
209
+ _obj.prop1 = 'value1';
210
+ _obj.prop4 = 'value4';
211
+ _obj.prop8 = 'value8';
212
+
213
+ // With the delete operator
214
+ delete _obj.prop0;
215
+ ```
216
+
217
+ ```js
218
+ // Obtain a reactive Proxy for an array
219
+ const _arr = Observer.proxy( arr );
220
+
221
+ // Make reactive operations
222
+ _arr[ 0 ] = 'item0';
223
+ _arr[ 1 ] = 'item1';
224
+ _arr[ 2 ] = 'item2';
225
+
226
+ // With an instance method
227
+ _arr.push( 'item3' );
228
+ ```
229
+
230
+ *And no problem if you end up nesting the approaches.*
231
+
232
+ ```js
233
+ // 'value1'-->obj
234
+ Observer.accessorize( obj, [ 'prop0', 'prop1', 'prop2', ] );
235
+ obj.prop1 = 'value1';
236
+
237
+ // 'value1'-->_obj-->obj
238
+ let _obj = Observer.proxy( obj );
239
+ _obj.prop1 = 'value1';
240
+
241
+ // 'value1'-->set()-->_obj-->obj
242
+ Observer.set( _obj, 'prop1', 'value1' );
243
+ ```
244
+
245
+ **-->** "Restore" accessorized properties to their normal state by using the `unaccessorize()` method:
246
+
247
+ ```js
248
+ Observer.unaccessorize( obj, [ 'prop1', 'prop6', 'prop10' ] );
249
+ ```
250
+
251
+ **-->** "Reproduce" original objects from Proxies obtained via `Observer.proxy()` by using the `unproxy()` method:
252
+
253
+ ```js
254
+ obj = Observer.unproxy( _obj );
255
+ ```
256
+
257
+ #### Concept: *Batch Mutations*
258
+
259
+ Make multiple mutations at a go, and they'll be correctly delivered in batch to observers!
260
+
261
+ ```js
262
+ // Batch operations on an object
263
+ Observer.set( obj, {
264
+ prop0: 'value0',
265
+ prop1: 'value1',
266
+ prop2: 'value2',
267
+ } );
268
+ Observer.defineProperties( obj, {
269
+ prop0: { value: 'value0' },
270
+ prop1: { value: 'value1' },
271
+ prop2: { get: () => 'value2' },
272
+ } );
273
+ Observer.deleteProperties( obj, [ 'prop0', 'prop1', 'prop2' ] );
274
+ ```
275
+
276
+ ```js
277
+ // Batch operations on an array
278
+ Observer.set( arr, {
279
+ '0': 'item0',
280
+ '1': 'item1',
281
+ '2': 'item2',
282
+ } );
283
+ Object.proxy( arr ).push( 'item3', 'item4', 'item5', );
284
+ Object.proxy( arr ).unshift( 'new-item0' );
285
+ Object.proxy( arr ).splice( 0 );
286
+ ```
287
+
288
+ **-->** Use the `Observer.batch()` to batch multiple arbitrary mutations - whether related or not:
289
+
290
+ ```js
291
+ Observer.batch( arr, async () => {
292
+ Observer.set( arr, 0, 'item0' ); // Array [ 'item0' ]
293
+ await somePromise();
294
+ Observer.set( arr, 2, 'item2' ); // Array [ 'item0', <1 empty slot>, 'item2' ]
295
+ } );
296
+ ```
297
+
298
+ > Method calls on a proxied instance - e.g. `Object.proxy( arr ).splice( 0 )` - also follow this strategy.
299
+
300
+ #### Concept: *Custom Details*
301
+
302
+ Pass some custom detail - an arbitrary value - to observers via a `params.detail` property.
303
+
304
+ ```js
305
+ // A set operation with detail
306
+ Observer.set( obj, {
307
+ prop2: 'value2',
308
+ prop3: 'value3',
309
+ }, { detail: 'Certain detail' } );
310
+ ```
311
+
312
+ *Observers recieve this value on their `mutation.detail` property.*
313
+
314
+ ```js
315
+ // An observer with detail
316
+ Observer.observe( obj, 'prop1', mutation => {
317
+ console.log( 'A mutation has been made with detail:' + mutation.detail );
318
+ } );
319
+ ```
320
+
321
+ #### Concept: *Diffing*
322
+
323
+ Receive notifications only for mutations that actually change property state, and ignore those that don't.
324
+
325
+ ```js
326
+ // Responding to state changes only
327
+ Observer.observe( obj, handleChanges, { diff: true } );
328
+ ```
329
+
330
+ ```js
331
+ // Recieved
332
+ Observer.set( obj, 'prop0', 'value' );
333
+ ```
334
+
335
+ ```js
336
+ // Ignored
337
+ Observer.set( obj, 'prop0', 'value' );
338
+ ```
339
+
340
+ <!--
341
+ ### Concept: *Live*
342
+ descripted
343
+ namespace
344
+ -->
345
+
346
+ ### Method: `Observer.intercept()`
347
+
348
+ Intercept operations on any object or array before they happen!
349
+
350
+ ```js
351
+ // Signature 1
352
+ Observer.intercept( obj, prop, handler[, options = {} ]);
353
+ ```
354
+
355
+ ```js
356
+ // Signature 2
357
+ Observer.intercept( obj, traps[, options = {} ]);
358
+ ```
359
+
360
+ #### Concept: *Traps*
361
+
362
+ Extend standard operations on an object - `Observer.set()`, `Observer.deleteProperty()`, etc - with custom traps using the [`Observer.intercept()`](https://webqit.io/tooling/observer/docs/api/reactions/intercept) method!
363
+
364
+ *Below, we intercept all "set" operations for an HTTP URL then transform it to an HTTPS URL.*
365
+
366
+ ```js
367
+ const setTrap = ( operation, previous, next ) => {
368
+ if ( operation.key === 'url' && operation.value.startsWith( 'http:' ) ) {
369
+ operation.value = operation.value.replace( 'http:', 'https:' );
370
+ }
371
+ return next();
372
+ };
373
+ Observer.intercept( obj, 'set', setTrap );
374
+ ```
375
+
376
+ *Now, only the first of the following will fly as-is.*
377
+
378
+ ```js
379
+ // Not transformed
380
+ Observer.set( obj, 'url', 'https://webqit.io' );
381
+
382
+ // Transformed
383
+ Observer.set( obj, 'url', 'http://webqit.io' );
384
+ ```
385
+
386
+ *And below, we intercept all "get" operations for a certain value to trigger a network fetch behind the scenes.*
387
+
388
+ ```js
389
+ const getTrap = ( operation, previous, next ) => {
390
+ if ( operation.key === 'token' ) {
391
+ return next( fetch( tokenUrl ) );
392
+ }
393
+ return next();
394
+ };
395
+ Observer.intercept( obj, 'get', getTrap );
396
+ ```
397
+
398
+ *And all of that can go into one "traps" object:*
399
+
400
+ ```js
401
+ Observer.intercept( obj, {
402
+ get: getTrap,
403
+ set: setTrap,
404
+ deleteProperty: deletePropertyTrap,
405
+ defineProperty: definePropertyTrap,
406
+ ownKeys: ownKeysTrap,
407
+ has: hasTrap,
408
+ // etc
409
+ } );
410
+ ```
411
+
412
+ ## The End?
413
+
414
+ Certainly not! But this rundown should be a good start. Next:
415
+
416
+ + Visit the [download](https://webqit.io/tooling/observer/docs/getting-started/download) page to obtain the Observer API.
417
+ + Explore the [API Reference](https://webqit.io/tooling/observer/docs/api).
418
+
419
+ ## Issues
420
+
421
+ To report bugs or request features, please submit an [issue](https://github.com/webqit/observer/issues).
422
+
423
+ ## License
424
+
425
+ MIT.