Skip to content

Commit

Permalink
Merge pull request #1 from aleph1/2.0.0-beta
Browse files Browse the repository at this point in the history
2.0.0 release
  • Loading branch information
aleph1 authored Dec 28, 2023
2 parents 088574b + 4c45d49 commit 8d4c1c0
Show file tree
Hide file tree
Showing 19 changed files with 1,293 additions and 5,461 deletions.
100 changes: 68 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# Simple Observable Proxy
### Simple Observable Proxy is a dependency-free library for JavaScript, that allows for observation of arrays and objects that are either flat or deep.

For objects, changing values, as well as adding, editing, or deleting keys results in a callback signal. For arrays changing values, direct modification using \[\], methods that change the array (pop, push, shift, unshift, etc.), or modifying length results in a callback signal. Multiple observers can be created per observable, and all signals are queued and sent on requestAnimationFrame.
For objects, changing values, as well as adding, editing, or deleting keys results in a callback signal. For arrays changing values, direct modification using \[\], methods that change the array (pop, push, shift, unshift, etc.), or modifying length results in a callback signal. Multiple observers can be created per observable, and all signals are queued and sent using requestAnimationFrame in the browser, and an interval of 16 milliseconds in a node environment.

Simple Observable Proxy is written in TypeScript and is intended to be a very small library (approximately 650 bytes when minified and gzipped), and, as such, it does not report on specific differences between the current and prior state of the observed object or array.
Simple Observable Proxy is written in TypeScript and compiles as CommonJS and ESM. It is intended to be a very small library (less than 800 bytes when minified and gzipped), and, as such, it does not report on specific differences between the current and prior state of the observed object or array.

