@joist/observable 2.0.0-beta.0 → 2.0.0-beta.12

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
@@ -6,16 +6,16 @@ Decorating a class with `@observable` means that instances of that class will BE
6
6
  #### Installation:
7
7
 
8
8
  ```BASH
9
- npm i @joist/observable@alpha
9
+ npm i @joist/observablebeta
10
10
  ```
11
11
 
12
12
  #### Example:
13
13
 
14
14
  ```TS
15
- import { observable, observer, OnChange, Changes } from '@joist/observable';
15
+ import { observable, observer, OnPropertyChanged, Changes } from '@joist/observable';
16
16
 
17
17
  @observable
18
- class State implements OnChange {
18
+ class State implements OnPropertyChanged {
19
19
  // Changes to these will trigger callback
20
20
  @observe todos: string[] = [];
21
21
  @observe userName?: string;
@@ -23,7 +23,7 @@ class State implements OnChange {
23
23
  // changes to this will not
24
24
  someValue: boolean = false;
25
25
 
26
- onChange(changes: Changes) {
26
+ onPropertyChanged(changes: Changes) {
27
27
  console.log(changes);
28
28
  // { todos: { value: ['Build Shit'], previousValue: [] }, userName: { value: 'Danny Blue', previousValue: undefined } }
29
29
  }
@@ -40,7 +40,7 @@ state.userName = 'Danny Blue'
40
40
  If you want to externally monitor your class for changes you can extend event target and dispatch events. (available in both node and the browser)
41
41
 
42
42
  ```TS
43
- import { observable, observer, OnChange, Changes } from '@joist/observable';
43
+ import { observable, observer, OnPropertyChanged, Changes } from '@joist/observable';
44
44
 
