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
@@ -0,0 +1,146 @@
1
+ import {BindableValue, subscribeElem} from './binding';
2
+ import {Holder, MultiHolder} from './dispose';
3
+ import {autoDisposeElem, domDispose} from './domDispose';
4
+ import {DomArg, DomMethod, frag} from './domImpl';
5
+
6
+ // Use the browser globals in a way that allows replacing them with mocks in tests.
7
+ import {G} from './browserGlobals';
8
+
9
+ // The type returned by domComputed(). It's actually an example of DomArg, but is given its own
10
+ // name for use in places where a DomComputed is suitable but a general DomArg is not.
11
+ export type DomComputed = [Node, Node, DomMethod];
12
+
13
+ export type DomContents = Node | string | DomComputed | void | null | undefined | IDomContentsArray;
14
+ export interface IDomContentsArray extends Array<DomContents> {}
15
+
16
+
17
+ /**
18
+ * Replaces the content between nodeBefore and nodeAfter, which should be two siblings within the
19
+ * same parent node. New content may be anything allowed as an argument to dom(), including null
20
+ * to insert nothing. Runs disposers, if any, on all removed content.
21
+ */
22
+ export function replaceContent(nodeBefore: Node, nodeAfter: Node, content: DomContents): void {
23
+ const elem = nodeBefore.parentNode;
24
+ if (elem) {
25
+ let next;
26
+ for (let n = nodeBefore.nextSibling; n && n !== nodeAfter; n = next) {
27
+ next = n.nextSibling;
28
+ domDispose(n);
29
+ elem.removeChild(n);
30
+ }
31
+ if (content) {
32
+ elem.insertBefore(content instanceof G.Node ? content : frag(content), nodeAfter);
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Appends dynamic DOM content to an element. The value may be an observable or function (from
39
+ * which a computed is created), whose value will be passed to `contentFunc` which should return
40
+ * DOM content. If the contentFunc is omitted, it defaults to identity, i.e. it's OK for the
41
+ * observable or function to return DOM directly.
42
+ *
43
+ * The DOM content returned may be an element, string, array, or null. Whenever the observable
44
+ * changes, previous content is disposed and removed, and new content added in its place.
45
+ *
46
+ * The following are roughly equivalent:
47
+ * (A) domComputed(nlinesObs, nlines => nlines > 1 ? dom('textarea') : dom('input'));
48
+ * (B) domComputed(use => use(nlinesObs) > 1 ? dom('textarea') : dom('input'));
49
+ * (C) domComputed(use => use(nlinesObs) > 1, isTall => isTall ? dom('textarea') : dom('input'));
50
+ *
51
+ * Here, (C) is best. Both (A) and (B) would rebuild DOM for any change in nlinesObs, but (C)
52
+ * encapsulates meaningful changes in the observable, and only recreates DOM when necessary.
53
+ *
54
+ * Syntax (B), without the second argument, may be useful in cases of DOM depending on several
55
+ * observables, e.g.
56
+ *
57
+ * domComputed(use => use(readonlyObs) ? dom('div') :
58
+ * (use(nlinesObs) > 1 ? dom('textarea') : dom('input')));
59
+ *
60
+ * If the argument is not an observable, domComputed() may but should not be used. The following
61
+ * are equivalent:
62
+ *
63
+ * dom(..., domComputed(listValue, list => `Have ${list.length} items`), ...)
64
+ * dom(..., `Have ${listValue.length} items`, ...)
65
+ *
66
+ * In this case, the latter is preferred as the clearly simpler one.
67
+ *
68
+ * @param valueObs: Observable or function for a computed.
69
+ * @param contentFunc: Function called with the result of valueObs as the input, and
70
+ * returning DOM as output. If omitted, defaults to the identity function.
71
+ */
72
+ // Note that DomMethod is excluded because it prevents typescript from inferring the type of
73
+ // the first argument when it's a function (and it's not useful).
74
+ export function domComputed(valueObs: BindableValue<Exclude<DomArg, DomMethod>>): DomComputed;
75
+ export function domComputed<T>(valueObs: BindableValue<T>, contentFunc: (val: T) => DomContents): DomComputed;
76
+ export function domComputed<T>(
77
+ valueObs: BindableValue<T>, contentFunc: (val: T) => DomContents = identity as any,
78
+ ): DomComputed {
79
+ const markerPre = G.document.createComment('a');
80
+ const markerPost = G.document.createComment('b');
81
+
82
+ // Function is added after markerPre and markerPost, so that it runs once they have already been
83
+ // attached to elem (the parent element).
84
+ return [markerPre, markerPost, (elem: Node) => {
85
+ subscribeElem(markerPost, valueObs,
86
+ (value) => replaceContent(markerPre, markerPost, contentFunc(value)));
87
+ }];
88
+ }
89
+
90
+ /**
91
+ * Like domComputed(), but the callback gets an additional first argument, owner, which may be
92
+ * used to take ownership of objects created by the callback. These will be disposed before each
93
+ * new call to the callback, and when the containing DOM is disposed.
94
+ *
95
+ * domComputedOwned(valueObs, (owner, value) => Editor.create(owner, value).renderSomething());
96
+ */
97
+ export function domComputedOwned<T>(
98
+ valueObs: BindableValue<T>, contentFunc: (owner: MultiHolder, val: T) => DomContents
99
+ ): DomComputed {
100
+ const holder = Holder.create(null);
101
+ const [markerPre, markerPost, func] = domComputed(valueObs,
102
+ (val: T) => contentFunc(MultiHolder.create(holder), val));
103
+ autoDisposeElem(markerPost, holder);
104
+ return [markerPre, markerPost, func];
105
+ }
106
+
107
+ function identity<T>(arg: T): T { return arg; }
108
+
109
+ /**
110
+ * Conditionally appends DOM to an element. The value may be an observable or function (from which
111
+ * a computed is created), whose value -- if truthy -- will be passed to `contentFunc` which
112
+ * should return DOM content. If the value is falsy, DOM content is removed.
113
+ *
114
+ * Note that if the observable changes between different truthy values, contentFunc gets called
115
+ * for each value, and previous content gets destroyed. To consider all truthy values the same,
116
+ * use an observable that returns a proper boolean, e.g.
117
+ *
118
+ * dom.maybe(use => Boolean(use(fooObs)), () => dom(...));
119
+ *
120
+ * As with domComputed(), dom.maybe() may but should not be used when the argument is not an
121
+ * observable or function. The following are equivalent:
122
+ *
123
+ * dom(..., dom.maybe(myValue, () => dom(...)));
124
+ * dom(..., myValue ? dom(...) : null);
125
+ *
126
+ * The latter is preferred for being simpler.
127
+ *
128
+ * @param boolValueObs: Observable or function for a computed.
129
+ * @param contentFunc: Called with the result of boolValueObs when it is truthy. Should return DOM.
130
+ */
131
+ export function maybe<T>(boolValueObs: BindableValue<T>,
132
+ contentFunc: (val: NonNullable<T>) => DomContents): DomComputed {
133
+ return domComputed(boolValueObs, (value) => value ? contentFunc(value!) : null);
134
+ }
135
+
136
+ /**
137
+ * Like maybe(), but the callback gets an additional first argument, owner, which may be used to
138
+ * take ownership of objects created by the callback. These will be disposed before each new call
139
+ * to the callback, and when the condition becomes false or the containing DOM gets disposed.
140
+ *
141
+ * maybeOwned(showEditor, (owner) => Editor.create(owner).renderSomething());
142
+ */
143
+ export function maybeOwned<T>(boolValueObs: BindableValue<T>,
144
+ contentFunc: (owner: MultiHolder, val: NonNullable<T>) => DomContents): DomComputed {
145
+ return domComputedOwned(boolValueObs, (owner, value) => value ? contentFunc(owner, value!) : null);
146
+ }
@@ -18,7 +18,7 @@ export type INodeFunc = (node: Node) => void;
18
18
  // Internal helper to walk the DOM tree, calling visitFunc(elem) on all descendants of elem.
19
19
  // Descendants are processed first.
20
20
  function _walkDom(elem: Node, visitFunc: INodeFunc): void {
21
- let c = elem.firstChild;
21
+ let c: Node|null = elem.firstChild;
22
22
  while (c) {
23
23
  // Note: this might be better done using an explicit stack, but in practice DOM trees aren't
24
24
  // so deep as to cause problems.
@@ -29,13 +29,13 @@ function _walkDom(elem: Node, visitFunc: INodeFunc): void {
29
29
  }
30
30
 
31
31
  // Internal helper to run all disposers for a single element.
32
- function _disposeElem(elem: Node): void {
33
- let disposer = _disposeMap.get(elem);
32
+ export function _disposeNode(node: Node): void {
33
+ let disposer = _disposeMap.get(node);
34
34
  if (disposer) {
35
- let key: Node|INodeFunc = elem;
35
+ let key: Node|INodeFunc = node;
36
36
  do {
37
37
  _disposeMap.delete(key);
38
- disposer(elem);
38
+ disposer(node);
39
39
  // Find the next disposer; these are chained when there are multiple.
40
40
  key = disposer;
41
41
  disposer = _disposeMap.get(key);
@@ -43,6 +43,24 @@ function _disposeElem(elem: Node): void {
43
43
  }
44
44
  }
45
45
 
46
+ function _disposeNodeRecursive(node: Node): void {
47
+ _walkDom(node, domDisposeHooks.disposeNode);
48
+ }
49
+
50
+ export interface IDomDisposeHooks {
51
+ disposeRecursive: (node: Node) => void;
52
+ disposeNode: (node: Node) => void;
53
+ }
54
+
55
+ /**
56
+ * Support for extending dom disposal. This is very low-level, and needs utmost care. Any
57
+ * disposers set should take care of calling the original versions of the disposers.
58
+ */
59
+ export const domDisposeHooks: IDomDisposeHooks = {
60
+ disposeNode: _disposeNode,
61
+ disposeRecursive: _disposeNodeRecursive,
62
+ };
63
+
46
64
  /**
47
65
  * Run disposers associated with any descendant of elem or with elem itself. Disposers get
48
66
  * associated with elements using dom.onDispose(). Descendants are processed first.
@@ -50,10 +68,10 @@ function _disposeElem(elem: Node): void {
50
68
  * It is automatically called if one of the function arguments to dom() throws an exception during
51
69
  * element creation. This way any onDispose() handlers set on the unfinished element get called.
52
70
  *
53
- * @param {Element} elem: The element to run disposers on.
71
+ * @param {Node} node: The element to run disposers on.
54
72
  */
55
- export function domDispose(elem: Node): void {
56
- _walkDom(elem, _disposeElem);
73
+ export function domDispose(node: Node): void {
74
+ domDisposeHooks.disposeRecursive(node);
57
75
  }
58
76
 
59
77
  /**
@@ -1,6 +1,6 @@
1
- import {domDispose} from './_domDispose';
2
- import {DomMethod, frag} from './_domImpl';
3
- import {replaceContent} from './_domMethods';
1
+ import {DomContents, replaceContent} from './domComputed';
2
+ import {autoDisposeElem, domDispose} from './domDispose';
3
+ import {frag} from './domImpl';
4
4
  import {computedArray, MaybeObsArray, ObsArray} from './obsArray';
5
5
 
6
6
  // Use the browser globals in a way that allows replacing them with mocks in tests.
@@ -24,19 +24,20 @@ import {G} from './browserGlobals';
24
24
  * If you'd like to map the DOM node back to its source item, use dom.data() and dom.getData() in
25
25
  * itemCreateFunc().
26
26
  */
27
- export function forEach<T>(obsArray: MaybeObsArray<T>, itemCreateFunc: (item: T) => Node|null): DomMethod {
28
- return (elem: Node) => {
29
- const markerPre = G.document.createComment('a');
30
- const markerPost = G.document.createComment('b');
31
- elem.appendChild(markerPre);
32
- elem.appendChild(markerPost);
33
-
27
+ export function forEach<T>(obsArray: MaybeObsArray<T>, itemCreateFunc: (item: T) => Node|null): DomContents {
28
+ const markerPre = G.document.createComment('a');
29
+ const markerPost = G.document.createComment('b');
30
+ return [markerPre, markerPost, (elem: Node) => {
34
31
  if (Array.isArray(obsArray)) {
35
32
  replaceContent(markerPre, markerPost, obsArray.map(itemCreateFunc));
36
33
  return;
37
34
  }
38
35
 
39
36
  const nodes: ObsArray<Node|null> = computedArray(obsArray, itemCreateFunc);
37
+
38
+ // Be sure to dispose the newly-created array when the DOM it's associated with is gone.
39
+ autoDisposeElem(markerPost, nodes);
40
+
40
41
  nodes.addListener((newArr: Array<Node|null>, oldArr: Array<Node|null>, splice?) => {
41
42
  if (splice) {
42
43
  // Remove the elements that are gone.
@@ -68,5 +69,5 @@ export function forEach<T>(obsArray: MaybeObsArray<T>, itemCreateFunc: (item: T)
68
69
  }
69
70
  });
70
71
  replaceContent(markerPre, markerPost, nodes.get());
71
- };
72
+ }];
72
73
  }
@@ -1,5 +1,5 @@
1
- import {domDispose} from './_domDispose';
2
- import {attrsElem} from './_domMethods';
1
+ import {domDispose} from './domDispose';
2
+ import {attrsElem} from './domMethods';
3
3
 
4
4
  // Use the browser globals in a way that allows replacing them with mocks in tests.
5
5
  import {G} from './browserGlobals';
@@ -14,23 +14,30 @@ import {G} from './browserGlobals';
14
14
  // is more flexible and robust, and only suffers from slightly more verbosity. E.g.
15
15
  // `dom('div', dom.attr('href', url))`.
16
16
 
17
- export type DomMethod = (elem: Node) => DomArg|void;
18
- export type DomElementMethod = (elem: Element) => DomElementArg|void;
17
+ export type DomMethod<T = Node> = (elem: T) => DomArg<T>|void;
18
+ export type DomElementMethod = DomMethod<HTMLElement>;
19
19
 
20
20
  export interface IAttrObj {
21
21
  [attrName: string]: string|boolean|null|undefined;
22
22
  }
23
23
 
24
- // Type of argument to dom-building functions, that work for any Node.
25
- export type DomArg = Node | string | IDomArgArray | DomMethod | void | null | undefined;
26
- export interface IDomArgArray extends Array<DomArg> {}
24
+ // Type of argument to dom-building functions. Allows IAttrObj when applied to an Element.
25
+ //
26
+ // Note that DomArg<A> differs from DomArg<B> in what callbacks are accepted, so DomArg<Element>
27
+ // can be assigned to DomArg<HTMLInputElement>, but not vice versa. When writing a function that
28
+ // accepts DomArgs and applies them to an element, use the most specific DomArg type that works
29
+ // for that element, e.g. DomArg<HTMLInputElement> if possible, then DomElementArg, then DomArg.
30
+ export type DomArg<T = Node> = Node | string | void | null | undefined |
31
+ IDomArgs<T> | DomMethod<T> | (T extends Element ? IAttrObj : never);
27
32
 
28
- // More options are allowed when dom-building functions are used on an Element.
29
- export type DomElementArg = DomArg | IAttrObj | IDomElementArgArray | DomElementMethod;
30
- export interface IDomElementArgArray extends Array<DomElementArg> {}
33
+ export interface IDomArgs<T = Node> extends Array<DomArg<T>> {}
34
+
35
+ // Alias for backward compatibility.
36
+ export type DomElementArg = DomArg<HTMLElement>;
31
37
 
32
38
  // The goal of the above declarations is to get help from TypeScript in detecting incorrect usage:
33
- // import {text, hide} from './_domMethods';
39
+ // (See test/types/dom.ts for a test of this.)
40
+ // import {text, hide} from './domMethods';
34
41
  // dom('div', text('hello')); // OK
35
42
  // dom('div', hide(true)); // OK
36
43
  // dom('div', {title: 'hello'}); // OK
@@ -43,6 +50,10 @@ export interface IDomElementArgArray extends Array<DomElementArg> {}
43
50
  * The first argument is a string consisting of a tag name, with optional #foo suffix
44
51
  * to add the ID 'foo', and zero or more .bar suffixes to add a CSS class 'bar'.
45
52
  *
53
+ * NOTE that better typings are available when a tag is used directly, e.g.
54
+ * dom('input', {id: 'foo'}, (elem) => ...) --> elem has type HTMLInputElement
55
+ * dom('input#foo', (elem) => ...) --> elem has type HTMLElement
56
+ *
46
57
  * The rest of the arguments are optional and may be:
47
58
  *
48
59
  * Nodes - which become children of the created element;
@@ -54,15 +65,18 @@ export interface IDomElementArgArray extends Array<DomElementArg> {}
54
65
  * "dom methods" - expressions such as `dom.attr('href', url)` or `dom.hide(obs)`, which
55
66
  * are actually special cases of the "functions" category.
56
67
  */
57
- export function dom(tagString: string, ...args: DomElementArg[]): HTMLElement {
58
- return _updateWithArgsOrDispose(_createFromTagString(_createElementHtml, tagString), args);
68
+ export function dom<Tag extends TagName>(tagString: Tag, ...args: IDomArgs<TagElem<Tag>>): TagElem<Tag> {
69
+ return _updateWithArgsOrDispose(_createFromTagString(_createElementHtml, tagString) as TagElem<Tag>, args);
59
70
  }
60
71
 
72
+ export type TagName = keyof HTMLElementTagNameMap|string;
73
+ export type TagElem<T extends TagName> = T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : HTMLElement;
74
+
61
75
  /**
62
76
  * svg('tag#id.class1.class2', ...args)
63
77
  * Same as dom(...), but creates an SVG element.
64
78
  */
65
- export function svg(tagString: string, ...args: DomElementArg[]): SVGElement {
79
+ export function svg(tagString: string, ...args: IDomArgs<SVGElement>): SVGElement {
66
80
  return _updateWithArgsOrDispose(_createFromTagString(_createElementSvg, tagString), args);
67
81
  }
68
82
 
@@ -116,18 +130,14 @@ function _createFromTagString<E extends Element>(createFunc: (tag: string) => E,
116
130
  /**
117
131
  * Update an element with any number of arguments, as documented in dom().
118
132
  */
119
- export function update<E extends Element>(elem: E, ...args: DomElementArg[]): E;
120
- export function update<E extends Node>(elem: E, ...args: DomArg[]): E;
121
- export function update(elem: any, ...args: DomElementArg[]): Node {
133
+ export function update<T extends Node, Args extends IDomArgs<T>>(elem: T, ...args: Args): T {
122
134
  return _updateWithArgs(elem, args);
123
135
  }
124
136
 
125
137
  /**
126
138
  * Update an element with an array of arguments.
127
139
  */
128
- function _updateWithArgs<E extends Element>(elem: E, args: DomElementArg[]): E;
129
- function _updateWithArgs<E extends Node>(elem: E, args: DomArg[]): E;
130
- function _updateWithArgs(elem: any, args: DomElementArg[]): Node {
140
+ function _updateWithArgs<T extends Node>(elem: T, args: IDomArgs<T>): T {
131
141
  for (const arg of args) {
132
142
  _updateWithArg(elem, arg);
133
143
  }
@@ -139,9 +149,7 @@ function _updateWithArgs(elem: any, args: DomElementArg[]): Node {
139
149
  * an internal helper to be used whenever elem is a newly-created element. If elem is an existing
140
150
  * element which the user already knows about, then _updateWithArgs should be called.
141
151
  */
142
- function _updateWithArgsOrDispose<E extends Element>(elem: E, args: DomElementArg[]): E;
143
- function _updateWithArgsOrDispose<E extends Node>(elem: E, args: DomArg[]): E;
144
- function _updateWithArgsOrDispose(elem: any, args: DomElementArg[]): Node {
152
+ function _updateWithArgsOrDispose<T extends Node>(elem: T, args: IDomArgs<T>): T {
145
153
  try {
146
154
  return _updateWithArgs(elem, args);
147
155
  } catch (e) {
@@ -150,11 +158,9 @@ function _updateWithArgsOrDispose(elem: any, args: DomElementArg[]): Node {
150
158
  }
151
159
  }
152
160
 
153
- function _updateWithArg(elem: Element, arg: DomElementArg): void;
154
- function _updateWithArg(elem: Node, arg: DomArg): void;
155
- function _updateWithArg(elem: any, arg: DomElementArg): void {
161
+ function _updateWithArg<T extends Node>(elem: T, arg: DomArg<T>): void {
156
162
  if (typeof arg === 'function') {
157
- const value: DomArg = (arg as DomMethod)(elem);
163
+ const value: DomArg<T> = arg(elem);
158
164
  // Skip the recursive call in the common case when the function returns nothing.
159
165
  if (value !== undefined && value !== null) {
160
166
  _updateWithArg(elem, value);
@@ -166,7 +172,7 @@ function _updateWithArg(elem: any, arg: DomElementArg): void {
166
172
  } else if (arg instanceof G.Node) {
167
173
  elem.appendChild(arg);
168
174
  } else if (typeof arg === 'object') {
169
- attrsElem(elem, arg);
175
+ attrsElem(elem as any, arg);
170
176
  } else {
171
177
  elem.appendChild(G.document.createTextNode(arg));
172
178
  }
@@ -175,9 +181,9 @@ function _updateWithArg(elem: any, arg: DomElementArg): void {
175
181
  /**
176
182
  * Creates a DocumentFragment processing arguments the same way as the dom() function.
177
183
  */
178
- export function frag(...args: DomArg[]): DocumentFragment {
184
+ export function frag(...args: IDomArgs<DocumentFragment>): DocumentFragment {
179
185
  const elem = G.document.createDocumentFragment();
180
- return _updateWithArgsOrDispose(elem, args);
186
+ return _updateWithArgsOrDispose<DocumentFragment>(elem, args);
181
187
  }
182
188
 
183
189
  /**
@@ -1,6 +1,6 @@
1
- import {autoDisposeElem, domDispose, onDisposeElem} from './_domDispose';
2
- import {DomArg, DomElementMethod, DomMethod, frag, IAttrObj} from './_domImpl';
3
- import {BindableValue, subscribe as subscribeBinding} from './binding';
1
+ import {BindableValue, subscribeElem as _subscribe} from './binding';
2
+ import {onDisposeElem} from './domDispose';
3
+ import {DomElementMethod, DomMethod, IAttrObj} from './domImpl';
4
4
 
5
5
  // Use the browser globals in a way that allows replacing them with mocks in tests.
6
6
  import {G} from './browserGlobals';
@@ -11,15 +11,6 @@ import {G} from './browserGlobals';
11
11
  */
12
12
  const _dataMap: WeakMap<Node, {[key: string]: any}> = new WeakMap();
13
13
 
14
- /**
15
- * Internal helper that binds the callback to valueObs, which may be a value, observble, or
16
- * function, and attaches a disposal callback to the passed-in element.
17
- */
18
- function _subscribe<T>(elem: Node, valueObs: BindableValue<T>,
19
- callback: (newVal: T, oldVal?: T) => void): void {
20
- autoDisposeElem(elem, subscribeBinding(valueObs, callback));
21
- }
22
-
23
14
  /**
24
15
  * Sets multiple attributes of a DOM element. The `attrs()` variant takes no `elem` argument.
25
16
  * Null and undefined values are omitted, and booleans are either omitted or set to empty string.
@@ -124,8 +115,8 @@ export function prop<T>(property: string, valueObs: BindableValue<T>): DomMethod
124
115
  * @param {Element} elem: The element to update.
125
116
  * @param {Boolean} boolValue: True to show the element, false to hide it.
126
117
  */
127
- export function showElem(elem: Element, boolValue: boolean): void {
128
- (elem as HTMLElement).style.display = boolValue ? '' : 'none';
118
+ export function showElem(elem: HTMLElement, boolValue: boolean): void {
119
+ elem.style.display = boolValue ? '' : 'none';
129
120
  }
130
121
  export function show(boolValueObs: BindableValue<boolean>): DomElementMethod {
131
122
  return (elem) =>
@@ -138,8 +129,8 @@ export function show(boolValueObs: BindableValue<boolean>): DomElementMethod {
138
129
  * @param {Element} elem: The element to update.
139
130
  * @param {Boolean} boolValue: True to hide the element, false to show it.
140
131
  */
141
- export function hideElem(elem: Element, boolValue: boolean): void {
142
- (elem as HTMLElement).style.display = boolValue ? 'none' : '';
132
+ export function hideElem(elem: HTMLElement, boolValue: boolean): void {
133
+ elem.style.display = boolValue ? 'none' : '';
143
134
  }
144
135
  export function hide(boolValueObs: BindableValue<boolean>): DomElementMethod {
145
136
  return (elem) =>
@@ -226,103 +217,42 @@ export function getData(elem: Node, key: string) {
226
217
  }
227
218
 
228
219
  /**
229
- * Replaces the content between nodeBefore and nodeAfter, which should be two siblings within the
230
- * same parent node. New content may be anything allowed as an argument to dom(), including null
231
- * to insert nothing. Runs disposers, if any, on all removed content.
232
- */
233
- export function replaceContent(nodeBefore: Node, nodeAfter: Node, content: DomArg): void {
234
- const elem = nodeBefore.parentNode;
235
- if (elem) {
236
- let next;
237
- for (let n = nodeBefore.nextSibling; n && n !== nodeAfter; n = next) {
238
- next = n.nextSibling;
239
- domDispose(n);
240
- elem.removeChild(n);
241
- }
242
- if (content) {
243
- elem.insertBefore(content instanceof G.Node ? content : frag(content), nodeAfter);
244
- }
245
- }
246
- }
247
-
248
- /**
249
- * Appends dynamic DOM content to an element. The value may be an observable or function (from
250
- * which a computed is created), whose value will be passed to `contentFunc` which should return
251
- * DOM content. If the contentFunc is omitted, it defaults to identity, i.e. it's OK for the
252
- * observable or function to return DOM directly.
253
- *
254
- * The DOM content returned may be an element, string, array, or null. Whenever the observable
255
- * changes, previous content is disposed and removed, and new content added in its place.
220
+ * A very simple setup to identify DOM elements for testing purposes. Here's the recommended
221
+ * usage.
256
222
  *
257
- * These are roughly equivalent:
258
- * (A) domComputed(nlinesObs, nlines => nlines > 1 ? dom('textarea') : dom('input'));
259
- * (B) domComputed(use => use(nlinesObs) > 1, isTall => isTall ? dom('textarea') : dom('input'));
260
- * (C) domComputed(use => use(nlinesObs) > 1 ? dom('textarea') : dom('input'));
223
+ * // In the component to be tested.
224
+ * import {noTestId, TestId} from 'grainjs';
261
225
  *
262
- * Here, (B) is best. It encapsulates meaningful changes in the observable, and separates DOM
263
- * creation, so that DOM is only recreated when necessary. Between (A) and (C), (A) should be
264
- * preferred. Both (A) and (C) would rebuild DOM for any change in nlinesObs, but in (C), it's too
265
- * easy to use `use` more than necessary and cause inadvertent rebuilding of DOM.
226
+ * function myComponent(myArgs, testId: TestId = noTestId) {
227
+ * return dom(..., testId("some-name"),
228
+ * dom(..., testId("another-name"), ...),
229
+ * );
230
+ * }
266
231
  *
267
- * Syntax (C), without the last argument, may be useful in cases of DOM depending on several
268
- * observables, e.g.
232
+ * In the fixture code using this component:
269
233
  *
270
- * domComputed(use => use(readonlyObs) ? dom('div') :
271
- * (use(nlinesObs) > 1 ? dom('textarea') : dom('input')));
234
+ * import {makeTestId} from 'grainjs';
272
235
  *
273
- * If the argument is not an observable, domComputed() may but should not be used. The following
274
- * are equivalent:
236
+ * dom(..., myComponent(myArgs, makeTestId('test-mycomp-'), ...)
275
237
  *
276
- * dom(..., domComputed(listValue, list => list.map(x => dom('div', x))), ...)
277
- * dom(..., listValue.map(x => dom('div', x)), ...)
238
+ * In the webdriver test code:
278
239
  *
279
- * In this case, the latter is preferred as the clearly simpler one.
240
+ * driver.find('.test-my-comp-some-name')
241
+ * driver.find('.test-my-comp-another-name')
280
242
  *
281
- * @param {Element} elem: The element to which to append the DOM content.
282
- * @param {Object} valueObs: Observable or function for a computed.
283
- * @param [Function] contentFunc: Function called with the result of valueObs as the input, and
284
- * returning DOM as output. If omitted, defaults to the identity function.
243
+ * When myComponent() is created with testId argument omitted, the testId() calls are no-ops. When
244
+ * makeTestId('test-foo-') is passed in, testId() calls simply add a css class with that prefix.
285
245
  */
286
- export function domComputed<T extends DomArg>(valueObs: BindableValue<T>): DomMethod;
287
- export function domComputed<T>(valueObs: BindableValue<T>, contentFunc: (val: T) => DomArg): DomMethod;
288
- export function domComputed<T>(valueObs: BindableValue<T>, contentFunc?: (val: T) => DomArg): DomMethod {
289
- const _contentFunc: (val: T) => DomArg = contentFunc || (identity as any);
290
- return (elem: Node) => {
291
- const markerPre = G.document.createComment('a');
292
- const markerPost = G.document.createComment('b');
293
- elem.appendChild(markerPre);
294
- elem.appendChild(markerPost);
295
- _subscribe(elem, valueObs,
296
- (value) => replaceContent(markerPre, markerPost, _contentFunc(value)));
297
- };
298
- }
299
-
300
- function identity<T>(arg: T): T { return arg; }
246
+ export type TestId = (name: string) => DomElementMethod|null;
301
247
 
302
248
  /**
303
- * Conditionally appends DOM to an element. The value may be an observable or function (from which
304
- * a computed is created), whose value -- if truthy -- will be passed to `contentFunc` which
305
- * should return DOM content. If the value is falsy, DOM content is removed.
306
- *
307
- * Note that if the observable changes between different truthy values, contentFunc gets called
308
- * for each value, and previous content gets destroyed. To consider all truthy values the same,
309
- * use an observable that returns a proper boolean, e.g.
310
- *
311
- * dom.maybe(use => Boolean(use(fooObs)), () => dom(...));
312
- *
313
- * As with domComputed(), dom.maybe() may but should not be used when the argument is not an
314
- * observable or function. The following are equivalent:
315
- *
316
- * dom(..., dom.maybe(myValue, () => dom(...)));
317
- * dom(..., myValue ? dom(...) : null);
318
- *
319
- * The latter is preferred for being simpler.
320
- *
321
- * @param {Element} elem: The element to which to append the DOM content.
322
- * @param {Object} boolValueObs: Observable or function for a computed.
323
- * @param [Function] contentFunc: Function called with the result of boolValueObs when it is
324
- * truthy. Should returning DOM as output.
249
+ * See documentation for TestId above.
325
250
  */
326
- export function maybe<T>(boolValueObs: BindableValue<T>, contentFunc: (val: T) => DomArg): DomMethod {
327
- return domComputed(boolValueObs, (value) => value ? contentFunc(value) : null);
251
+ export function makeTestId(prefix: string): TestId {
252
+ return clsPrefix.bind(null, prefix);
328
253
  }
254
+
255
+ /**
256
+ * See documentation for TestId above.
257
+ */
258
+ export const noTestId: TestId = (name: string) => null;