- [Installation](#real-cool-heading)
- [Basic Usage](#basic-usage)
- [Methods](#methods)
- [Migrating from 1.x to 2.x](#migrating-from-1x-to-2x)
- [Notes](#notes)
- [Roadmap](#roadmap)
- [Browser Support](#browser-support)

## Installation

Expand All @@ -14,26 +22,26 @@ npm install simple-observable-proxy

Browser
```
import { observable, observe } from 'simple-observable-proxy';
import { observable } from 'simple-observable-proxy';
```

## Basic Usage

Objects and arrays can also be observed and unobserved.

```js
import { observable, observe, unobserve } from 'simple-observable-proxy';
const stateChange = () => {
import { observable, ObservableEvents, on, off } from 'simple-observable-proxy';
const stateChange = state => {
console.log('stateChange()');
}
const state = observable({
test: 'test'
});
observe(state, stateChange);
on(state, ObservableEvents.change, stateChange);
state.test = 'test2'; // stateChange() will be called on RAF
window.requestAnimationFrame(() => {
console.log('state.test : ' + state.test);
unobserve(state, stateChange);
off(state, ObservableEvents.change, stateChange);
state.test = 'test3'; // stateChange() will not be called
console.log('state.test : ' + state.test);
});
Expand All @@ -42,7 +50,7 @@ window.requestAnimationFrame(() => {
It is possible to have multiple callbacks on the same observable. This can be useful in specific cases such as multiple components sharing state.

```js
import { observable, observe } from 'simple-observable-proxy';
import { observable, ObservableEvents, on } from 'simple-observable-proxy';
// create
const sharedState = observable([
{
Expand All @@ -55,66 +63,94 @@ const sharedState = observable([
}
]);

const sharedStateCallback1 = () => {
const sharedStateCallback1 = state => {
console.log('sharedStateCallback1()');
};

const sharedStateCallback2 = () => {
const sharedStateCallback2 = state => {
console.log('sharedStateCallback2()');
};

observe(sharedState, sharedStateCallback1);
observe(sharedState, sharedStateCallback2);
on(sharedState, ObservableEvents.change, sharedStateCallback1);
on(sharedState, ObservableEvents.change, sharedStateCallback2);
```

## Methods

### observable(plainObjectOrArray)
Converts a plain object or array to an instance of `Proxy` and returns it. If any other value is passed to the function it returns `false`.
### observable(arrayOrPlainObject: Observable): Observable
Converts an `Array` or plain `Object` (not an instance of a class) to an instance of `Proxy` and returns it. There are numerous cases where this function will throw an Error:

- If any value other than an `Array` or plain `Object` is passed.
- If the `Array` or plain `Object` contains an instance of a class.
- If the `Array` or plain `Object` contains an `Array` or plain `Object` that has already been passed to `observable()`.

### on(proxy: Observable, ObservableEventType: "change" | "destroy", callbackFn: (proxy: Observable) => void): boolean
Subscribes to either the change or destroy event using callbackFn, Returns `true` if successfully subscribed, or `false` in cases where the proxy or callback function is invalid, or the callback is already registered.

### observe(proxy, callbackFn)
Subscribes to proxy changes using callbackFn, Returns `true` if successfully subscribed, or `false` in cases where the proxy or callback function is invalid, or the callback is already registered.
### off(proxy: Observable, ObservableEventType: "change" | "destroy", callbackFn: (proxy: Observable) => void): boolean
Unsubscribes from either the change or destroy event using callbackFn. Returns `true` if successfully unsubscribed, or `false` in cases where the proxy or callback function is invalid.

### unobserve(proxy, callbackFn)
Unsubscribes from proxy changes using callbackFn. Returns `true` if successfully unsubscribed, or `false` in cases where the proxy or callback function is invalid.
### DEPRECATED observe(proxy: Observable, callbackFn: (proxy: Observable) => void): boolean
Shorthand method that calls observe(proxy, "change", callbackFn). Maintained for backwards compatibility with v1, but will be dropped in a future release.

### destroy(proxy)
### DEPRECATED unobserve(proxy: Observable, callbackFn: (proxy: Observable) => void): boolean
Shorthand method that calls unobserve(proxy, "change", callbackFn). Maintained for backwards compatibility with v1, but will be dropped in a future release.

### destroy(proxy: Observable): boolean
Cleans up the proxy. Returns `true` if successfully destroyed, or `false` in cases where the proxy has already been destroyed, or is not a valid proxy.

## Notes
## Migrating from 1.x to 2.x
In order to ensure that 1.x code does not break, the `observe` and `unobserve` methods have been retained as part of the 2.x codeset, but are marked as deprecated. If you wish to convert these to the 2.x syntax, you shoud modify your code as follows:

1. Where you import the `observe` and `unobserve` methods, instead import the `on` and `off` methods, and the `ObservableEvents` object.
2. Replace any calls to `observe(yourState, yourChangeCallback)` to `on(yourState, ObservableEvents.change, yourChangeCallback)`, and any calls to `unobserve(yourState, yourChangeCallback)` to `off(yourState, ObservableEvents.change, yourChangeCallback)`.

If you wish to recreate the deprecated `observe` and `unobserve` methods, you can add the following to your codeset.

Simple Observable Proxy maintains a list of sources to prevent observing the same object or array more than once. Attempting to do so will throw an error.
### TypeScript
```typescript
const observe = (observableProxy: Observable, callback: ObservableCallback): boolean => on(observableProxy, ObservableEvents.change, callback);
const unobserve = (observableProxy: Observable, callback: ObservableCallback): boolean => off(observableProxy, ObservableEvents.change, callback);
```

### JavaScript
```js
const observe = (observableProxy, callback) => on(observableProxy, ObservableEvents.change, callback);
const unobserve = (observableProxy, callback) => off(observableProxy, ObservableEvents.change, callback);
```

Attempting to nest one observable within another will throw an error.
## Notes

When multiple values are modified on an observable proxy only one notification is sent per callback on the next requestAnimationFrame. For example:
When a value is modified on an observable proxy, its updated values is available immediately. When multiple values are modified on the observable proxy each registered callback will be called only once using requestAnimationFrame in the browser, or after 16ms in a Node environment. For example:

```js
import { observable, observe } from 'simple-observable-proxy';
import { observable, ObservableEvents, on } from 'simple-observable-proxy';
const stateChange = () => {
console.log('state changed')
console.log('state changed callback')
};
const state = observable({
test: 'test',
test2: 'test2'
});
observe(state, stateChange);
on(state, ObservableEvents.change, stateChange);
state.test = 'test2'; // modify key
state.nested = [1, 2, 3]; // added key
delete state.test2; // delete key

// despite multiple changes to state, stateChange will
// only be called once on next requestAnimationFrame
// only be called once on next requestAnimationFrame or after 16ms
console.log('before state changed callback');

// output in the console should be
// 'before state changed callback'
// 'state changed callback'
```

## Roadmap

Version 2.0 will include the following changes:

- observable will throw errors when trying to observe a value other than an array or plain object

Version 3.0 will consider the following changes:
Version 3.0 will consider the following change:

- Support for Map and Set
- Instances of classes are observable

## Browser Support
Expand Down
45 changes: 45 additions & 0 deletions demo/multi-obserever.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Simple Observable Proxy Demo</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
Check the console :)
<script type="module">
import { observable, ObservableEvents, on } from '../dist/simple-observable-proxy.js';
// create
const sharedState = observable([
{
id: 1,
name: 'My first book.'
},
{
id: 2,
name: 'My second book.'
}
]);

const sharedStateCallback1 = state => {
console.log('sharedStateCallback1()');
};

const sharedStateCallback2 = state => {
console.log('sharedStateCallback2()');
};

on(sharedState, ObservableEvents.change, sharedStateCallback1);
on(sharedState, ObservableEvents.change, sharedStateCallback2);

sharedState.push({
id: 3,
name: 'My third book.'
});

// log: sharedStateCallback1();
// log: sharedStateCallback2();
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions demo/performance.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<body>
Check the console :)
<script type="module">
import { observable, observe, destroy } from '../dist/simple-observable-proxy.js';
import { observable, ObservableEvents, on, destroy } from '../dist/simple-observable-proxy.js';
const states = [];
const statesSize = 1000;
let observables = [];
Expand All @@ -30,7 +30,7 @@
const t0 = performance.now();
for( let i = 0; i < statesSize; i++ ) {
const observableObj = observable(states[i]);
observe(observableObj, () => {});
on(observableObj, ObservableEvents.change, () => {});
observables.push(observableObj);
}
const t1 = performance.now();
Expand Down
18 changes: 9 additions & 9 deletions demo/index.html → demo/single-observer.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
<body>
Check the console :)
<script type="module">
import { observable, observe, unobserve } from '../dist/simple-observable-proxy.js';
const stateChange = () => {
console.log('stateChange()');
import { observable, ObservableEvents, on, off } from '../dist/simple-observable-proxy.js';
const stateChange = state => {
console.log('stateChange()');
}
const state = observable({
test: 'test'
test: 'test'
});
observe(state, stateChange);
on(state, ObservableEvents.change, stateChange);
state.test = 'test2'; // stateChange() will be called on RAF
window.requestAnimationFrame(() => {
console.log('state.test : ' + state.test);
unobserve(state, stateChange);
state.test = 'test3'; // stateChange() will not be called
console.log('state.test : ' + state.test);
console.log('state.test : ' + state.test);
off(state, ObservableEvents.change, stateChange);
state.test = 'test3'; // stateChange() will not be called
console.log('state.test : ' + state.test);
});
</script>
</body>
Expand Down
25 changes: 19 additions & 6 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
export type PlainObject = {
export type Observable = {
[name: string]: any;
};
type ObservableCallback = () => void;
export declare const observable: (data: any) => PlainObject | boolean;
export declare const observe: (observableProxy: any, callback: ObservableCallback) => boolean;
export declare const unobserve: (observableProxy: any | PlainObject, callback: ObservableCallback) => boolean;
export declare const destroy: (observableProxy: any) => boolean;
export type ObservableCallback = (proxy: Observable) => void;
export declare const ObservableEvents: {
readonly change: "change";
readonly destroy: "destroy";
};
type ObservableEventKey = keyof typeof ObservableEvents;
export type ObservableEvent = typeof ObservableEvents[ObservableEventKey];
type ObservableCallbackObject = {
change: Set<ObservableCallback>;
destroy: Set<ObservableCallback>;
};
export type ObservableCallbackMap = Map<Observable, ObservableCallbackObject>;
export declare const observable: (data: Observable) => Observable;
export declare const on: (observableProxy: Observable, eventType: ObservableEventKey, callback: ObservableCallback) => boolean;
export declare const off: (observableProxy: Observable, eventType: ObservableEventKey, callback: ObservableCallback) => boolean;
export declare const observe: (observableProxy: Observable, callback: ObservableCallback) => boolean;
export declare const unobserve: (observableProxy: Observable, callback: ObservableCallback) => boolean;
export declare const destroy: (observableProxy: Observable) => boolean;
export {};
Loading

0 comments on commit 8d4c1c0

Please sign in to comment.