45
45
  class StateChangeEvent extends Event {
46
46
  consetructor(public changes: Changes) {
@@ -49,7 +49,7 @@ class StateChangeEvent extends Event {
49
49
  }
50
50
 
51
51
  @observable
52
- class State extends EventTarget implements OnChange {
52
+ class State extends EventTarget implements OnPropertyChanged {
53
53
  // Changes to these will trigger callback
54
54
  @observe todos: string[] = [];
55
55
  @observe userName?: string;
@@ -57,8 +57,8 @@ class State extends EventTarget implements OnChange {
57
57
  // changes to this will not
58
58
  someValue: boolean = false;
59
59
 
60
- onChange(changes: Changes) {
61
- this.dispatchEvent(new StateChangeEvent());
60
+ onPropertyChanged(changes: Changes) {
61
+ this.dispatchEvent(new StateChangeEvent(changes));
62
62
  }
63
63
  }
64
64
 
@@ -71,3 +71,31 @@ state.addEventListener('statechange', (e) => {
71
71
  state.todos = [...state.todos, 'Build Shit'];
72
72
  state.userName = 'Danny Blue'
73
73
  ```
74
+
75
+ #### Attributes
76
+
77
+ If you are using @observable with custom elements it is very likely that you will want to read from and write to attributes.
78
+ Joist accounts for this by giving you an `@attr` decorator.
79
+
80
+ ```TS
81
+ import { observable, observe, attr} from '@joist/observable';
82
+
83
+ @observable
84
+ class TestElement extends HTMLElement {
85
+ // reads as a string and writes directly to the name attribute
86
+ @observe @attr name = '';
87
+
88
+ // reads as a number and writes back a string
89
+ @observe
90
+ @attr({ read: Number })
91
+ count: number = 0;
92
+
93
+ // reads as a Date object and writes back a string
94
+ @observe
95
+ @attr<Date>({
96
+ read: (val) => new Date(val),
97
+ write: (val) => val.toString()
98
+ })
99
+ count = new Date();
100
+ }
101
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/observable",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0-beta.12",
4
4
  "main": "./target/build/lib.js",
5
5
  "module": "./target/build/lib.js",
6
6
  "exports": {
@@ -12,15 +12,16 @@
12
12
  "target/build"
13
13
  ],
14
14
  "sideEffects": false,
15
- "description": "Dependency Injection in ~800 bytes",
15
+ "description": "Monitor and respond to object changes",
16
16
  "repository": {
17
17
  "type": "git",
18
18
  "url": "git+https://github.com/deebloo/joist.git"
19
19
  },
20
20
  "keywords": [
21
21
  "TypeScript",
22
- "DI",
23
- "Dependency Injection"
22
+ "Observable",
23
+ "WebComponents",
24
+ "Reactive"
24
25
  ],
25
26
  "author": "deebloo",
26
27
  "license": "MIT",
@@ -34,5 +35,5 @@
34
35
  "test": "tsc -p tsconfig.test.json && wtr --config ../../wtr.config.mjs --port 8002",
35
36
  "build": "tsc -p tsconfig.build.json"
36
37
  },
37
- "gitHead": "c78191a2cab50ec09abee20f688b245d235948d1"
38
+ "gitHead": "1962273dca1744f0227fd76c143b07d93566e809"
38
39
  }
@@ -0,0 +1,8 @@
1
+ export interface AttributeParser<T> {
2
+ read(val: string): T;
3
+ write(val: T): string;
4
+ mapTo: string;
5
+ }
6
+ export declare type AttributeParsers = Record<string, AttributeParser<unknown>>;
7
+ export declare function defaultParser(mapTo: string): AttributeParser<boolean | string>;
8
+ export declare function propNameToAttrName(prop: string): string;
@@ -0,0 +1,19 @@
1
+ export function defaultParser(mapTo) {
2
+ return {
3
+ read(val) {
4
+ // if a boolean assume such
5
+ if (val === 'true' || val === 'false') {
6
+ return val === 'true';
7
+ }
8
+ return val;
9
+ },
10
+ write: String,
11
+ mapTo,
12
+ };
13
+ }
14
+ export function propNameToAttrName(prop) {
15
+ return prop
16
+ .split(/(?=[A-Z])/)
17
+ .join('-')
18
+ .toLowerCase();
19
+ }
@@ -0,0 +1,5 @@
1
+ import { AttributeParser } from './attribute-parsers';
2
+ export declare function getObservableAttributes(c: typeof HTMLElement): Array<string>;
3
+ export declare function getAttributeParsers<T extends typeof HTMLElement>(c: T): Record<string, AttributeParser<unknown>>;
4
+ export declare function attr<T>(p: Partial<AttributeParser<T>>): <E extends HTMLElement>(t: E, key: string) => void;
5
+ export declare function attr<T extends HTMLElement>(t: T, key: string): void;
@@ -0,0 +1,41 @@
1
+ import { defaultParser, propNameToAttrName, } from './attribute-parsers';
2
+ export function getObservableAttributes(c) {
3
+ return Reflect.get(c, 'observedAttributes') || [];
4
+ }
5
+ export function getAttributeParsers(c) {
6
+ const parsers = Reflect.get(c, 'attributeParsers') || {};
7
+ return parsers;
8
+ }
9
+ export function attr(targetOrParser, key) {
10
+ if (targetOrParser instanceof HTMLElement && typeof key === 'string') {
11
+ const attrName = propNameToAttrName(key);
12
+ return defineAttribute(targetOrParser, attrName, key);
13
+ }
14
+ return (target, key) => {
15
+ const parser = targetOrParser;
16
+ const attrName = propNameToAttrName(key);
17
+ defineAttribute(target, attrName, key);
18
+ const attributeParsers = Reflect.get(target.constructor, 'attributeParsers');
19
+ attributeParsers[attrName].read = parser.read || attributeParsers[attrName].read;
20
+ attributeParsers[attrName].write = parser.write || attributeParsers[attrName].write;
21
+ Reflect.set(target.constructor, 'attributeParsers', attributeParsers);
22
+ return void 0;
23
+ };
24
+ }
25
+ function defineAttribute(target, attrName, propName) {
26
+ const observedAttributes = Reflect.get(target.constructor, 'observedAttributes');
27
+ if (observedAttributes) {
28
+ observedAttributes.push(attrName);
29
+ }
30
+ else {
31
+ Reflect.set(target.constructor, 'observedAttributes', [attrName]);
32
+ }
33
+ const attributeParsers = Reflect.get(target.constructor, 'attributeParsers');
34
+ if (attributeParsers) {
35
+ attributeParsers[attrName] = defaultParser(propName);
36
+ }
37
+ else {
38
+ Reflect.set(target.constructor, 'attributeParsers', { [attrName]: defaultParser(propName) });
39
+ }
40
+ return void 0;
41
+ }
@@ -5,16 +5,16 @@ export declare class Change<T = any> {
5
5
  constructor(value: T, previousValue: T | undefined, firstChange: boolean);
6
6
  }
7
7
  export declare type Changes = Record<string | symbol, Change>;
8
- export interface OnChange {
9
- onChange(changes: Changes): void;
8
+ export interface OnPropertyChanged {
9
+ onPropertyChanged(changes: Changes): void;
10
10
  }
11
- export declare function readPropertyDefs(c: any): Array<string | symbol>;
11
+ export declare function getObservableProperties(c: any): Array<string | symbol>;
12
12
  export interface ObservableBase {
13
13
  propChanges: Changes;
14
14
  propChange: Promise<void> | null;
15
15
  initializedChanges: Set<string | symbol>;
16
- onChange?: (changes: Changes) => void;
17
16
  definePropChange(key: string | symbol, propChange: Change): Promise<void>;
17
+ onPropertyChanged?(changes: Changes): void;
18
18
  }
19
19
  export declare function observe(target: any, key: string): void;
20
20
  export declare function observable<T extends new (...args: any[]) => any>(Base: T): {
@@ -24,6 +24,9 @@ export declare function observable<T extends new (...args: any[]) => any>(Base:
24
24
  propChange: Promise<void> | null;
25
25
  initializedChanges: Set<string | symbol>;
26
26
  definePropChange: typeof definePropChange;
27
+ connectedCallback(this: HTMLElement & any): void;
28
+ attributeChangedCallback(this: HTMLElement & any, name: string, oldVal: string, newVal: string): void;
29
+ onPropertyChanged(changes: Changes): void;
27
30
  };
28
31
  } & T;
29
32
  declare function definePropChange(this: ObservableBase, key: string | symbol, propChange: Change): Promise<void>;
@@ -1,3 +1,5 @@
1
+ import { getAttributeParsers, getObservableAttributes } from './attribute';
2
+ import { propNameToAttrName } from './attribute-parsers';
1
3
  export class Change {
2
4
  constructor(value, previousValue, firstChange) {
3
5
  this.value = value;
@@ -5,17 +7,19 @@ export class Change {
5
7
  this.firstChange = firstChange;
6
8
  }
7
9
  }
8
- const PROPERTY_KEY = 'props';
9
- export function readPropertyDefs(c) {
10
- return c[PROPERTY_KEY] || {};
10
+ const PROPERTY_KEY = 'observedProperties';
11
+ export function getObservableProperties(c) {
12
+ return c[PROPERTY_KEY] || [];
11
13
  }
12
14
  export function observe(target, key) {
13
15
  target.constructor[PROPERTY_KEY] = target.constructor[PROPERTY_KEY] || [];
14
16
  target.constructor[PROPERTY_KEY].push(key);
15
17
  }
16
18
  export function observable(Base) {
17
- const props = readPropertyDefs(Base);
18
- const descriptors = createPropertyDescripors(props);
19
+ const properties = getObservableProperties(Base);
20
+ const attributes = getObservableAttributes(Base);
21
+ const parsers = getAttributeParsers(Base);
22
+ const descriptors = createPropertyDescripors(properties);
19
23
  return class Observable extends Base {
20
24
  constructor(...args) {
21
25
  super(...args);
@@ -23,16 +27,51 @@ export function observable(Base) {
23
27
  this.propChange = null;
24
28
  this.initializedChanges = new Set();
25
29
  this.definePropChange = definePropChange;
26
- initObservbale.call(this, descriptors);
30
+ for (let prop in descriptors) {
31
+ Reflect.set(this, createPrivateKey(prop), Reflect.get(this, prop));
32
+ }
33
+ Object.defineProperties(this, descriptors);
34
+ }
35
+ connectedCallback() {
36
+ for (let i = 0; i < attributes.length; i++) {
37
+ const key = attributes[i];
38
+ const { write, mapTo } = parsers[key];
39
+ if (this.getAttribute(key) === null) {
40
+ const propVal = Reflect.get(this, mapTo);
41
+ if (propVal !== undefined && propVal !== null && propVal !== '') {
42
+ this.setAttribute(key, write(propVal));
43
+ }
44
+ }
45
+ }
46
+ if (super.connectedCallback) {
47
+ super.connectedCallback();
48
+ }
49
+ }
50
+ attributeChangedCallback(name, oldVal, newVal) {
51
+ const { read, mapTo } = parsers[name];
52
+ Reflect.set(this, mapTo, read(newVal));
53
+ if (super.attributeChangedCallback) {
54
+ super.attributeChangedCallback(name, oldVal, newVal);
55
+ }
56
+ }
57
+ onPropertyChanged(changes) {
58
+ if (this instanceof HTMLElement) {
59
+ for (let change in changes) {
60
+ const attrName = propNameToAttrName(change);
61
+ if (attributes.includes(attrName)) {
62
+ const value = parsers[attrName].write(changes[change].value);
63
+ if (value !== this.getAttribute(attrName)) {
64
+ this.setAttribute(attrName, value);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ if (super.onPropertyChanged) {
70
+ super.onPropertyChanged(changes);
71
+ }
27
72
  }
28
73
  };
29
74
  }
30
- function initObservbale(descriptors) {
31
- for (let prop in descriptors) {
32
- Reflect.set(this, createPrivateKey(prop), Reflect.get(this, prop));
33
- }
34
- Object.defineProperties(this, descriptors);
35
- }
36
75
  function createPrivateKey(key) {
37
76
  return `__${key.toString()}`;
38
77
  }
@@ -65,19 +104,20 @@ function definePropChange(key, propChange) {
65
104
  // If there is no previous change defined set it up
66
105
  this.propChange = Promise.resolve().then(() => {
67
106
  // run onPropChanges here. This makes sure we capture all changes
68
- // keep track of whether or not this is the first time a given property has changes
107
+ const changes = {};
108
+ // Copy changes and keep track of whether or not this is the first time a given property has changes
69
109
  for (let change in this.propChanges) {
70
- this.propChanges[change].firstChange = !this.initializedChanges.has(change);
110
+ changes[change] = this.propChanges[change];
111
+ changes[change].firstChange = !this.initializedChanges.has(change);
71
112
  this.initializedChanges.add(change);
72
113
  }
73
- if (this.onChange) {
74
- this.onChange(this.propChanges);
75
- }
76
- // reset for next time
77
- this.propChanges = {};
114
+ // clear out before calling to account for changes made INSIDE of the onPropertyChanged callback
78
115
  this.propChange = null;
116
+ this.propChanges = {};
117
+ if (this.onPropertyChanged) {
118
+ this.onPropertyChanged(changes);
119
+ }
79
120
  });
80
121
  }
81
122
  return this.propChange;
82
123
  }
83
- //# sourceMappingURL=observable.js.map
@@ -1 +1,2 @@
1
- export { observable, Change, OnChange, observe, Changes } from './lib/observable';
1
+ export { observable, Change, OnPropertyChanged, observe, Changes } from './lib/observable';
2
+ export { attr } from './lib/attribute';
@@ -1,2 +1,2 @@
1
1
  export { observable, Change, observe } from './lib/observable';
2
- //# sourceMappingURL=lib.js.map
2
+ export { attr } from './lib/attribute';
@@ -1 +0,0 @@
1
- {"version":3,"file":"observable.js","sourceRoot":"","sources":["../../../lib/observable.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"lib.js","sourceRoot":"","sources":["../../lib.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAY,OAAO,EAAW,MAAM,kBAAkB,CAAC"}