@joist/observable 3.0.6 → 3.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/observable",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -13,6 +13,7 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
+ "src",
16
17
  "target"
17
18
  ],
18
19
  "sideEffects": false,
@@ -0,0 +1,22 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { MetaData } from './meta.js';
3
+
4
+ describe('meta:meta', () => {
5
+ it('should return default metadata', () => {
6
+ const key = {};
7
+ const data = new MetaData().read(key);
8
+
9
+ expect(data).to.deep.equal({
10
+ changes: new Set(),
11
+ upgradable: new Map(),
12
+ scheduler: null,
13
+ });
14
+ });
15
+
16
+ it('should return the same metadata object after init', () => {
17
+ const key = {};
18
+ const data = new MetaData();
19
+
20
+ expect(data.read(key)).to.equal(data.read(key));
21
+ });
22
+ });
@@ -0,0 +1,23 @@
1
+ export type EffectFn = (changes: Set<string | symbol>) => void;
2
+
3
+ export class ObservableMetaData {
4
+ scheduler: Promise<void> | null = null;
5
+ upgradable = new Map<string | symbol, unknown>();
6
+ changes = new Set<string | symbol>();
7
+ }
8
+
9
+ export class MetaData {
10
+ #data = new WeakMap<object, ObservableMetaData>();
11
+
12
+ read<T extends object>(value: T) {
13
+ if (this.#data.has(value)) {
14
+ return this.#data.get(value) as ObservableMetaData;
15
+ }
16
+
17
+ const data = new ObservableMetaData();
18
+
19
+ this.#data.set(value, data);
20
+
21
+ return data;
22
+ }
23
+ }
@@ -0,0 +1,106 @@
1
+ import { expect, fixture, html } from '@open-wc/testing';
2
+
3
+ import { effect, observe } from './observe.js';
4
+
5
+ describe('observable: observe()', () => {
6
+ it('should work with static accessors', (done) => {
7
+ class Counter {
8
+ @observe static accessor value = 0;
9
+
10
+ @effect static onPropChanged() {
11
+ expect(Counter.value).to.equal(1);
12
+
13
+ done();
14
+ }
15
+ }
16
+
17
+ expect(Counter.value).to.equal(0);
18
+
19
+ Counter.value++;
20
+
21
+ expect(Counter.value).to.equal(1);
22
+ });
23
+
24
+ it('should work with instance accessors', (done) => {
25
+ class Counter {
26
+ @observe accessor value = 0;
27
+
28
+ // confirm it works with private methods
29
+ // @ts-ignore
30
+ @effect #onChange() {
31
+ expect(this.value).to.equal(1);
32
+
33
+ done();
34
+ }
35
+ }
36
+
37
+ const counter = new Counter();
38
+
39
+ expect(counter.value).to.equal(0);
40
+
41
+ counter.value++;
42
+
43
+ expect(counter.value).to.equal(1);
44
+ });
45
+
46
+ it('should return a set of changed props', (done) => {
47
+ class Counter {
48
+ @observe accessor value = 0;
49
+
50
+ @effect onChange(changes: Set<symbol | string>) {
51
+ expect(changes.has('value')).to.be.true;
52
+
53
+ done();
54
+ }
55
+ }
56
+
57
+ const counter = new Counter();
58
+ counter.value++;
59
+ });
60
+
61
+ it('should work as an even emitter', (done) => {
62
+ class Counter extends EventTarget {
63
+ @observe accessor value = 0;
64
+
65
+ @effect onChange() {
66
+ this.dispatchEvent(new Event('changed'));
67
+ }
68
+ }
69
+
70
+ const counter = new Counter();
71
+
72
+ counter.addEventListener('changed', () => {
73
+ expect(counter.value).to.equal(1);
74
+
75
+ done();
76
+ });
77
+
78
+ counter.value++;
79
+ });
80
+
81
+ it('should upgrade custom elements', (done) => {
82
+ class Counter extends HTMLElement {
83
+ @observe accessor value = 0;
84
+
85
+ constructor() {
86
+ super();
87
+
88
+ expect(this.value).to.equal(100);
89
+ }
90
+
91
+ @effect onChange() {
92
+ expect(this.value).to.equal(101);
93
+
94
+ done();
95
+ }
96
+ }
97
+
98
+ fixture<any>(html`<observable-1></observable-1>`).then((el) => {
99
+ el.value = 100;
100
+
101
+ customElements.define('observable-1', Counter);
102
+
103
+ el.value++;
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,76 @@
1
+ // ensure that the metadata symbol exists
2
+ (Symbol as any).metadata ??= Symbol('Symbol.metadata');
3
+
4
+ import { EffectFn, MetaData } from './meta.js';
5
+
6
+ const metaData = new MetaData();
7
+
8
+ export interface ObservableCtx {
9
+ metadata: {
10
+ effects?: Set<EffectFn>;
11
+ };
12
+ }
13
+
14
+ export function observe<This extends object, Value>(
15
+ base: ClassAccessorDecoratorTarget<This, Value>,
16
+ ctx: ClassAccessorDecoratorContext<This, Value> & ObservableCtx
17
+ ): ClassAccessorDecoratorResult<This, Value> {
18
+ // handle upgradable values (specifically custom elements)
19
+ ctx.addInitializer(function (this: This) {
20
+ const meta = metaData.read(this);
21
+
22
+ let value: Value | undefined;
23
+
24
+ // attempt to read value.
25
+ try {
26
+ value = ctx.access.get(this);
27
+ } catch {}
28
+
29
+ if (value) {
30
+ // if there is a value, delete it and cache it for init
31
+ delete (<any>this)[ctx.name];
32
+
33
+ meta.upgradable.set(ctx.name, value);
34
+ }
35
+ });
36
+
37
+ return {
38
+ init(value) {
39
+ const meta = metaData.read(this);
40
+
41
+ if (meta.upgradable.has(ctx.name)) {
42
+ return meta.upgradable.get(ctx.name) as Value;
43
+ }
44
+
45
+ return value;
46
+ },
47
+ set(value) {
48
+ const meta = metaData.read(this);
49
+
50
+ if (meta.scheduler === null) {
51
+ meta.scheduler = Promise.resolve().then(() => {
52
+ if (ctx.metadata.effects) {
53
+ for (let effect of ctx.metadata.effects) {
54
+ effect.call(this, meta.changes);
55
+ }
56
+ }
57
+
58
+ meta.scheduler = null;
59
+ meta.changes.clear();
60
+ });
61
+ }
62
+
63
+ meta.changes.add(ctx.name);
64
+
65
+ base.set.call(this, value);
66
+ },
67
+ };
68
+ }
69
+
70
+ export function effect<T extends object>(
71
+ value: EffectFn,
72
+ ctx: ClassMethodDecoratorContext<T> & ObservableCtx
73
+ ) {
74
+ ctx.metadata.effects ??= new Set();
75
+ ctx.metadata.effects.add(value);
76
+ }
package/src/lib.ts ADDED
@@ -0,0 +1 @@
1
+ export { observe, effect } from './lib/observe.js';