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.
- package/README.md +54 -9
- package/dist/cjs/index.d.ts +6 -2
- package/dist/cjs/index.js +24 -17
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/PriorityQueue.d.ts +1 -1
- package/dist/cjs/lib/PriorityQueue.js +1 -0
- package/dist/cjs/lib/PriorityQueue.js.map +1 -1
- package/dist/cjs/lib/_computed_queue.d.ts +18 -0
- package/dist/cjs/lib/_computed_queue.js +6 -1
- package/dist/cjs/lib/_computed_queue.js.map +1 -1
- package/dist/cjs/lib/binding.d.ts +16 -10
- package/dist/cjs/lib/binding.js +22 -27
- package/dist/cjs/lib/binding.js.map +1 -1
- package/dist/cjs/lib/browserGlobals.d.ts +4 -1
- package/dist/cjs/lib/browserGlobals.js +2 -0
- package/dist/cjs/lib/browserGlobals.js.map +1 -1
- package/dist/cjs/lib/computed.d.ts +11 -7
- package/dist/cjs/lib/computed.js +16 -0
- package/dist/cjs/lib/computed.js.map +1 -1
- package/dist/cjs/lib/dispose.d.ts +106 -14
- package/dist/cjs/lib/dispose.js +76 -11
- package/dist/cjs/lib/dispose.js.map +1 -1
- package/dist/cjs/lib/dom.d.ts +21 -17
- package/dist/cjs/lib/dom.js +33 -26
- package/dist/cjs/lib/dom.js.map +1 -1
- package/dist/cjs/lib/domComponent.d.ts +71 -0
- package/dist/cjs/lib/domComponent.js +15 -0
- package/dist/cjs/lib/domComponent.js.map +1 -0
- package/dist/cjs/lib/domComputed.d.ts +89 -0
- package/dist/cjs/lib/domComputed.js +92 -0
- package/dist/cjs/lib/domComputed.js.map +1 -0
- package/dist/cjs/lib/{_domDispose.d.ts → domDispose.d.ts} +12 -2
- package/dist/cjs/lib/{_domDispose.js → domDispose.js} +21 -8
- package/dist/cjs/lib/domDispose.js.map +1 -0
- package/dist/cjs/lib/{_domForEach.d.ts → domForEach.d.ts} +2 -2
- package/dist/cjs/lib/domForEach.js +72 -0
- package/dist/cjs/lib/domForEach.js.map +1 -0
- package/dist/cjs/lib/{_domImpl.d.ts → domImpl.d.ts} +15 -12
- package/dist/cjs/lib/{_domImpl.js → domImpl.js} +23 -6
- package/dist/cjs/lib/domImpl.js.map +1 -0
- package/dist/cjs/lib/{_domMethods.d.ts → domMethods.d.ts} +27 -62
- package/dist/cjs/lib/{_domMethods.js → domMethods.js} +21 -76
- package/dist/cjs/lib/domMethods.js.map +1 -0
- package/dist/cjs/lib/domevent.d.ts +32 -21
- package/dist/cjs/lib/domevent.js +33 -12
- package/dist/cjs/lib/domevent.js.map +1 -1
- package/dist/cjs/lib/emit.d.ts +25 -2
- package/dist/cjs/lib/emit.js +3 -1
- package/dist/cjs/lib/emit.js.map +1 -1
- package/dist/cjs/lib/kowrap.d.ts +45 -3
- package/dist/cjs/lib/kowrap.js +93 -10
- package/dist/cjs/lib/kowrap.js.map +1 -1
- package/dist/cjs/lib/obsArray.d.ts +8 -8
- package/dist/cjs/lib/obsArray.js +1 -0
- package/dist/cjs/lib/obsArray.js.map +1 -1
- package/dist/cjs/lib/observable.d.ts +6 -1
- package/dist/cjs/lib/observable.js +11 -2
- package/dist/cjs/lib/observable.js.map +1 -1
- package/dist/cjs/lib/pureComputed.d.ts +3 -3
- package/dist/cjs/lib/pureComputed.js +2 -1
- package/dist/cjs/lib/pureComputed.js.map +1 -1
- package/dist/cjs/lib/styled.d.ts +76 -11
- package/dist/cjs/lib/styled.js +55 -23
- package/dist/cjs/lib/styled.js.map +1 -1
- package/dist/cjs/lib/subscribe.d.ts +15 -6
- package/dist/cjs/lib/subscribe.js +6 -2
- package/dist/cjs/lib/subscribe.js.map +1 -1
- package/dist/cjs/lib/util.js +1 -0
- package/dist/cjs/lib/util.js.map +1 -1
- package/dist/cjs/lib/widgets/input.d.ts +2 -2
- package/dist/cjs/lib/widgets/input.js +2 -2
- package/dist/cjs/lib/widgets/input.js.map +1 -1
- package/dist/cjs/lib/widgets/select.d.ts +1 -1
- package/dist/cjs/lib/widgets/select.js +1 -0
- package/dist/cjs/lib/widgets/select.js.map +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/PriorityQueue.js.map +1 -1
- package/dist/esm/lib/_computed_queue.js +5 -1
- package/dist/esm/lib/_computed_queue.js.map +1 -1
- package/dist/esm/lib/binding.js +20 -27
- package/dist/esm/lib/binding.js.map +1 -1
- package/dist/esm/lib/browserGlobals.js +1 -0
- package/dist/esm/lib/browserGlobals.js.map +1 -1
- package/dist/esm/lib/computed.js +15 -0
- package/dist/esm/lib/computed.js.map +1 -1
- package/dist/esm/lib/dispose.js +74 -11
- package/dist/esm/lib/dispose.js.map +1 -1
- package/dist/esm/lib/dom.js +21 -17
- package/dist/esm/lib/dom.js.map +1 -1
- package/dist/esm/lib/domComponent.js +11 -0
- package/dist/esm/lib/domComponent.js.map +1 -0
- package/dist/esm/lib/domComputed.js +84 -0
- package/dist/esm/lib/domComputed.js.map +1 -0
- package/dist/esm/lib/{_domDispose.js → domDispose.js} +19 -8
- package/dist/esm/lib/domDispose.js.map +1 -0
- package/dist/esm/lib/domForEach.js +68 -0
- package/dist/esm/lib/domForEach.js.map +1 -0
- package/dist/esm/lib/{_domImpl.js → domImpl.js} +20 -4
- package/dist/esm/lib/domImpl.js.map +1 -0
- package/dist/esm/lib/{_domMethods.js → domMethods.js} +8 -63
- package/dist/esm/lib/domMethods.js.map +1 -0
- package/dist/esm/lib/domevent.js +30 -11
- package/dist/esm/lib/domevent.js.map +1 -1
- package/dist/esm/lib/emit.js +2 -1
- package/dist/esm/lib/emit.js.map +1 -1
- package/dist/esm/lib/kowrap.js +90 -10
- package/dist/esm/lib/kowrap.js.map +1 -1
- package/dist/esm/lib/obsArray.js.map +1 -1
- package/dist/esm/lib/observable.js +9 -1
- package/dist/esm/lib/observable.js.map +1 -1
- package/dist/esm/lib/pureComputed.js +1 -1
- package/dist/esm/lib/pureComputed.js.map +1 -1
- package/dist/esm/lib/styled.js +52 -22
- package/dist/esm/lib/styled.js.map +1 -1
- package/dist/esm/lib/subscribe.js +5 -2
- package/dist/esm/lib/subscribe.js.map +1 -1
- package/dist/esm/lib/util.js.map +1 -1
- package/dist/esm/lib/widgets/input.js +1 -2
- package/dist/esm/lib/widgets/input.js.map +1 -1
- package/dist/esm/lib/widgets/select.js.map +1 -1
- package/dist/grain-full.debug.js +1627 -1222
- package/dist/grain-full.min.js +1 -1
- package/dist/grain-full.min.js.map +1 -1
- package/index.ts +6 -2
- package/lib/_computed_queue.ts +7 -1
- package/lib/binding.ts +33 -28
- package/lib/browserGlobals.ts +3 -1
- package/lib/computed.ts +37 -7
- package/lib/dispose.ts +81 -33
- package/lib/dom.ts +24 -18
- package/lib/domComponent.ts +89 -0
- package/lib/domComputed.ts +146 -0
- package/lib/{_domDispose.ts → domDispose.ts} +26 -8
- package/lib/{_domForEach.ts → domForEach.ts} +12 -11
- package/lib/{_domImpl.ts → domImpl.ts} +36 -30
- package/lib/{_domMethods.ts → domMethods.ts} +33 -103
- package/lib/domevent.ts +59 -22
- package/lib/emit.ts +2 -1
- package/lib/kowrap.ts +109 -11
- package/lib/obsArray.ts +2 -2
- package/lib/observable.ts +10 -2
- package/lib/pureComputed.ts +7 -6
- package/lib/styled.ts +65 -39
- package/lib/subscribe.ts +24 -8
- package/lib/widgets/input.ts +9 -7
- package/lib/widgets/select.ts +3 -3
- package/package.json +41 -42
- package/dist/cjs/lib/_domComponent.d.ts +0 -84
- package/dist/cjs/lib/_domComponent.js +0 -160
- package/dist/cjs/lib/_domComponent.js.map +0 -1
- package/dist/cjs/lib/_domDispose.js.map +0 -1
- package/dist/cjs/lib/_domForEach.js +0 -71
- package/dist/cjs/lib/_domForEach.js.map +0 -1
- package/dist/cjs/lib/_domImpl.js.map +0 -1
- package/dist/cjs/lib/_domMethods.js.map +0 -1
- package/dist/esm/lib/_domComponent.js +0 -155
- package/dist/esm/lib/_domComponent.js.map +0 -1
- package/dist/esm/lib/_domDispose.js.map +0 -1
- package/dist/esm/lib/_domForEach.js +0 -68
- package/dist/esm/lib/_domForEach.js.map +0 -1
- package/dist/esm/lib/_domImpl.js.map +0 -1
- package/dist/esm/lib/_domMethods.js.map +0 -1
- 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
|
|
33
|
-
let disposer = _disposeMap.get(
|
|
32
|
+
export function _disposeNode(node: Node): void {
|
|
33
|
+
let disposer = _disposeMap.get(node);
|
|
34
34
|
if (disposer) {
|
|
35
|
-
let key: Node|INodeFunc =
|
|
35
|
+
let key: Node|INodeFunc = node;
|
|
36
36
|
do {
|
|
37
37
|
_disposeMap.delete(key);
|
|
38
|
-
disposer(
|
|
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 {
|
|
71
|
+
* @param {Node} node: The element to run disposers on.
|
|
54
72
|
*/
|
|
55
|
-
export function domDispose(
|
|
56
|
-
|
|
73
|
+
export function domDispose(node: Node): void {
|
|
74
|
+
domDisposeHooks.disposeRecursive(node);
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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):
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 './
|
|
2
|
-
import {attrsElem} from './
|
|
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:
|
|
18
|
-
export type DomElementMethod =
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
//
|
|
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:
|
|
58
|
-
return _updateWithArgsOrDispose(_createFromTagString(_createElementHtml, tagString)
|
|
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:
|
|
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<
|
|
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<
|
|
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<
|
|
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:
|
|
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 =
|
|
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:
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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:
|
|
128
|
-
|
|
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:
|
|
142
|
-
|
|
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
|
-
*
|
|
230
|
-
*
|
|
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
|
-
*
|
|
258
|
-
*
|
|
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
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
226
|
+
* function myComponent(myArgs, testId: TestId = noTestId) {
|
|
227
|
+
* return dom(..., testId("some-name"),
|
|
228
|
+
* dom(..., testId("another-name"), ...),
|
|
229
|
+
* );
|
|
230
|
+
* }
|
|
266
231
|
*
|
|
267
|
-
*
|
|
268
|
-
* observables, e.g.
|
|
232
|
+
* In the fixture code using this component:
|
|
269
233
|
*
|
|
270
|
-
*
|
|
271
|
-
* (use(nlinesObs) > 1 ? dom('textarea') : dom('input')));
|
|
234
|
+
* import {makeTestId} from 'grainjs';
|
|
272
235
|
*
|
|
273
|
-
*
|
|
274
|
-
* are equivalent:
|
|
236
|
+
* dom(..., myComponent(myArgs, makeTestId('test-mycomp-'), ...)
|
|
275
237
|
*
|
|
276
|
-
*
|
|
277
|
-
* dom(..., listValue.map(x => dom('div', x)), ...)
|
|
238
|
+
* In the webdriver test code:
|
|
278
239
|
*
|
|
279
|
-
*
|
|
240
|
+
* driver.find('.test-my-comp-some-name')
|
|
241
|
+
* driver.find('.test-my-comp-another-name')
|
|
280
242
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
327
|
-
return
|
|
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;
|