grainjs 0.1.0 → 1.0.2

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.
Files changed (164) hide show
  1. package/README.md +54 -9
  2. package/dist/cjs/index.d.ts +6 -2
  3. package/dist/cjs/index.js +24 -17
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/lib/PriorityQueue.d.ts +1 -1
  6. package/dist/cjs/lib/PriorityQueue.js +1 -0
  7. package/dist/cjs/lib/PriorityQueue.js.map +1 -1
  8. package/dist/cjs/lib/_computed_queue.d.ts +18 -0
  9. package/dist/cjs/lib/_computed_queue.js +6 -1
  10. package/dist/cjs/lib/_computed_queue.js.map +1 -1
  11. package/dist/cjs/lib/binding.d.ts +16 -10
  12. package/dist/cjs/lib/binding.js +22 -27
  13. package/dist/cjs/lib/binding.js.map +1 -1
  14. package/dist/cjs/lib/browserGlobals.d.ts +4 -1
  15. package/dist/cjs/lib/browserGlobals.js +2 -0
  16. package/dist/cjs/lib/browserGlobals.js.map +1 -1
  17. package/dist/cjs/lib/computed.d.ts +11 -7
  18. package/dist/cjs/lib/computed.js +16 -0
  19. package/dist/cjs/lib/computed.js.map +1 -1
  20. package/dist/cjs/lib/dispose.d.ts +106 -14
  21. package/dist/cjs/lib/dispose.js +76 -11
  22. package/dist/cjs/lib/dispose.js.map +1 -1
  23. package/dist/cjs/lib/dom.d.ts +21 -17
  24. package/dist/cjs/lib/dom.js +33 -26
  25. package/dist/cjs/lib/dom.js.map +1 -1
  26. package/dist/cjs/lib/domComponent.d.ts +71 -0
  27. package/dist/cjs/lib/domComponent.js +15 -0
  28. package/dist/cjs/lib/domComponent.js.map +1 -0
  29. package/dist/cjs/lib/domComputed.d.ts +89 -0
  30. package/dist/cjs/lib/domComputed.js +92 -0
  31. package/dist/cjs/lib/domComputed.js.map +1 -0
  32. package/dist/cjs/lib/{_domDispose.d.ts → domDispose.d.ts} +12 -2
  33. package/dist/cjs/lib/{_domDispose.js → domDispose.js} +21 -8
  34. package/dist/cjs/lib/domDispose.js.map +1 -0
  35. package/dist/cjs/lib/{_domForEach.d.ts → domForEach.d.ts} +2 -2
  36. package/dist/cjs/lib/domForEach.js +72 -0
  37. package/dist/cjs/lib/domForEach.js.map +1 -0
  38. package/dist/cjs/lib/{_domImpl.d.ts → domImpl.d.ts} +15 -12
  39. package/dist/cjs/lib/{_domImpl.js → domImpl.js} +23 -6
  40. package/dist/cjs/lib/domImpl.js.map +1 -0
  41. package/dist/cjs/lib/{_domMethods.d.ts → domMethods.d.ts} +27 -62
  42. package/dist/cjs/lib/{_domMethods.js → domMethods.js} +21 -76
  43. package/dist/cjs/lib/domMethods.js.map +1 -0
  44. package/dist/cjs/lib/domevent.d.ts +32 -21
  45. package/dist/cjs/lib/domevent.js +33 -12
  46. package/dist/cjs/lib/domevent.js.map +1 -1
  47. package/dist/cjs/lib/emit.d.ts +25 -2
  48. package/dist/cjs/lib/emit.js +3 -1
  49. package/dist/cjs/lib/emit.js.map +1 -1
  50. package/dist/cjs/lib/kowrap.d.ts +45 -3
  51. package/dist/cjs/lib/kowrap.js +93 -10
  52. package/dist/cjs/lib/kowrap.js.map +1 -1
  53. package/dist/cjs/lib/obsArray.d.ts +8 -8
  54. package/dist/cjs/lib/obsArray.js +1 -0
  55. package/dist/cjs/lib/obsArray.js.map +1 -1
  56. package/dist/cjs/lib/observable.d.ts +6 -1
  57. package/dist/cjs/lib/observable.js +11 -2
  58. package/dist/cjs/lib/observable.js.map +1 -1
  59. package/dist/cjs/lib/pureComputed.d.ts +3 -3
  60. package/dist/cjs/lib/pureComputed.js +2 -1
  61. package/dist/cjs/lib/pureComputed.js.map +1 -1
  62. package/dist/cjs/lib/styled.d.ts +76 -11
  63. package/dist/cjs/lib/styled.js +55 -23
  64. package/dist/cjs/lib/styled.js.map +1 -1
  65. package/dist/cjs/lib/subscribe.d.ts +15 -6
  66. package/dist/cjs/lib/subscribe.js +6 -2
  67. package/dist/cjs/lib/subscribe.js.map +1 -1
  68. package/dist/cjs/lib/util.js +1 -0
  69. package/dist/cjs/lib/util.js.map +1 -1
  70. package/dist/cjs/lib/widgets/input.d.ts +2 -2
  71. package/dist/cjs/lib/widgets/input.js +2 -2
  72. package/dist/cjs/lib/widgets/input.js.map +1 -1
  73. package/dist/cjs/lib/widgets/select.d.ts +1 -1
  74. package/dist/cjs/lib/widgets/select.js +1 -0
  75. package/dist/cjs/lib/widgets/select.js.map +1 -1
  76. package/dist/esm/index.js +6 -2
  77. package/dist/esm/index.js.map +1 -1
  78. package/dist/esm/lib/PriorityQueue.js.map +1 -1
  79. package/dist/esm/lib/_computed_queue.js +5 -1
  80. package/dist/esm/lib/_computed_queue.js.map +1 -1
  81. package/dist/esm/lib/binding.js +20 -27
  82. package/dist/esm/lib/binding.js.map +1 -1
  83. package/dist/esm/lib/browserGlobals.js +1 -0
  84. package/dist/esm/lib/browserGlobals.js.map +1 -1
  85. package/dist/esm/lib/computed.js +15 -0
  86. package/dist/esm/lib/computed.js.map +1 -1
  87. package/dist/esm/lib/dispose.js +74 -11
  88. package/dist/esm/lib/dispose.js.map +1 -1
  89. package/dist/esm/lib/dom.js +21 -17
  90. package/dist/esm/lib/dom.js.map +1 -1
  91. package/dist/esm/lib/domComponent.js +11 -0
  92. package/dist/esm/lib/domComponent.js.map +1 -0
  93. package/dist/esm/lib/domComputed.js +84 -0
  94. package/dist/esm/lib/domComputed.js.map +1 -0
  95. package/dist/esm/lib/{_domDispose.js → domDispose.js} +19 -8
  96. package/dist/esm/lib/domDispose.js.map +1 -0
  97. package/dist/esm/lib/domForEach.js +68 -0
  98. package/dist/esm/lib/domForEach.js.map +1 -0
  99. package/dist/esm/lib/{_domImpl.js → domImpl.js} +20 -4
  100. package/dist/esm/lib/domImpl.js.map +1 -0
  101. package/dist/esm/lib/{_domMethods.js → domMethods.js} +8 -63
  102. package/dist/esm/lib/domMethods.js.map +1 -0
  103. package/dist/esm/lib/domevent.js +30 -11
  104. package/dist/esm/lib/domevent.js.map +1 -1
  105. package/dist/esm/lib/emit.js +2 -1
  106. package/dist/esm/lib/emit.js.map +1 -1
  107. package/dist/esm/lib/kowrap.js +90 -10
  108. package/dist/esm/lib/kowrap.js.map +1 -1
  109. package/dist/esm/lib/obsArray.js.map +1 -1
  110. package/dist/esm/lib/observable.js +9 -1
  111. package/dist/esm/lib/observable.js.map +1 -1
  112. package/dist/esm/lib/pureComputed.js +1 -1
  113. package/dist/esm/lib/pureComputed.js.map +1 -1
  114. package/dist/esm/lib/styled.js +52 -22
  115. package/dist/esm/lib/styled.js.map +1 -1
  116. package/dist/esm/lib/subscribe.js +5 -2
  117. package/dist/esm/lib/subscribe.js.map +1 -1
  118. package/dist/esm/lib/util.js.map +1 -1
  119. package/dist/esm/lib/widgets/input.js +1 -2
  120. package/dist/esm/lib/widgets/input.js.map +1 -1
  121. package/dist/esm/lib/widgets/select.js.map +1 -1
  122. package/dist/grain-full.debug.js +1627 -1222
  123. package/dist/grain-full.min.js +1 -1
  124. package/dist/grain-full.min.js.map +1 -1
  125. package/index.ts +6 -2
  126. package/lib/_computed_queue.ts +7 -1
  127. package/lib/binding.ts +33 -28
  128. package/lib/browserGlobals.ts +3 -1
  129. package/lib/computed.ts +37 -7
  130. package/lib/dispose.ts +81 -33
  131. package/lib/dom.ts +24 -18
  132. package/lib/domComponent.ts +89 -0
  133. package/lib/domComputed.ts +146 -0
  134. package/lib/{_domDispose.ts → domDispose.ts} +26 -8
  135. package/lib/{_domForEach.ts → domForEach.ts} +12 -11
  136. package/lib/{_domImpl.ts → domImpl.ts} +36 -30
  137. package/lib/{_domMethods.ts → domMethods.ts} +33 -103
  138. package/lib/domevent.ts +59 -22
  139. package/lib/emit.ts +2 -1
  140. package/lib/kowrap.ts +109 -11
  141. package/lib/obsArray.ts +2 -2
  142. package/lib/observable.ts +10 -2
  143. package/lib/pureComputed.ts +7 -6
  144. package/lib/styled.ts +65 -39
  145. package/lib/subscribe.ts +24 -8
  146. package/lib/widgets/input.ts +9 -7
  147. package/lib/widgets/select.ts +3 -3
  148. package/package.json +41 -42
  149. package/dist/cjs/lib/_domComponent.d.ts +0 -84
  150. package/dist/cjs/lib/_domComponent.js +0 -160
  151. package/dist/cjs/lib/_domComponent.js.map +0 -1
  152. package/dist/cjs/lib/_domDispose.js.map +0 -1
  153. package/dist/cjs/lib/_domForEach.js +0 -71
  154. package/dist/cjs/lib/_domForEach.js.map +0 -1
  155. package/dist/cjs/lib/_domImpl.js.map +0 -1
  156. package/dist/cjs/lib/_domMethods.js.map +0 -1
  157. package/dist/esm/lib/_domComponent.js +0 -155
  158. package/dist/esm/lib/_domComponent.js.map +0 -1
  159. package/dist/esm/lib/_domDispose.js.map +0 -1
  160. package/dist/esm/lib/_domForEach.js +0 -68
  161. package/dist/esm/lib/_domForEach.js.map +0 -1
  162. package/dist/esm/lib/_domImpl.js.map +0 -1
  163. package/dist/esm/lib/_domMethods.js.map +0 -1
  164. package/lib/_domComponent.ts +0 -167
