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/lib/domevent.ts CHANGED
@@ -39,10 +39,14 @@
39
39
  * let lis = domevent.onElem(elem, 'mouseup', e => { lis.dispose(); other_work(); });
40
40
  */
41
41
 
42
- import {DomElementMethod} from './_domImpl';
43
42
  import {IDisposable} from './dispose';
43
+ import {DomElementMethod, DomMethod} from './domImpl';
44
44
 
45
- export type EventCB = (this: void, event: Event, elem: Element) => void;
45
+ export type EventName = keyof HTMLElementEventMap;
46
+ export type EventType<E extends EventName|string> = E extends EventName ? HTMLElementEventMap[E] : Event;
47
+
48
+ export type EventCB<E extends Event = Event, T extends EventTarget = EventTarget> =
49
+ (this: void, event: E, elem: T) => void;
46
50
 
47
51
  function _findMatch(inner: Element, outer: Element, selector: string): Element|null {
48
52
  for (let el: Element|null = inner; el && el !== outer; el = el.parentElement) {
@@ -53,18 +57,18 @@ function _findMatch(inner: Element, outer: Element, selector: string): Element|n
53
57
  return null;
54
58
  }
55
59
 
56
- class DomEventListener implements EventListenerObject, IDisposable {
57
- constructor(protected elem: EventTarget,
60
+ class DomEventListener<E extends Event, T extends EventTarget> implements EventListenerObject, IDisposable {
61
+ constructor(protected elem: T,
58
62
  protected eventType: string,
59
- protected callback: EventCB,
63
+ protected callback: EventCB<E, T>,
60
64
  protected useCapture: boolean,
61
65
  protected selector?: string) {
62
66
  this.elem.addEventListener(this.eventType, this, this.useCapture);
63
67
  }
64
68
 
65
- public handleEvent(event: Event) {
69
+ public handleEvent(event: E) {
66
70
  const cb = this.callback;
67
- cb(event, this.elem as Element);
71
+ cb(event, this.elem);
68
72
  }
69
73
 
70
74
  public dispose() {
@@ -72,8 +76,8 @@ class DomEventListener implements EventListenerObject, IDisposable {
72
76
  }
73
77
  }
74
78
 
75
- class DomEventMatchListener extends DomEventListener {
76
- public handleEvent(event: Event) {
79
+ class DomEventMatchListener<E extends Event> extends DomEventListener<E, EventTarget> {
80
+ public handleEvent(event: E) {
77
81
  const elem = _findMatch(event.target as Element, this.elem as Element, this.selector!);
78
82
  if (elem) {
79
83
  const cb = this.callback;
@@ -92,11 +96,13 @@ class DomEventMatchListener extends DomEventListener {
92
96
  * rarely be useful (e.g. JQuery doesn't even offer it as an option).
93
97
  * @returns {Object} Listener object whose .dispose() method will remove the event listener.
94
98
  */
95
- export function onElem(elem: EventTarget, eventType: string, callback: EventCB,
96
- {useCapture = false} = {}): IDisposable {
99
+ export function onElem<E extends EventName|string, T extends EventTarget>(
100
+ elem: T, eventType: E, callback: EventCB<EventType<E>, T>, {useCapture = false} = {}): IDisposable {
97
101
  return new DomEventListener(elem, eventType, callback, useCapture);
98
102
  }
99
- export function on(eventType: string, callback: EventCB, {useCapture = false} = {}): DomElementMethod {
103
+
104
+ export function on<E extends EventName|string, T extends EventTarget>(
105
+ eventType: E, callback: EventCB<EventType<E>, T>, {useCapture = false} = {}): DomMethod<T> {
100
106
  // tslint:disable-next-line:no-unused-expression
101
107
  return (elem) => { new DomEventListener(elem, eventType, callback, useCapture); };
102
108
  }
@@ -126,27 +132,58 @@ export function onMatch(selector: string, eventType: string, callback: EventCB,
126
132
  return (elem) => { new DomEventMatchListener(elem, eventType, callback, useCapture, selector); };
127
133
  }
128
134
 
135
+ export type KeyEventType = 'keypress' | 'keyup' | 'keydown';
136
+
137
+ export interface IKeyHandlers<T extends HTMLElement = HTMLElement> {
138
+ [key: string]: (this: void, ev: KeyboardEvent, elem: T) => void;
139
+ }
140
+
129
141
  /**
130
- * Listen to key presses, with specified per-key callbacks. The `onKeyPress()` variant takes no
131
- * `elem` argument, and may be used as an argument to dom().
132
- *
142
+ * Listen to key events (typically 'keydown' or 'keypress'), with specified per-key callbacks.
133
143
  * Key names are listed at https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
134
144
  *
145
+ * Methods onKeyPress() and onKeyDown() are intended to be used as arguments to dom().
146
+ *
147
+ * By default, handled events are stopped from bubbling with stopPropagation() and
148
+ * preventDefault(). If, however, you register a key with a "$" suffix (i.e. "Enter$" instead of
149
+ * "Enter"), then the event is allowed to bubble normally.
150
+ *
151
+ * When this handler is set on an element, we automatically ensure that tabindex attribute is set,
152
+ * to allow this element to receive keyboard events.
153
+ *
135
154
  * For example:
136
155
  *
137
156
  * dom('input', ...
138
- * dom.onKeyPress({
157
+ * dom.onKeyDown({
139
158
  * Enter: (e, elem) => console.log("Enter pressed"),
140
159
  * Escape: (e, elem) => console.log("Escape pressed"),
160
+ * Delete$: (e, elem) => console.log("Delete pressed, will bubble"),
141
161
  * })
142
162
  * )
143
163
  */
144
- export function onKeyPressElem(elem: EventTarget, callbacks: {[key: string]: EventCB}): IDisposable {
145
- return onElem(elem, 'keypress', (e, _elem) => {
146
- const cb = callbacks[(e as KeyboardEvent).key];
147
- if (cb) { cb(e, _elem); }
164
+ export function onKeyElem<T extends HTMLElement>(
165
+ elem: T, evType: KeyEventType, keyHandlers: IKeyHandlers<T>,
166
+ ): IDisposable {
167
+ if (!(elem.tabIndex >= 0)) { // If tabIndex property is undefined or -1,
168
+ elem.setAttribute('tabindex', '-1'); // Set tabIndex attribute to make the element focusable.
169
+ }
170
+ return onElem(elem, evType, (ev, _elem) => {
171
+ const plainHandler = keyHandlers[ev.key];
172
+ const handler = plainHandler || keyHandlers[ev.key + '$'];
173
+ if (handler) {
174
+ if (plainHandler!) {
175
+ ev.stopPropagation();
176
+ ev.preventDefault();
177
+ }
178
+ handler(ev, _elem);
179
+ }
148
180
  });
149
181
  }
150
- export function onKeyPress(callbacks: {[key: string]: EventCB}): DomElementMethod {
151
- return (elem) => { onKeyPressElem(elem, callbacks); };
182
+
183
+ export function onKeyPress<T extends HTMLElement>(keyHandlers: IKeyHandlers<T>): DomMethod<T> {
184
+ return (elem) => { onKeyElem(elem, 'keypress', keyHandlers); };
185
+ }
186
+
187
+ export function onKeyDown<T extends HTMLElement>(keyHandlers: IKeyHandlers<T>): DomMethod<T> {
188
+ return (elem) => { onKeyElem(elem, 'keydown', keyHandlers); };
152
189
  }
package/lib/emit.ts CHANGED
@@ -116,7 +116,8 @@ export class Emitter extends LLink {
116
116
  * Sets the single callback that would get called when a listener is added or removed.
117
117
  * @param {Function} changeCB(hasListeners): Function to call after a listener is added or
118
118
  * removed. It's called with a boolean indicating whether this Emitter has any listeners.
119
- * Pass in `null` to unset the callback.
119
+ * Pass in `null` to unset the callback. Note that it can be called multiple times in a row
120
+ * with hasListeners `true`.
120
121
  */
121
122
  public setChangeCB(changeCB: ChangeCB, optContext?: any): void {
122
123
  this._changeCB = changeCB || _noop;
package/lib/kowrap.ts CHANGED
@@ -19,10 +19,12 @@
19
19
  * knockout as a dependency of grainjs.
20
20
  *
21
21
  * In both cases, calling fromKo/toKo twice on the same observable will return the same wrapper,
22
- * and subscriptions and disposal are appropriately set up to make usage seamless.
22
+ * and subscriptions and disposal are appropriately set up to make usage seamless. In particular,
23
+ * the returned wrapper should not be disposed; it's tied to the lifetime of the wrapped object.
23
24
  */
24
25
 
25
- import {observable, Observable} from './observable';
26
+ import {domDisposeHooks} from './domDispose';
27
+ import {bundleChanges, Observable} from './observable';
26
28
 
27
29
  // Implementation note. Both wrappers are implemented in the same way.
28
30
  //
@@ -31,31 +33,69 @@ import {observable, Observable} from './observable';
31
33
  // be garbage-collected once it has no listeners AND the underlying observable is disposed or
32
34
  // unreferenced.
33
35
 
34
- export interface IKnockoutObservable<T> {
36
+ export interface IKnockoutObservable<T> extends IKnockoutReadObservable<T> {
35
37
  (val: T): void;
38
+ }
39
+
40
+ export interface IKnockoutReadObservable<T> {
41
+ (): T;
36
42
  peek(): T;
37
43
  subscribe(callback: (newValue: T) => void, target?: any, event?: "change"): any;
44
+ getSubscriptionsCount(): number;
38
45
  }
39
46
 
47
+ // Inference from Knockout observable gets very tricky because ko.Observable includes the function
48
+ // signature `(val: T) => any` from which type `any` gets inferred. We can infer the correct type
49
+ // with this helper.
50
+ export type InferKoType<KObs extends IKnockoutReadObservable<any>> =
51
+ KObs extends {peek(): infer T} ? T : never;
52
+
40
53
  const fromKoWrappers: WeakMap<IKnockoutObservable<any>, Observable<any>> = new WeakMap();
41
54
  const toKoWrappers: WeakMap<Observable<any>, IKnockoutObservable<any>> = new WeakMap();
42
55
 
43
56
  /**
44
57
  * Returns a Grain.js observable which mirrors a Knockout observable.
58
+ *
59
+ * Do not dispose this wrapper, as it is shared by all code using koObs, and its lifetime is tied
60
+ * to the lifetime of koObs. If unused, it consumes minimal resources, and should get garbage
61
+ * collected along with koObs.
62
+ */
63
+ export function fromKo<KObs extends IKnockoutObservable<any>>(koObs: KObs): Observable<InferKoType<KObs>> {
64
+ return fromKoWrappers.get(koObs) || fromKoWrappers.set(koObs, new KoWrapObs(koObs)).get(koObs)!;
65
+ }
66
+
67
+ /**
68
+ * An Observable that wraps a Knockout observable, created via fromKo(). It keeps minimal overhead
69
+ * when unused by only subscribing to the wrapped observable while it itself has subscriptions.
70
+ *
71
+ * This way, when unused, the only reference is from the wrapper to the wrapped object. KoWrapObs
72
+ * should not be disposed; its lifetime is tied to that of the wrapped object.
45
73
  */
46
- export function fromKo<T>(koObservable: IKnockoutObservable<T>): Observable<T> {
47
- const prevObs = fromKoWrappers.get(koObservable);
48
- if (prevObs) {
49
- return prevObs;
74
+ export class KoWrapObs<T> extends Observable<T> {
75
+ private _koSub: any = null;
76
+
77
+ constructor(private _koObs: IKnockoutObservable<T>) {
78
+ super(_koObs.peek());
79
+ this.setListenerChangeCB((hasListeners) => {
80
+ if (!hasListeners) {
81
+ this._koSub.dispose();
82
+ this._koSub = null;
83
+ } else if (!this._koSub) {
84
+ // TODO this is a little hack, really, BaseObservable should expose a way to set the value
85
+ // directly by derived classes, i.e. a protected setter.
86
+ (this as any)._value = this._koObs.peek();
87
+ this._koSub = this._koObs.subscribe((val) => this.setAndTrigger(val));
88
+ }
89
+ });
50
90
  }
51
- const newObs = observable(koObservable.peek());
52
- fromKoWrappers.set(koObservable, newObs);
53
- koObservable.subscribe((val) => newObs.set(val));
54
- return newObs;
91
+ public get(): T { return this._koObs.peek(); }
92
+ public set(value: T): void { bundleChanges(() => this._koObs(value)); }
93
+ public dispose(): void { throw new Error("KoWrapObs should not be disposed"); }
55
94
  }
56
95
 
57
96
  export interface IKnockoutModule {
58
97
  observable<T>(value: T): IKnockoutObservable<T>;
98
+ cleanNode(node: Node): void;
59
99
  }
60
100
 
61
101
  /**
@@ -71,3 +111,61 @@ export function toKo<T>(knockout: IKnockoutModule, grainObs: Observable<T>): IKn
71
111
  grainObs.addListener((val) => newKoObs(val));
72
112
  return newKoObs;
73
113
  }
114
+
115
+ // Marker for when knockout-disposal integration has already been setup.
116
+ let koDisposalIsSetup = false;
117
+
118
+ /**
119
+ * Set up integration between grainjs and knockout disposal. Knockout does cleanup using
120
+ * ko.removeNode / ko.cleanNode (it also takes care of JQuery cleanup if needed). GrainJS does
121
+ * cleanup using dom.domDispose(). By default these don't know about each other.
122
+ *
123
+ * If you mix the two libraries, however, disposing an element may need to trigger disposers
124
+ * registered by either library.
125
+ *
126
+ * This method ensures that this happens.
127
+ *
128
+ * Note: grainjs disposes text nodes too, but nothing relies on it. When disposal is triggered via
129
+ * knockout, we are forced to rely on knockout's node traversal which ignores text nodes.
130
+ */
131
+ export function setupKoDisposal(ko: IKnockoutModule) {
132
+ // Ensure we don't do the setup more than once, or things will get called multiple times.
133
+ if (koDisposalIsSetup) { return; }
134
+ koDisposalIsSetup = true;
135
+
136
+ const koDomNodeDisposal = (ko as any).utils.domNodeDisposal;
137
+
138
+ // Knockout by default has an external-data-cleanup func set to cleanup JQuery. Whatever it is
139
+ // set to, we will continue calling it, and also will call grainjs domDisposeNode.
140
+ const origKoCleanExternalData = koDomNodeDisposal.cleanExternalData;
141
+
142
+ // The original function called by grainjs to clean nodes recursively. We'll override it.
143
+ const origGrainDisposeRecursive = domDisposeHooks.disposeRecursive;
144
+
145
+ // New function called by knockout to do extra cleanup. Now calls grainjs single-node cleanup.
146
+ // (In knockout, we can only override single-node cleanup.)
147
+ function newKoCleanExternalData(node: Node) {
148
+ origKoCleanExternalData(node);
149
+ domDisposeHooks.disposeNode(node);
150
+ }
151
+
152
+ // Function called by grainjs to clean nodes recursively. We override the recursive cleanup
153
+ // function to call the recursive knockout cleanup (letting knockout do the dom traversal it
154
+ // normally does).
155
+ function newGrainDisposeRecursive(node: Node) {
156
+ origGrainDisposeRecursive(node);
157
+
158
+ // While doing knockout cleanup, do NOT have it call grainjs cleanup too, as that would cause
159
+ // multiple unnecessary traversals of DOM.
160
+ koDomNodeDisposal.cleanExternalData = origKoCleanExternalData;
161
+ try {
162
+ ko.cleanNode(node);
163
+ } finally {
164
+ koDomNodeDisposal.cleanExternalData = newKoCleanExternalData;
165
+ }
166
+ }
167
+
168
+ // Use knockout and grainjs hooks to actually set the new cleanup functions.
169
+ koDomNodeDisposal.cleanExternalData = newKoCleanExternalData;
170
+ domDisposeHooks.disposeRecursive = newGrainDisposeRecursive;
171
+ }
package/lib/obsArray.ts CHANGED
@@ -31,7 +31,7 @@
31
31
  * ownership of those disposables that are added to it as array elements.
32
32
  */
33
33
 
34
- import {IDisposable, IDisposableOwner, setDisposeOwner} from './dispose';
34
+ import {IDisposable, IDisposableOwnerT, setDisposeOwner} from './dispose';
35
35
  import {Listener} from './emit';
36
36
  import {BaseObservable, Observable} from './observable';
37
37
  import {subscribe, Subscription} from './subscribe';
@@ -291,7 +291,7 @@ export function computedArray<T, U>(
291
291
  * The returned observable has an additional .setLive(bool) method. While set to false, the
292
292
  * observable will not be adjusted as the array changes, except to keep it valid.
293
293
  */
294
- export function makeLiveIndex<T>(owner: IDisposableOwner|null, obsArr: ObsArray<T>,
294
+ export function makeLiveIndex<T>(owner: IDisposableOwnerT<LiveIndex>|null, obsArr: ObsArray<T>,
295
295
  initialIndex: number = 0): LiveIndex {
296
296
  return setDisposeOwner(owner, new LiveIndex(obsArr, initialIndex));
297
297
  }
package/lib/observable.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import {compute, DepItem} from './_computed_queue';
25
- import {IDisposable, IDisposableOwnerT} from './dispose';
25
+ import {IDisposable, IDisposableOwnerT, setDisposeOwner} from './dispose';
26
26
  import {Emitter, Listener} from './emit';
27
27
 
28
28
  export {bundleChanges} from './_computed_queue';
@@ -90,7 +90,8 @@ export class BaseObservable<T> {
90
90
  * previously-set such callback.
91
91
  * @param {Function} changeCB(hasListeners): Function to call after a listener is added or
92
92
  * removed. It's called with a boolean indicating whether this observable has any listeners.
93
- * Pass in `null` to unset the callback.
93
+ * Pass in `null` to unset the callback. Note that it can be called multiple times in a row
94
+ * with hasListeners `true`.
94
95
  */
95
96
  public setListenerChangeCB(changeCB: (hasListeners: boolean) => void, optContext?: any): void {
96
97
  this._onChange.setChangeCB(changeCB, optContext);
@@ -143,6 +144,13 @@ export class Observable<T> extends BaseObservable<T> implements IDisposableOwner
143
144
  return obs;
144
145
  }
145
146
 
147
+ /**
148
+ * Creates a new Observable with the given initial value, and owned by owner.
149
+ */
150
+ public static create<T>(owner: IDisposableOwnerT<Observable<T>>|null, value: T): Observable<T> {
151
+ return setDisposeOwner(owner, new Observable<T>(value));
152
+ }
153
+
146
154
  private _owned?: T & IDisposable = undefined;
147
155
 
148
156
  /**
@@ -11,15 +11,16 @@
11
11
  */
12
12
 
13
13
  import {DepItem} from './_computed_queue';
14
- import {Observable} from './observable';
15
- import {ISubscribable, Subscription, UseCB} from './subscribe';
14
+ import {IKnockoutReadObservable} from './kowrap';
15
+ import {BaseObservable, Observable} from './observable';
16
+ import {ISubscribable, ISubscribableObs, Subscription, UseCB} from './subscribe';
16
17
 
17
18
  function _noWrite(): never {
18
19
  throw new Error("Can't write to non-writable pureComputed");
19
20
  }
20
21
 
21
- function _useFunc<T>(obs: Observable<T>): T {
22
- return obs.get();
22
+ function _useFunc<T>(obs: BaseObservable<T>|IKnockoutReadObservable<T>): T {
23
+ return ('get' in obs) ? obs.get() : obs.peek();
23
24
  }
24
25
 
25
26
  // Constant empty array, which we use to avoid allocating new read-only empty arrays.
@@ -29,7 +30,7 @@ export class PureComputed<T> extends Observable<T> {
29
30
  private _callback: (use: UseCB, ...args: any[]) => T;
30
31
  private _write: (value: T) => void;
31
32
  private _sub: Subscription|null;
32
- private readonly _dependencies: ReadonlyArray<ISubscribable>;
33
+ private readonly _dependencies: ReadonlyArray<ISubscribableObs>;
33
34
  private _inCall: boolean;
34
35
 
35
36
  /**
@@ -57,7 +58,7 @@ export class PureComputed<T> extends Observable<T> {
57
58
  // _inCall member prevents infinite recursion.
58
59
  this._inCall = true;
59
60
  try {
60
- const readArgs: any[] = [_useFunc];
61
+ const readArgs: [UseCB, ...any[]] = [_useFunc];
61
62
  // Note that this attempts to optimize for speed.
62
63
  for (let i = 0, len = this._dependencies.length; i < len; i++) {
63
64
  readArgs[i + 1] = this._dependencies[i].get();
package/lib/styled.ts CHANGED
@@ -54,32 +54,38 @@
54
54
  * myButton(myButton.cls('-small'), 'Test')
55
55
  *
56
56
  * creates a button with both the myButton style above, and the style specified under "&-small".
57
+ *
58
+ * Animations with @keyframes may be created with a unique name by using the keyframes() helper:
59
+ *
60
+ * const rotate360 = keyframes(`
61
+ * from { transform: rotate(0deg); }
62
+ * to { transform: rotate(360deg); }
63
+ * `);
64
+ *
65
+ * const Rotate = styled('div', `
66
+ * display: inline-block;
67
+ * animation: ${rotate360} 2s linear infinite;
68
+ * `);
57
69
  */
58
70
 
59
71
  // Use the browser globals in a way that allows replacing them with mocks in tests.
60
72
  import {G} from './browserGlobals';
61
- import {dom, DomElementArg, DomElementMethod} from './dom';
62
-
63
- export type DomCreateFunc0<R> = (...args: DomElementArg[]) => R;
64
- export type DomCreateFunc1<R, T> = (a: T, ...args: DomElementArg[]) => R;
65
- export type DomCreateFunc2<R, T, U> = (a: T, b: U, ...args: DomElementArg[]) => R;
66
- export type DomCreateFunc3<R, T, U, W> = (a: T, b: U, c: W, ...args: DomElementArg[]) => R;
73
+ import {dom, IDomArgs, TagElem, TagName} from './domImpl';
74
+ import {cls, clsPrefix} from './domMethods';
67
75
 
68
76
  // The value returned by styled() matches the input (first argument), and also implements IClsName
69
77
  // interface.
70
78
  export interface IClsName {
71
79
  className: string; // Name of the generated class.
72
- cls: typeof dom.cls; // Helper like dom.cls(), but which prefixes classes by className.
80
+ cls: typeof cls; // Helper like dom.cls(), but which prefixes classes by className.
73
81
  }
74
82
 
83
+ export type DomCreateFunc<R, Args extends IDomArgs<R> = IDomArgs<R>> = (...args: Args) => R;
84
+
75
85
  // See module documentation for details.
76
- export function styled<R>(tag: string, styles: string): DomCreateFunc0<Element> & IClsName;
77
- export function styled<R>(creator: DomCreateFunc0<R>, styles: string): DomCreateFunc0<R> & IClsName;
78
- export function styled<R, T>(creator: DomCreateFunc1<R, T>, styles: string): DomCreateFunc1<R, T> & IClsName;
79
- export function styled<R, T, U>(
80
- creator: DomCreateFunc2<R, T, U>, styles: string): DomCreateFunc2<R, T, U> & IClsName;
81
- export function styled<R, T, U, W>(
82
- creator: DomCreateFunc3<R, T, U, W>, styles: string): DomCreateFunc3<R, T, U, W> & IClsName;
86
+ export function styled<Tag extends TagName>(tag: Tag, styles: string): DomCreateFunc<TagElem<Tag>> & IClsName;
87
+ export function styled<Args extends any[], R extends Element>(
88
+ creator: (...args: Args) => R, styles: string): typeof creator & IClsName;
83
89
  export function styled(creator: any, styles: string): IClsName {
84
90
  // Note that we intentionally minimize the work done when styled() is called; it's better to do
85
91
  // any needed work on first use. That's when we will actually build the css rules.
@@ -88,45 +94,54 @@ export function styled(creator: any, styles: string): IClsName {
88
94
  // Creator function reflects the input, with only the addition of style.use() at the end. Note
89
95
  // that it needs to be at the end because creator() might take special initial arguments.
90
96
  const newCreator = (typeof creator === 'string') ?
91
- (...args: DomElementArg[]) => dom(creator, ...args, style.use()) :
92
- (...args: any[]) => creator(...args, style.use());
97
+ (...args: any[]) => style.addToElem(dom(creator, ...args)) :
98
+ (...args: any[]) => style.addToElem(creator(...args));
93
99
  return Object.assign(newCreator, {
94
100
  className: style.className,
95
- cls: dom.clsPrefix.bind(null, style.className),
101
+ cls: clsPrefix.bind(null, style.className),
96
102
  });
97
103
  }
98
104
 
105
+ // Keyframes produces simply a string with the generated name. Note that these does not support
106
+ // nesting or ampersand (&) handling, since these would be difficult and are entirely unneeded.
107
+ export function keyframes(styles: string): string {
108
+ return (new KeyframePiece(styles)).className;
109
+ }
110
+
99
111
  function createCssRules(className: string, styles: string) {
100
- const nestedRules: string[] = [];
101
-
102
- // Parse out nested styles. Replacing them by empty string in the main section, and add them to
103
- // nestedRules array to be joined up at the end. Replace & with .className.
104
- const mainRules = styles.replace(/([^;]*)\s*{([^}]*)\s*}/g, (match, selector, rules) => {
105
- const fullSelector = selector.replace(/&/g, '.' + className);
106
- nestedRules.push(`${fullSelector} {${rules}}`);
107
- return '';
108
- });
112
+ // The first time we encounter a nested section, we know which are the "main" rules, and can
113
+ // wrap them appropriately.
114
+ const nestedStart = styles.search(/[^;]*\{/);
115
+ const mainRules = nestedStart < 0 ? styles : styles.slice(0, nestedStart);
116
+ const nestedRules = nestedStart < 0 ? "" : styles.slice(nestedStart);
117
+
118
+ // At the end, replace all occurrences of & with ".className".
119
+ return `& {${mainRules}\n}\n${nestedRules}`.replace(/&/g, className);
120
+ }
121
+
122
+ // Used by getNextStyleNum when running without a global window object (e.g. in tests).
123
+ const _global = {};
109
124
 
110
- // Actual styles to include into the generated stylesheet.
111
- return `.${className} {${mainRules}}\n` + nestedRules.join('\n');
125
+ // Keep the counter for next class attached to the global window object rather than be a library
126
+ // global. This way if by some chance multiple instance of grainjs are loaded into the page, it
127
+ // still works without overwriting class names (which would be extremely confusing).
128
+ function getNextStyleNum() {
129
+ const g: any = G.window || _global;
130
+ return g._grainNextStyleNum = (g._grainNextStyleNum || 0) + 1;
112
131
  }
113
132
 
114
133
  class StylePiece {
115
- // Index of next auto-generated css class name.
116
- private static _next: number = 1;
117
-
118
134
  // Set of all StylePieces created but not yet mounted.
119
135
  private static _unmounted = new Set<StylePiece>();
120
136
 
121
- // Generate a new css class name.
122
- private static _nextClassName() { return `_grain${this._next++}`; }
137
+ // Generate a new css class name. The suffix ensures that names like "&2" can't cause a conflict.
138
+ private static _nextClassName() { return `_grain${getNextStyleNum()}_`; }
123
139
 
124
140
  // Mount all unmounted StylePieces, and clear the _unmounted map.
125
141
  private static _mountAll(): void {
126
- const sheet = Array.from(this._unmounted, (p) => createCssRules(p.className, p._styles))
127
- .join('\n\n');
142
+ const sheet: string = Array.from(this._unmounted, (p) => p._createRules()).join("\n\n");
128
143
 
129
- G.document.head.appendChild(dom('style', sheet));
144
+ G.document.head!.appendChild(dom('style', sheet));
130
145
  for (const piece of this._unmounted) {
131
146
  piece._mounted = true;
132
147
  }
@@ -136,13 +151,24 @@ class StylePiece {
136
151
  public readonly className: string;
137
152
  private _mounted: boolean = false;
138
153
 
139
- constructor(private _styles: string) {
154
+ constructor(protected _styles: string) {
140
155
  this.className = StylePiece._nextClassName();
141
156
  StylePiece._unmounted.add(this);
142
157
  }
143
158
 
144
- public use(): DomElementMethod {
159
+ public addToElem<T extends Element>(elem: T): T {
145
160
  if (!this._mounted) { StylePiece._mountAll(); }
146
- return (elem) => { elem.classList.add(this.className); };
161
+ elem.classList.add(this.className);
162
+ return elem;
163
+ }
164
+
165
+ protected _createRules(): string {
166
+ return createCssRules('.' + this.className, this._styles);
167
+ }
168
+ }
169
+
170
+ class KeyframePiece extends StylePiece {
171
+ protected _createRules(): string {
172
+ return `@keyframes ${this.className} {${this._styles}}`;
147
173
  }
148
174
  }
package/lib/subscribe.ts CHANGED
@@ -19,17 +19,31 @@
19
19
  */
20
20
 
21
21
  import {DepItem} from './_computed_queue';
22
+ import {IDisposableOwner} from './dispose';
22
23
  import {Listener} from './emit';
24
+ import {fromKo, IKnockoutReadObservable} from './kowrap';
23
25
  import {BaseObservable as Obs} from './observable';
24
26
 
25
- export interface ISubscribable {
27
+ export interface ISubscribableObs {
26
28
  _getDepItem(): DepItem|null;
27
29
  addListener(callback: (val: any, prev: any) => void, optContext?: object): Listener;
28
30
  get(): any;
29
31
  }
30
32
 
33
+ export type ISubscribable = ISubscribableObs | IKnockoutReadObservable<any>;
34
+
35
+ // Type inference from the simpler Obs<T>|IKnockoutReadObservable<T> does not always produce
36
+ // correct T for ko.Observable. The formula below is a workaround. See also InferKoType in kowrap.
37
+ export type InferUseType<TObs extends Obs<any>|IKnockoutReadObservable<any>> =
38
+ TObs extends Obs<infer T> ? T :
39
+ TObs extends {peek(): infer U} ? U : never;
40
+
31
41
  // The generic type for the use() function that callbacks get.
32
- export type UseCB = <T>(obs: Obs<T>) => T;
42
+ export type UseCB = <TObs extends Obs<any>|IKnockoutReadObservable<any>>(obs: TObs) => InferUseType<TObs>;
43
+
44
+ export interface UseCBOwner extends UseCB { // tslint:disable-line:interface-name
45
+ owner: IDisposableOwner;
46
+ }
33
47
 
34
48
  interface IListenerWithInUse extends Listener {
35
49
  _inUse: boolean;
@@ -40,9 +54,9 @@ const emptyArray: ReadonlyArray<any> = [];
40
54
 
41
55
  export class Subscription {
42
56
  private readonly _depItem: DepItem;
43
- private readonly _dependencies: ReadonlyArray<ISubscribable>;
57
+ private readonly _dependencies: ReadonlyArray<ISubscribableObs>;
44
58
  private readonly _depListeners: ReadonlyArray<Listener>;
45
- private _dynDeps: Map<ISubscribable, IListenerWithInUse>;
59
+ private _dynDeps: Map<ISubscribableObs, IListenerWithInUse>;
46
60
  private _callback: (use: UseCB, ...args: any[]) => void;
47
61
  private _useFunc: UseCB;
48
62
 
@@ -59,7 +73,7 @@ export class Subscription {
59
73
  this._callback = callback;
60
74
  this._useFunc = this._useDependency.bind(this);
61
75
  if (owner) {
62
- (this._useFunc as any).owner = owner;
76
+ (this._useFunc as UseCBOwner).owner = owner;
63
77
  }
64
78
 
65
79
  this._evaluate();
@@ -85,7 +99,8 @@ export class Subscription {
85
99
  * subscription to `obs` if one doesn't yet exist.
86
100
  * @param {Observable} obs: The observable being used as a dependency.
87
101
  */
88
- private _useDependency(obs: ISubscribable) {
102
+ private _useDependency(_obs: ISubscribable) {
103
+ const obs = ('_getDepItem' in _obs) ? _obs : fromKo(_obs);
89
104
  let listener = this._dynDeps.get(obs);
90
105
  if (!listener) {
91
106
  listener = this._subscribeTo(obs) as IListenerWithInUse;
@@ -105,7 +120,7 @@ export class Subscription {
105
120
  if (this._callback === null) { return; } // Means this Subscription has been disposed.
106
121
  try {
107
122
  // Note that this is faster than using .map().
108
- const readArgs = [this._useFunc];
123
+ const readArgs: [UseCB, ...any[]] = [this._useFunc];
109
124
  for (let i = 0, len = this._dependencies.length; i < len; i++) {
110
125
  readArgs[i + 1] = this._dependencies[i].get();
111
126
  this._depItem.useDep(this._dependencies[i]._getDepItem());
@@ -130,7 +145,8 @@ export class Subscription {
130
145
  * @param {Observable} obs: The observable to subscribe to.
131
146
  * @returns {Listener} Listener object.
132
147
  */
133
- private _subscribeTo(obs: ISubscribable) {
148
+ private _subscribeTo(_obs: ISubscribable) {
149
+ const obs = ('_getDepItem' in _obs) ? _obs : fromKo(_obs);
134
150
  return obs.addListener(this._enqueue, this);
135
151
  }
136
152