package/index.ts CHANGED
@@ -1,10 +1,14 @@
1
- export {Computed, computed} from './lib/computed';
1
+ export * from './lib/binding';
2
+ export * from './lib/computed';
2
3
  export * from './lib/dispose';
3
4
  export * from './lib/dom';
4
5
  export * from './lib/emit';
5
6
  export * from './lib/kowrap';
6
7
  export * from './lib/obsArray';
7
8
  export * from './lib/observable';
9
+ export * from './lib/pureComputed';
8
10
  export * from './lib/styled';
9
- export {ISubscribable, Subscription, subscribe} from './lib/subscribe';
11
+ export * from './lib/subscribe';
10
12
  export * from './lib/util';
13
+ export * from './lib/widgets/input';
14
+ export * from './lib/widgets/select';
@@ -24,7 +24,7 @@ import {PriorityQueue} from './PriorityQueue';
24
24
  */
25
25
  export class DepItem {
26
26
  public static isPrioritySmaller(a: DepItem, b: DepItem): boolean {
27
- return a._priority < b._priority;
27
+ return a._priority < b._priority || (a._priority === b._priority && a._creation < b._creation);
28
28
  }
29
29
 
30
30
  private _priority: number = 0;
@@ -32,6 +32,9 @@ export class DepItem {
32
32
  private _callback: () => void;
33
33
  private _context?: object;
34
34
 
35
+ // Order of creation, used for ordering items at same priority.
36
+ private _creation: number = ++_nextCreationNum;
37
+
35
38
  /**
36
39
  * Callback should call depItem.useDep(dep) for each DepInput it depends on.
37
40
  */
@@ -73,6 +76,9 @@ export class DepItem {
73
76
  // The main compute queue.
74
77
  const queue = new PriorityQueue<DepItem>(DepItem.isPrioritySmaller);
75
78
 
79
+ // Counter for creation order, used to create a stable ordering of DepItems at same priority.
80
+ let _nextCreationNum = 0;
81
+
76
82
  // Array to keep track of items recomputed during this call to compute(). It could be a local
77
83
  // variable in compute(), but is made global to minimize allocations.
78
84
  const _seen: any[] = [];
package/lib/binding.ts CHANGED
@@ -5,18 +5,14 @@
5
5
 
6
6
  import {computed} from './computed';
7
7
  import {IDisposable} from './dispose';
8
- import {Observable} from './observable';
9
- import {UseCB} from './subscribe';
8
+ import {autoDisposeElem} from './domDispose';
9
+ import {IKnockoutReadObservable, InferKoType} from './kowrap';
10
+ import {BaseObservable} from './observable';
11
+ import {subscribe, UseCBOwner} from './subscribe';
10
12
 
11
- export type BindableValue<T> = Observable<T> | ComputedCallback<T> | T | IKnockoutObservable<T>;
13
+ export type BindableValue<T> = BaseObservable<T> | ComputedCallback<T> | T | IKnockoutReadObservable<T>;
12
14
 
13
- export type ComputedCallback<T> = (use: UseCB, ...args: any[]) => T;
14
-
15
- export interface IKnockoutObservable<T> {
16
- (): T;
17
- peek(): T;
18
- subscribe(callback: (newValue: T) => void, target?: any, event?: "change"): IDisposable;
19
- }
15
+ export type ComputedCallback<T> = (use: UseCBOwner, ...args: any[]) => T;
20
16
 
21
17
  /**
22
18
  * Subscribes a callback to valueObs, which may be one a plain value, an observable, a knockout
@@ -29,20 +25,20 @@ export interface IKnockoutObservable<T> {
29
25
  *
30
26
  * Returns an object which should be disposed to remove the created subscriptions, or null.
31
27
  */
32
- export function subscribe<T>(valueObs: BindableValue<T>,
33
- callback: (newVal: T, oldVal?: T) => void): IDisposable|null {
28
+ // The overload below is annoying, but needed for correct type inference; see test/types/kowrap.ts.
29
+ export function subscribeBindable<KObs extends IKnockoutReadObservable<any>>(
30
+ valueObs: KObs, callback: (val: InferKoType<KObs>) => void): IDisposable|null;
31
+ export function subscribeBindable<T>(
32
+ valueObs: BindableValue<T>, callback: (val: T) => void): IDisposable|null;
33
+ export function subscribeBindable<T>(
34
+ valueObs: BindableValue<T>, callback: (val: T) => void): IDisposable|null {
34
35
  // A plain function (to make a computed from), or a knockout observable.
35
36
  if (typeof valueObs === 'function') {
36
37
  // Knockout observable.
37
- const koValue = valueObs as IKnockoutObservable<T>;
38
+ const koValue = valueObs as IKnockoutReadObservable<T>;
38
39
  if (typeof koValue.peek === 'function') {
39
- let savedValue = koValue.peek();
40
- const sub = koValue.subscribe((val: T) => {
41
- const old = savedValue;
42
- savedValue = val;
43
- callback(val, old);
44
- });
45
- callback(savedValue, undefined);
40
+ const sub = koValue.subscribe((val) => callback(val));
41
+ callback(koValue.peek());
46
42
  return sub;
47
43
  }
48
44
 
@@ -50,19 +46,28 @@ export function subscribe<T>(valueObs: BindableValue<T>,
50
46
  // let sub = subscribe(use => callback(valueObs(use)));
51
47
  // The difference is that when valueObs() evaluates to unchanged value, callback would be
52
48
  // called in the version above, but not in the version below.
53
- const comp = computed(valueObs);
54
- comp.addListener(callback);
55
- callback(comp.get(), undefined);
49
+ const comp = computed(valueObs as ComputedCallback<T>);
50
+ comp.addListener((val) => callback(val));
51
+ callback(comp.get());
56
52
  return comp; // Disposing this will dispose its one listener.
57
53
  }
58
54
 
59
55
  // An observable.
60
- if (valueObs instanceof Observable) {
61
- const sub = valueObs.addListener(callback);
62
- callback(valueObs.get(), undefined);
63
- return sub;
56
+ if (valueObs instanceof BaseObservable) {
57
+ // Use subscribe() rather than addListener(), so that bundling of changes (implicit and with
58
+ // bundleChanges()) is respected. This matters when callback also uses observables.
59
+ return subscribe(valueObs, (use, val) => callback(val));
64
60
  }
65
61
 
66
- callback(valueObs, undefined);
62
+ callback(valueObs);
67
63
  return null;
68
64
  }
65
+
66
+ /**
67
+ * Subscribes a callback to valueObs (which may be a value, observable, or function) using
68
+ * subscribe(), and disposes the subscription with the passed-in element.
69
+ */
70
+ export function subscribeElem<T>(elem: Node, valueObs: BindableValue<T>,
71
+ callback: (newVal: T, oldVal?: T) => void): void {
72
+ autoDisposeElem(elem, subscribeBindable(valueObs, callback));
73
+ }
@@ -27,6 +27,8 @@ export interface IBrowserGlobals {
27
27
  window: typeof window;
28
28
  }
29
29
 
30
+ export interface IBrowserGlobalsLax extends IBrowserGlobals {window: any};
31
+
30
32
  function _updateGlobals(dest: IBrowserGlobals, source: IBrowserGlobals): void {
31
33
  dest.DocumentFragment = source.DocumentFragment;
32
34
  dest.Element = source.Element;
@@ -48,7 +50,7 @@ const _globalsStack: IBrowserGlobals[] = [initial];
48
50
  /**
49
51
  * Replace globals with those from the given object. Use popGlobals() to restore previous values.
50
52
  */
51
- export function pushGlobals(globals: IBrowserGlobals): void {
53
+ export function pushGlobals(globals: IBrowserGlobalsLax): void {
52
54
  _globalsStack.push(globals);
53
55
  _updateGlobals(G, globals);
54
56
  }
package/lib/computed.ts CHANGED
@@ -35,21 +35,51 @@
35
35
  */
36
36
 
37
37
  import {DepItem} from './_computed_queue';
38
- import {IDisposableOwner} from './dispose';
38
+ import {IDisposableOwnerT, setDisposeOwner} from './dispose';
39
39
  import {BaseObservable as Obs, Observable} from './observable';
40
- import {ISubscribable, Subscription} from './subscribe';
40
+ import {ISubscribable, Subscription, UseCBOwner as UseCB} from './subscribe';
41
41
 
42
42
  function _noWrite(): never {
43
43
  throw new Error("Can't write to non-writable computed");
44
44
  }
45
45
 
46
- // The generic type for the use() function that callbacks get.
47
- export interface UseCB { // tslint:disable-line:interface-name
48
- <U>(obs: Obs<U>): U;
49
- owner: IDisposableOwner;
50
- }
46
+ type Owner<T> = IDisposableOwnerT<Computed<T>>|null;
51
47
 
52
48
  export class Computed<T> extends Observable<T> {
49
+ // Still need repetitive declarations to support varargs that are not the final argument.
50
+ public static create<T>(
51
+ owner: Owner<T>, cb: (use: UseCB) => T): Computed<T>;
52
+ public static create<T, A>(
53
+ owner: Owner<T>, a: Obs<A>,
54
+ cb: (use: UseCB, a: A) => T): Computed<T>;
55
+ public static create<T, A, B>(
56
+ owner: Owner<T>, a: Obs<A>, b: Obs<B>,
57
+ cb: (use: UseCB, a: A, b: B) => T): Computed<T>;
58
+ public static create<T, A, B, C>(
59
+ owner: Owner<T>, a: Obs<A>, b: Obs<B>, c: Obs<C>,
60
+ cb: (use: UseCB, a: A, b: B, c: C) => T): Computed<T>;
61
+ public static create<T, A, B, C, D>(
62
+ owner: Owner<T>, a: Obs<A>, b: Obs<B>, c: Obs<C>, d: Obs<D>,
63
+ cb: (use: UseCB, a: A, b: B, c: C, d: D) => T): Computed<T>;
64
+ public static create<T, A, B, C, D, E>(
65
+ owner: Owner<T>, a: Obs<A>, b: Obs<B>, c: Obs<C>, d: Obs<D>, e: Obs<E>,
66
+ cb: (use: UseCB, a: A, b: B, c: C, d: D, e: E) => T): Computed<T>;
67
+
68
+ /**
69
+ * Creates a new Computed, owned by the given owner.
70
+ * @param owner: Object to own this Computed, or null to handle disposal manually.
71
+ * @param ...observables: Zero or more observables on which this computes depends. The callback
72
+ * will get called when any of these changes.
73
+ * @param callback: Read callback that will be called with (use, ...values),
74
+ * i.e. the `use` function and values for all of the ...observables. The callback is called
75
+ * immediately and whenever any dependency changes.
76
+ * @returns {Computed} The newly created computed observable.
77
+ */
78
+ public static create<T>(owner: IDisposableOwnerT<Computed<T>>|null, ...args: any[]): Computed<T> {
79
+ const readCb = args.pop();
80
+ return setDisposeOwner(owner, new Computed<T>(readCb, args));
81
+ }
82
+
53
83
  private _callback: (use: UseCB, ...args: any[]) => T;
54
84
  private _write: (value: T) => void;
55
85
  private _sub: Subscription;
package/lib/dispose.ts CHANGED
@@ -45,9 +45,29 @@
45
45
  * this._holder.clear(); // disposes contained object
46
46
  * this._holder.release(); // releases contained object
47
47
  *
48
+ * If you need a container for multiple objects and dispose them all together, use a MultiHolder:
49
+ * this._mholder = MultiHolder.create(null);
50
+ * Bar.create(this._mholder, 1); // create new Bar(1)
51
+ * Bar.create(this._mholder, 2); // create new Bar(2)
52
+ * this._mholder.dispose(); // disposes both objects
53
+ *
48
54
  * If creating your own class with a dispose() method, do NOT throw exceptions from dispose().
49
55
  * These cannot be handled properly in all cases. Read here about the same issue in C++:
50
56
  * http://bin-login.name/ftp/pub/docs/programming_languages/cpp/cffective_cpp/MAGAZINE/SU_FRAME.HTM#destruct
57
+ *
58
+ * Using a parametrized (generic) class as a Disposable is tricky. E.g.
59
+ * class Bar<T> extends Disposable { ... }
60
+ * // Bar<T>.create(...) <-- doesn't work
61
+ * // Bar.create<T>(...) <-- doesn't work
62
+ * // Bar.create(...) <-- works, but with {} for Bar's type parameters
63
+ *
64
+ * The solution is to expose the constructor type using a helper method:
65
+ * class Bar<T> extends Disposable {
66
+ * // Note the tuple below which must match the constructor parameters of Bar<U>.
67
+ * public static ctor<U>(): IDisposableCtor<Bar<U>, [U, boolean]> { return this; }
68
+ * constructor(a: T, b: boolean) { ... }
69
+ * }
70
+ * Bar.ctor<T>().create(...) // <-- works, creates Bar<T>, and does type-checking!
51
71
  */
52
72
 
53
73
  import {LLink} from './emit';
@@ -84,6 +104,15 @@ const _noopOwner: IDisposableOwner = {
84
104
  // is used by create() for the safe creation of Disposables.
85
105
  let _defaultDisposableOwner = _noopOwner;
86
106
 
107
+ /**
108
+ * The static portion of class Disposable.
109
+ */
110
+ export interface IDisposableCtor<Derived, CtorArgs extends any[]> {
111
+ new(...args: CtorArgs): Derived;
112
+ create<T extends new(...args: any[]) => any>(
113
+ this: T, owner: IDisposableOwnerT<InstanceType<T>>|null, ...args: ConstructorParameters<T>): InstanceType<T>;
114
+ }
115
+
87
116
  /**
88
117
  * Base class for disposable objects that can own other objects. See the module documentation.
89
118
  */
@@ -95,27 +124,9 @@ export abstract class Disposable implements IDisposable, IDisposableOwner {
95
124
  * exception, dispose() gets called to clean up the partially-constructed object.
96
125
  *
97
126
  * Owner may be null if intend to ensure disposal some other way.
98
- *
99
- * TODO: create() needs more unittests, including to ensure that TypeScript types are done
100
- * correctly.
101
127
  */
102
- // The complex-looking overloads are to ensure that it can do type-checking for constuctors of
103
- // different arity. E.g. if Foo's constructor takes (number, string), we want Foo.create to
104
- // require (owner, number, string) as arguments.
105
- public static create<T extends IDisposable>(
106
- this: new () => T, owner: IDisposableOwnerT<T>|null): T;
107
- public static create<T extends IDisposable, A>(
108
- this: new (a: A) => T, owner: IDisposableOwnerT<T>|null, a: A): T;
109
- public static create<T extends IDisposable, A, B>(
110
- this: new (a: A, b: B) => T, owner: IDisposableOwnerT<T>|null, a: A, b: B): T;
111
- public static create<T extends IDisposable, A, B, C>(
112
- this: new (a: A, b: B, c: C) => T, owner: IDisposableOwnerT<T>|null, a: A, b: B, c: C): T;
113
- public static create<T extends IDisposable, A, B, C, D>(
114
- this: new (a: A, b: B, c: C, d: D) => T, owner: IDisposableOwnerT<T>|null, a: A, b: B, c: C, d: D): T;
115
- public static create<T extends IDisposable, A, B, C, D, E>(
116
- this: new (a: A, b: B, c: C, d: D, e: E) => T, owner: IDisposableOwnerT<T>|null, a: A, b: B, c: C, d: D, e: E): T;
117
- public static create<T extends IDisposable>(
118
- this: new (...args: any[]) => T, owner: IDisposableOwnerT<T>|null, ...args: any[]): T {
128
+ public static create<T extends new (...args: any[]) => any>(
129
+ this: T, owner: IDisposableOwnerT<InstanceType<T>>|null, ...args: ConstructorParameters<T>): InstanceType<T> {
119
130
 
120
131
  const origDefaultOwner = _defaultDisposableOwner;
121
132
  const holder = new Holder();
@@ -145,6 +156,9 @@ export abstract class Disposable implements IDisposable, IDisposableOwner {
145
156
  constructor() {
146
157
  // This registers with a temp Holder when using create(), and is a no-op when using `new Foo`.
147
158
  _defaultDisposableOwner.autoDispose(this);
159
+ // Be sure to reset to no-op, so that a (non-recommended) direct call like 'new Bar()', from
160
+ // inside Foo's constructor doesn't use the same Holder that's temporarily holding Foo.
161
+ _defaultDisposableOwner = _noopOwner;
148
162
  }
149
163
 
150
164
  /** Take ownership of obj, and dispose it when this.dispose() is called. */
@@ -154,8 +168,8 @@ export abstract class Disposable implements IDisposable, IDisposableOwner {
154
168
  }
155
169
 
156
170
  /** Call the given callback when this.dispose() is called. */
157
- public onDispose<T>(callback: (this: T) => void, context?: T): void {
158
- this._disposalList.addListener(callback, context);
171
+ public onDispose<T>(callback: (this: T) => void, context?: T): DisposeListener {
172
+ return this._disposalList.addListener(callback, context);
159
173
  }
160
174
 
161
175
  /**
@@ -190,8 +204,13 @@ export abstract class Disposable implements IDisposable, IDisposableOwner {
190
204
  */
191
205
  public dispose(): void {
192
206
  const disposalList = this._disposalList;
193
- this._disposalList = null!;
194
- disposalList.callAndDispose(this);
207
+ if (!disposalList) {
208
+ // tslint:disable-next-line:no-console
209
+ console.error("Error disposing %s which is already disposed", _describe(this));
210
+ } else {
211
+ this._disposalList = null!;
212
+ disposalList.callAndDispose(this);
213
+ }
195
214
  }
196
215
 
197
216
  /**
@@ -215,28 +234,28 @@ export abstract class Disposable implements IDisposable, IDisposableOwner {
215
234
  *
216
235
  * If the object is an instance of Disposable, the holder will also notice when the object gets
217
236
  * disposed from outside of it, in which case the holder will become empty again.
218
- *
219
- * TODO Holder needs unittests.
220
237
  */
221
238
  export class Holder<T extends IDisposable> implements IDisposable, IDisposableOwner {
222
- public static create<T extends IDisposable>(owner: IDisposableOwner|null): Holder<T> {
239
+ public static create<T extends IDisposable>(owner: IDisposableOwnerT<Holder<T>>|null): Holder<T> {
223
240
  return setDisposeOwner(owner, new Holder<T>());
224
241
  }
225
242
 
226
243
  protected _owned: T|null = null;
244
+ private _disposalListener: DisposeListener|undefined = undefined;
227
245
 
228
246
  /** Take ownership of a new object, disposing the previously held one. */
229
247
  public autoDispose(obj: T): T {
230
- if (this._owned) { this._owned.dispose(); }
248
+ this.clear();
231
249
  this._owned = obj;
232
250
  if (obj instanceof Disposable) {
233
- obj.onDispose(this.release, this);
251
+ this._disposalListener = obj.onDispose(this._onOutsideDispose, this);
234
252
  }
235
253
  return obj;
236
254
  }
237
255
 
238
256
  /** Releases the held object without disposing it, emptying the holder. */
239
257
  public release(): IDisposable|null {
258
+ this._unlisten();
240
259
  const ret = this._owned;
241
260
  this._owned = null;
242
261
  return ret;
@@ -244,9 +263,11 @@ export class Holder<T extends IDisposable> implements IDisposable, IDisposableOw
244
263
 
245
264
  /** Disposes the held object and empties the holder. */
246
265
  public clear(): void {
247
- if (this._owned) {
248
- this._owned.dispose();
266
+ this._unlisten();
267
+ const owned = this._owned;
268
+ if (owned) {
249
269
  this._owned = null;
270
+ owned.dispose();
250
271
  }
251
272
  }
252
273
 
@@ -258,8 +279,29 @@ export class Holder<T extends IDisposable> implements IDisposable, IDisposableOw
258
279
 
259
280
  /** When the holder is disposed, it disposes the held object if any. */
260
281
  public dispose(): void { this.clear(); }
282
+
283
+ /** Stop listening for the disposal of this._owned. */
284
+ private _unlisten() {
285
+ const disposalListener = this._disposalListener;
286
+ if (disposalListener) {
287
+ this._disposalListener = undefined;
288
+ disposalListener.dispose();
289
+ }
290
+ }
291
+
292
+ private _onOutsideDispose() {
293
+ this._disposalListener = undefined;
294
+ this._owned = null;
295
+ }
261
296
  }
262
297
 
298
+ /**
299
+ * MultiHolder keeps multiple disposable object. It disposes all held object when the holder
300
+ * itself is disposed. It's actually nothing more than the Disposable base class itself, just
301
+ * exposed with a clearer name that describes its purpose.
302
+ */
303
+ export class MultiHolder extends Disposable {}
304
+
263
305
  /**
264
306
  * Sets owner of obj (i.e. calls owner.autoDispose(obj)) unless owner is null. Returns obj.
265
307
  */
@@ -282,9 +324,10 @@ function _describe(obj: any) {
282
324
  class DisposalList extends LLink {
283
325
  constructor() { super(); }
284
326
 
285
- public addListener<T>(callback: (this: T) => void, optContext?: T): void {
327
+ public addListener<T>(callback: (this: T) => void, optContext?: T): DisposeListener {
286
328
  const lis = new DisposeListener(callback, optContext);
287
329
  this._insertBefore(this._next!, lis);
330
+ return lis;
288
331
  }
289
332
 
290
333
  /**
@@ -304,7 +347,7 @@ class DisposalList extends LLink {
304
347
  * Internal class that keeps track of one item of the DisposalList. It mimicks emit.Listener, but
305
348
  * reports and swallows erros when it calls the callbacks in the list.
306
349
  */
307
- class DisposeListener extends LLink {
350
+ class DisposeListener extends LLink implements IDisposable {
308
351
  public static callAll(begin: LLink, end: LLink, owner: Disposable): void {
309
352
  while (begin !== end) {
310
353
  const lis = begin as DisposeListener;
@@ -319,4 +362,9 @@ class DisposeListener extends LLink {
319
362
  }
320
363
 
321
364
  constructor(private callback: () => void, private context?: any) { super(); }
365
+
366
+ public dispose(): void {
367
+ if (this.isDisposed()) { return; }
368
+ this._removeNode(this);
369
+ }
322
370
  }
package/lib/dom.ts CHANGED
@@ -18,24 +18,29 @@
18
18
  */
19
19
 
20
20
  // We keep various dom-related functions organized in private modules, but they are exposed here.
21
- export {DomMethod, DomElementMethod, DomArg, DomElementArg, svg, update, frag, find, findAll} from './_domImpl';
22
- export * from './_domComponent';
23
- export * from './_domDispose';
24
- export * from './_domForEach';
25
- export * from './_domMethods';
21
+ export * from './domImpl';
22
+ export * from './domComponent';
23
+ export * from './domComputed';
24
+ export * from './domDispose';
25
+ export * from './domForEach';
26
+ export * from './domMethods';
26
27
  export * from './domevent';
27
28
 
28
- import * as _domComponent from './_domComponent';
29
- import * as _domDispose from './_domDispose';
30
- import * as _domForEach from './_domForEach';
31
- import * as _domImpl from './_domImpl';
32
- import * as _domMethods from './_domMethods';
29
+ import * as _domComponent from './domComponent';
30
+ import * as _domComputed from './domComputed';
31
+ import * as _domDispose from './domDispose';
32
+ import * as _domForEach from './domForEach';
33
+ import * as _domImpl from './domImpl';
34
+ import * as _domMethods from './domMethods';
35
+
33
36
  import * as domevent from './domevent';
34
37
 
38
+ import {dom as _dom, IDomArgs, TagElem, TagName} from './domImpl';
39
+
35
40
  // We just want to re-export _domImpl.dom, but to allow adding methods to it in a typesafe way,
36
41
  // TypeScript wants us to declare a real function in the same file.
37
- export function dom(tagString: string, ...args: _domImpl.DomElementArg[]): HTMLElement {
38
- return _domImpl.dom(tagString, ...args);
42
+ export function dom<Tag extends TagName>(tagString: Tag, ...args: IDomArgs<TagElem<Tag>>): TagElem<Tag> {
43
+ return _dom(tagString, ...args);
39
44
  }
40
45
 
41
46
  // Additionally export all methods as properties of dom() function.
@@ -74,20 +79,21 @@ export namespace dom { // tslint:disable-line:no-namespace
74
79
  export const dataElem = _domMethods.dataElem;
75
80
  export const data = _domMethods.data;
76
81
  export const getData = _domMethods.getData;
77
- export const replaceContent = _domMethods.replaceContent;
78
- export const domComputed = _domMethods.domComputed;
79
- export const maybe = _domMethods.maybe;
82
+ export const replaceContent = _domComputed.replaceContent;
83
+ export const domComputed = _domComputed.domComputed;
84
+ export const domComputedOwned = _domComputed.domComputedOwned;
85
+ export const maybe = _domComputed.maybe;
86
+ export const maybeOwned = _domComputed.maybeOwned;
80
87
 
81
88
  export const forEach = _domForEach.forEach;
82
89
 
83
- export const Component = _domComponent.Component;
84
90
  export const create = _domComponent.create;
85
- export const createInit = _domComponent.createInit;
86
91
 
87
92
  export const onElem = domevent.onElem;
88
93
  export const on = domevent.on;
89
94
  export const onMatchElem = domevent.onMatchElem;
90
95
  export const onMatch = domevent.onMatch;
91
- export const onKeyPressElem = domevent.onKeyPressElem;
96
+ export const onKeyElem = domevent.onKeyElem;
92
97
  export const onKeyPress = domevent.onKeyPress;
98
+ export const onKeyDown = domevent.onKeyDown;
93
99
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * UI components that can be inserted into dom().
3
+ *
4
+ * Components are created and inserted using dom.create():
5
+ *
6
+ * dom('div',
7
+ * dom.create(MyWidget, ...myArgs), // Calls MyWidget.create(owner, ...myArgs)
8
+ * dom.create(createMyWidget, ...myArgs), // Calls createMyWidget(owner, ...myArgs)
9
+ * )
10
+ *
11
+ * The first argument may be a function, which is called directly, or a class with a .create()
12
+ * static method, in which case that's what gets called.
13
+ *
14
+ * In both cases, the call gets a first argument of `owner` followed by the rest of the arguments
15
+ * to dom.create(). The `owner` is a MultiHolder that will own this component. This works
16
+ * naturally with any class that derives from Disposable, since it then has a suitable static
17
+ * create() method.
18
+ *
19
+ * Function-based components may use owner to easily handle disposal. For example:
20
+ *
21
+ * dom.create(createMyWidget)
22
+ * function createMyWidget(owner) {
23
+ * const foo = Foo.create(owner);
24
+ * return dom('div', foo.getTitle());
25
+ * }
26
+ *
27
+ * The `owner` argument is the main benefit of dom.create(). Logically, the owner is the DOM where
28
+ * the component is attached. When the parent DOM element is disposed, so is the component.
29
+ *
30
+ * [Explanation] To understand why the syntax is such, consider a potential alternative such as:
31
+ *
32
+ * dom('div', _insert_(new Comp1()), _insert_(new Comp2(...args)))
33
+ *
34
+ * In both cases, the constructor for Comp1 runs before the constructor for Comp2. What happens
35
+ * when Comp2's constructor throws an exception? In the second case, nothing yet owns the
36
+ * created Comp1 component, and it will never get cleaned up. With dom.create(), the DOM
37
+ * gets ownership of Comp1 early enough and will dispose it.
38
+ *
39
+ * A function component may return DOM directly. A class component returns the class instance,
40
+ * which must have a .buildDom() method which will be called right after the constructor to get
41
+ * the DOM. Note that buildDom is only called once.
42
+ *
43
+ * A function component may also return an object with .buildDom(). So these are equivalent:
44
+ *
45
+ * dom.create(MyWidget)
46
+ * dom.create((owner) => MyWidget.create(owner))
47
+ *
48
+ * Note that ownership should be handled using the `owner` argument. Don't do this:
49
+ *
50
+ * // NON-EXAMPLE: Nothing will dispose the created object:
51
+ * // dom.create(() => new MyWidget());
52
+ *
53
+ * The returned DOM may includes Nodes, strings, and domComputed() values, as well as arrays of
54
+ * any of these. In other words, any DomArg goes except DomMethods. All the DOM returned will be
55
+ * disposed when the containing element is disposed, followed by the `owner` itself.
56
+ */
57
+ import {MultiHolder} from './dispose';
58
+ import {domComputedOwned, DomContents} from './domComputed';
59
+
60
+ export interface IDomComponent {
61
+ buildDom(): DomContents;
62
+ }
63
+
64
+ export type DomComponentReturn = DomContents | IDomComponent;
65
+
66
+ export type IDomCreateFunc<Args extends any[]> = (owner: MultiHolder, ...args: Args) => DomComponentReturn;
67
+
68
+ // It's not that we must have a constructor matching create(), but specifying type of new allows
69
+ // type checking of classes that derive from Disposable, whereas matching only create() does not
70
+ // (presumably because of the too much magic that Disposable does for the type of create()).
71
+ export interface IDomCreateClass<Args extends any[]> {
72
+ create: IDomCreateFunc<Args>;
73
+ new (...args: Args): DomComponentReturn;
74
+ }
75
+ export type IDomCreator<Args extends any[]> = IDomCreateFunc<Args> | IDomCreateClass<Args>;
76
+
77
+ type DomCreatorArgs<T> =
78
+ T extends (owner: MultiHolder, ...args: infer P) => any ? P :
79
+ (T extends new (...args: infer P) => any ? P : never);
80
+
81
+ export function create<Fn extends IDomCreator<any[]>>(fn: Fn, ...args: DomCreatorArgs<Fn>): DomContents {
82
+ return domComputedOwned(null, (owner) => {
83
+ const value: DomComponentReturn = ('create' in fn) ?
84
+ (fn as IDomCreateClass<any[]>).create(owner, ...args) :
85
+ (fn as IDomCreateFunc<any[]>)(owner, ...args);
86
+ return (value && typeof value === 'object' && 'buildDom' in value) ?
87
+ value.buildDom() : value;
88
+ });
89
+ }