solarite 0.7.1 → 0.9.0
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/dist/Solarite-debug.js +2206 -955
- package/dist/Solarite.js +2154 -909
- package/dist/Solarite.min.js +2 -2
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-dev-runtime.min.js +3 -0
- package/dist/jsx-runtime.js +86 -0
- package/dist/jsx-runtime.min.js +2 -0
- package/package.json +8 -7
- package/readme.md +9 -9
- package/src/Globals.js +22 -2
- package/src/MappedList.js +34 -0
- package/src/MultiValueMap.js +4 -4
- package/src/NodeGroup.js +205 -119
- package/src/Path.js +52 -53
- package/src/PathToAttribValue.js +259 -112
- package/src/PathToAttribs.js +10 -18
- package/src/PathToComponent.js +121 -83
- package/src/PathToEvent.js +32 -12
- package/src/PathToKey.js +0 -6
- package/src/PathToNodes.js +695 -155
- package/src/RootNodeGroup.js +45 -35
- package/src/Selector.js +207 -0
- package/src/Shell.js +313 -123
- package/src/Solarite.d.ts +91 -13
- package/src/Solarite.js +38 -25
- package/src/Template.js +20 -7
- package/src/Util.js +88 -60
- package/src/assert.js +1 -1
- package/src/assignAttributes.js +20 -19
- package/src/h.js +64 -37
- package/src/jsx.js +4 -7
- package/src/toEl.js +1 -1
- package/src/HtmlParser.js +0 -91
- package/src/PathToComment.js +0 -8
package/src/PathToComponent.js
CHANGED
|
@@ -4,6 +4,7 @@ import delve, {isDelvePath} from "./delve.js";
|
|
|
4
4
|
import assert from "./assert.js";
|
|
5
5
|
import PathToAttribValue from "./PathToAttribValue.js";
|
|
6
6
|
import PathToKey from "./PathToKey.js";
|
|
7
|
+
import PathToEvent from "./PathToEvent.js";
|
|
7
8
|
import Globals from "./Globals.js";
|
|
8
9
|
import {getObjectHash} from "./Template.js";
|
|
9
10
|
|
|
@@ -23,15 +24,15 @@ export default class PathToComponent extends Path {
|
|
|
23
24
|
* Call render() on the component pointed to by this Path.
|
|
24
25
|
* And instantiate it (from a -solarite-placeholder element) if it hasn't been done yet.
|
|
25
26
|
* @param exprs {Expr[][]} Expressions to evaluate for each attribute to pass to the constructor.
|
|
26
|
-
* This is different than other Path.
|
|
27
|
+
* This is different than other Path.applyAll() functions which only receive Expr[] and not Expr[][].
|
|
27
28
|
* Because here we're receiving an array of arrays of expressions, one for each dynamic attribute. */
|
|
28
|
-
|
|
29
|
-
//#
|
|
29
|
+
applyAll(exprs) {
|
|
30
|
+
//#IFDEBUG
|
|
30
31
|
assert(Array.isArray(exprs));
|
|
31
32
|
assert(!exprs.length || Array.isArray(exprs[0]));
|
|
32
33
|
//#ENDIF
|
|
33
34
|
|
|
34
|
-
//#
|
|
35
|
+
//#IFDEBUG
|
|
35
36
|
assert(exprs.length === this.attribPaths.length);
|
|
36
37
|
//#ENDIF
|
|
37
38
|
|
|
@@ -47,8 +48,15 @@ export default class PathToComponent extends Path {
|
|
|
47
48
|
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
48
49
|
if (attribPath instanceof PathToKey) // The list key is never a component arg.
|
|
49
50
|
continue;
|
|
51
|
+
// Event attributes like onchange=${...} are bound with addEventListener when the
|
|
52
|
+
// PathToEvent itself is applied. They must not also become constructor fields:
|
|
53
|
+
// a component that assigns its fields onto itself would set the native on*
|
|
54
|
+
// property, making the handler fire a second time with only the (event) argument
|
|
55
|
+
// instead of Solarite's documented (event, element) signature.
|
|
56
|
+
if (attribPath instanceof PathToEvent)
|
|
57
|
+
continue;
|
|
50
58
|
if (attribPath instanceof PathToAttribValue) {
|
|
51
|
-
let name = Util.dashesToCamel(attribPath.
|
|
59
|
+
let name = Util.dashesToCamel(attribPath.attribName);
|
|
52
60
|
|
|
53
61
|
// Resolve two way bindimg path before we pass it to the component.
|
|
54
62
|
let value = attribPath.getValue(exprs[i]);
|
|
@@ -70,85 +78,120 @@ export default class PathToComponent extends Path {
|
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
// Constructing a component runs arbitrary user code -- field initializers, the
|
|
82
|
+
// constructor body, render() -- and that code can build more components, re-entering
|
|
83
|
+
// this method and overwriting the hand-off parked below. Saving the caller's value
|
|
84
|
+
// here and restoring it in the finally makes the JS call stack the stack this hand-off
|
|
85
|
+
// needs, and unlike an explicit stack it cannot leak if construction throws.
|
|
86
|
+
let prevSlotChildren = Globals.currentSlotChildren;
|
|
87
|
+
try {
|
|
88
|
+
// 2. Instantiate component on first time.
|
|
89
|
+
let isAttrib = el.getAttribute('_is');
|
|
90
|
+
if (el.tagName.endsWith('-SOLARITE-PLACEHOLDER') || isAttrib) {
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
// 2a. Instantiate component
|
|
94
|
+
let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
|
|
95
|
+
let Constructor = customElements.get(tagName);
|
|
96
|
+
|
|
97
|
+
// Not defined yet (e.g. the module is being lazily imported): keep the placeholder
|
|
98
|
+
// and instantiate when the definition lands, like a native custom-element upgrade.
|
|
99
|
+
// deferredExprs always holds the LATEST exprs so re-renders while undefined win.
|
|
100
|
+
if (!Constructor) {
|
|
101
|
+
this.deferredExprs = exprs;
|
|
102
|
+
if (!this.whenDefinedPending) {
|
|
103
|
+
this.whenDefinedPending = true;
|
|
104
|
+
console.warn(`Solarite: <${tagName}> is not defined yet; waiting for customElements.define().`);
|
|
105
|
+
customElements.whenDefined(tagName).then(() => {
|
|
106
|
+
this.whenDefinedPending = false;
|
|
107
|
+
let deferred = this.deferredExprs;
|
|
108
|
+
this.deferredExprs = null;
|
|
109
|
+
// Skip if a newer render already instantiated or replaced the placeholder.
|
|
110
|
+
if (deferred && this.nodeMarker === el && el.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
|
|
111
|
+
this.applyAll(deferred);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
76
116
|
|
|
117
|
+
// Hand the children declared inside the component's tag to the RootNodeGroup that
|
|
118
|
+
// its render() is about to create. There is no other channel: the children have
|
|
119
|
+
// to be parked before new Constructor(), because a Solarite constructor may call
|
|
120
|
+
// this.render() itself, and the element that would otherwise carry them does not
|
|
121
|
+
// exist yet.
|
|
122
|
+
Globals.currentSlotChildren = {Constructor, nodes: [...el.childNodes]};
|
|
123
|
+
let newEl = new Constructor(attribs);
|
|
124
|
+
|
|
125
|
+
// 2b. Copy attributes over.
|
|
126
|
+
if (isAttrib) {
|
|
127
|
+
newEl.setAttribute('is', isAttrib);
|
|
128
|
+
// el.removeAttribute('_is');
|
|
129
|
+
}
|
|
130
|
+
for (let attrib of el.attributes)
|
|
131
|
+
if (attrib.name !== '_is')
|
|
132
|
+
newEl.setAttribute(attrib.name, attrib.value);
|
|
133
|
+
|
|
134
|
+
// Set dynamic attributes if they are primitive types.
|
|
135
|
+
for (let name in attribs) {
|
|
136
|
+
let val = attribs[name];
|
|
137
|
+
let valType = typeof val;
|
|
138
|
+
// Only true and false can reach here, so the undefined/null halves of the
|
|
139
|
+
// falsy test this used to spell out could never have decided anything.
|
|
140
|
+
if (valType === 'boolean') {
|
|
141
|
+
if (val)
|
|
142
|
+
newEl.setAttribute(name, '');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
146
|
+
else if (valType==='string' || valType === 'number' || valType==='bigint')
|
|
147
|
+
newEl.setAttribute(name, val);
|
|
148
|
+
}
|
|
77
149
|
|
|
78
|
-
// 2a. Instantiate component
|
|
79
|
-
let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
|
|
80
|
-
let Constructor = customElements.get(tagName);
|
|
81
|
-
if (!Constructor)
|
|
82
|
-
throw new Error(`Must call customElements.define('${tagName}', Class) before using it.`);
|
|
83
150
|
|
|
84
|
-
|
|
85
|
-
|
|
151
|
+
// 2c. If an id pointed at the placeholder, update it to point to the new element.
|
|
152
|
+
let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
|
|
153
|
+
if (id)
|
|
154
|
+
delve(this.parentNg.getRootEl(), id.split(/\./g), newEl);
|
|
86
155
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
156
|
+
// 2d. Update paths to use replaced element.
|
|
157
|
+
let ng = this.parentNg;
|
|
158
|
+
this.nodeMarker = newEl;
|
|
159
|
+
for (let path of ng.paths) {
|
|
160
|
+
if (path.nodeMarker === el)
|
|
161
|
+
path.nodeMarker = newEl;
|
|
162
|
+
if (path.nodeBefore === el)
|
|
163
|
+
path.nodeBefore = newEl;
|
|
164
|
+
}
|
|
165
|
+
if (ng.startNode === el)
|
|
166
|
+
ng.startNode = newEl;
|
|
167
|
+
if (ng.endNode === el)
|
|
168
|
+
ng.endNode = newEl;
|
|
169
|
+
|
|
170
|
+
// 2f. Call render() if it wasn't called by the constructor.
|
|
171
|
+
// This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
|
|
172
|
+
// Because that path renders it without the attribute expressions.
|
|
173
|
+
if (typeof newEl.render === 'function' && !Globals.rendered.has(newEl))
|
|
174
|
+
newEl.render(attribs, true);
|
|
175
|
+
|
|
176
|
+
// 2g. Update attribute paths to use the new element and re-apply them.
|
|
177
|
+
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
178
|
+
attribPath.parentNg = this.parentNg;
|
|
179
|
+
attribPath.nodeMarker = newEl;
|
|
180
|
+
attribPath.applyAll(exprs[i]);
|
|
103
181
|
}
|
|
104
182
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
newEl.setAttribute(name, val);
|
|
183
|
+
// 2e. Swap it to the DOM.
|
|
184
|
+
el.replaceWith(newEl);
|
|
108
185
|
}
|
|
109
186
|
|
|
187
|
+
// 2f. Render
|
|
188
|
+
else if (typeof el.render === 'function')
|
|
189
|
+
el.render(attribs, changed);
|
|
110
190
|
|
|
111
|
-
// 2c. If an id pointed at the placeholder, update it to point to the new element.
|
|
112
|
-
let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
|
|
113
|
-
if (id)
|
|
114
|
-
delve(this.parentNg.getRootNode(), id.split(/\./g), newEl);
|
|
115
|
-
|
|
116
|
-
// 2d. Update paths to use replaced element.
|
|
117
|
-
let ng = this.parentNg;
|
|
118
|
-
this.nodeMarker = newEl;
|
|
119
|
-
for (let path of ng.paths) {
|
|
120
|
-
if (path.nodeMarker === el)
|
|
121
|
-
path.nodeMarker = newEl;
|
|
122
|
-
if (path.nodeBefore === el)
|
|
123
|
-
path.nodeBefore = newEl;
|
|
124
|
-
}
|
|
125
|
-
if (ng.startNode === el)
|
|
126
|
-
ng.startNode = newEl;
|
|
127
|
-
if (ng.endNode === el)
|
|
128
|
-
ng.endNode = newEl;
|
|
129
|
-
|
|
130
|
-
// 2f. Call render() if it wasn't called by the constructor.
|
|
131
|
-
// This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
|
|
132
|
-
// Because that path renders it without the attribute expressions.
|
|
133
|
-
if (typeof newEl.render === 'function' && !Globals.rendered.has(newEl))
|
|
134
|
-
newEl.render(attribs, true);
|
|
135
|
-
|
|
136
|
-
// 2g. Update attribute paths to use the new element and re-apply them.
|
|
137
|
-
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
138
|
-
attribPath.parentNg = this.parentNg;
|
|
139
|
-
attribPath.nodeMarker = newEl;
|
|
140
|
-
attribPath.apply(exprs[i]);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 2e. Swap it to the DOM.
|
|
144
|
-
el.replaceWith(newEl);
|
|
145
191
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
el.render(attribs, changed);
|
|
150
|
-
|
|
151
|
-
Globals.currentSlotChildren = null;
|
|
192
|
+
finally {
|
|
193
|
+
Globals.currentSlotChildren = prevSlotChildren;
|
|
194
|
+
}
|
|
152
195
|
}
|
|
153
196
|
|
|
154
197
|
/**
|
|
@@ -156,21 +199,16 @@ export default class PathToComponent extends Path {
|
|
|
156
199
|
* @param pathOffset {int}
|
|
157
200
|
* @return {Path} */
|
|
158
201
|
clone(newRoot, pathOffset=0) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
let result =
|
|
202
|
+
// A component path's nodeBefore is always null (the constructor discards it), so the
|
|
203
|
+
// base clone() resolves only the nodeMarker and hands back a new PathToComponent.
|
|
204
|
+
let result = super.clone(newRoot, pathOffset);
|
|
162
205
|
result.attribPaths = this.attribPaths.map(path => path.clone(newRoot, pathOffset));
|
|
163
|
-
|
|
164
|
-
//#IFDEV
|
|
165
|
-
result.verify();
|
|
166
|
-
//#ENDIF
|
|
167
|
-
|
|
168
206
|
return result;
|
|
169
207
|
}
|
|
170
208
|
|
|
171
209
|
getExpressionCount() { return 0 }
|
|
172
210
|
|
|
173
|
-
//#
|
|
211
|
+
//#IFDEBUG
|
|
174
212
|
verify() {
|
|
175
213
|
super.verify();
|
|
176
214
|
assert(this.nodeMarker.nodeType === Node.ELEMENT_NODE);
|
package/src/PathToEvent.js
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import assert from "./assert.js";
|
|
2
|
-
import PathToAttribValue from "./PathToAttribValue.js";
|
|
2
|
+
import PathToAttribValue, {delegatedKeyFor} from "./PathToAttribValue.js";
|
|
3
|
+
import {nativeEventPrefix} from "./Util.js";
|
|
3
4
|
|
|
4
5
|
// TODO: Merge this into PathToAttribValue?
|
|
5
6
|
export default class PathToEvent extends PathToAttribValue {
|
|
6
7
|
|
|
7
|
-
/** @type {string} The
|
|
8
|
+
/** @type {string} The attribName without the "on" prefix. */
|
|
8
9
|
eventName;
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** @type {symbol|undefined} Expando key nodes store this event's delegated handler under.
|
|
12
|
+
* Undefined for non-delegatable (non-bubbling) events; bindEvent() then binds directly. */
|
|
13
|
+
delegatedKey;
|
|
14
|
+
|
|
15
|
+
/** @type {boolean} True for `native:onclick`: the handler is registered with addEventListener
|
|
16
|
+
* when the template renders, so it runs at its element's own turn in the browser's dispatch
|
|
17
|
+
* order instead of being delegated to the component root. */
|
|
18
|
+
native;
|
|
19
|
+
|
|
20
|
+
constructor(nodeBefore, nodeMarker, attribName=null, attrValue=null) {
|
|
21
|
+
super(null, nodeMarker, attribName, attrValue);
|
|
22
|
+
this.skipIfSame = true;
|
|
23
|
+
let name = attribName;
|
|
24
|
+
this.native = name !== null && name.startsWith(nativeEventPrefix);
|
|
25
|
+
if (this.native)
|
|
26
|
+
name = name.slice(nativeEventPrefix.length);
|
|
27
|
+
this.eventName = name ? name.slice(2) : null;
|
|
28
|
+
|
|
29
|
+
// A native binding leaves delegatedKey undefined. That is the single switch both
|
|
30
|
+
// bindEvent() and the compiled stamp program test to choose the direct
|
|
31
|
+
// addEventListener path, so nothing else has to know about the prefix.
|
|
32
|
+
this.delegatedKey = (this.eventName !== null && !this.native) ? delegatedKeyFor(this.eventName) : undefined;
|
|
13
33
|
}
|
|
14
34
|
|
|
15
35
|
/**
|
|
@@ -19,8 +39,8 @@ export default class PathToEvent extends PathToAttribValue {
|
|
|
19
39
|
* onclick=${[this, 'doSomething', 'meow']}
|
|
20
40
|
*
|
|
21
41
|
* @param exprs {Expr[]} Only the first is used.*/
|
|
22
|
-
|
|
23
|
-
//#
|
|
42
|
+
applyAll(exprs) {
|
|
43
|
+
//#IFDEBUG
|
|
24
44
|
assert(Array.isArray(exprs));
|
|
25
45
|
//#ENDIF
|
|
26
46
|
|
|
@@ -28,7 +48,7 @@ export default class PathToEvent extends PathToAttribValue {
|
|
|
28
48
|
// We have expressions within a string attribute value that's not a Solarite event. E.g.
|
|
29
49
|
// <div onclick="alert(${1});"
|
|
30
50
|
if (this.attrValue?.length > 1) {
|
|
31
|
-
super.
|
|
51
|
+
super.applyAll(exprs);
|
|
32
52
|
return;
|
|
33
53
|
}
|
|
34
54
|
|
|
@@ -40,16 +60,16 @@ export default class PathToEvent extends PathToAttribValue {
|
|
|
40
60
|
applySingle(expr) {
|
|
41
61
|
// Expressions within a string attribute value that's not a Solarite event.
|
|
42
62
|
if (this.attrValue?.length > 1)
|
|
43
|
-
return super.
|
|
63
|
+
return super.applyAll([expr]);
|
|
44
64
|
|
|
45
65
|
// Don't bind events to component placeholders.
|
|
46
66
|
// PathToComponent will do the binding later when it instantiates the component.
|
|
47
67
|
if (this.isComponentAttrib && this.nodeMarker.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
|
|
48
68
|
return;
|
|
49
69
|
|
|
50
|
-
let root = this.parentNg.rootNg.
|
|
70
|
+
let root = this.parentNg.rootNg.rootEl
|
|
51
71
|
|
|
52
|
-
/*#
|
|
72
|
+
/*#IFDEBUG*/
|
|
53
73
|
assert(root?.nodeType === 1);
|
|
54
74
|
/*#ENDIF*/
|
|
55
75
|
|
|
@@ -67,7 +87,7 @@ export default class PathToEvent extends PathToAttribValue {
|
|
|
67
87
|
expr = null;
|
|
68
88
|
}
|
|
69
89
|
else
|
|
70
|
-
throw new Error(`
|
|
90
|
+
throw new Error(`Solarite: ${this.attribName}=\${...} is not a function.`);
|
|
71
91
|
|
|
72
92
|
this.bindEvent(node, root, eventName, eventName, func, expr);
|
|
73
93
|
}
|
package/src/PathToKey.js
CHANGED
|
@@ -7,12 +7,6 @@ import Path from "./Path.js";
|
|
|
7
7
|
* matches NodeGroups to new templates by this key. */
|
|
8
8
|
export default class PathToKey extends Path {
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* @param exprs {Expr[]} Only the first is used. */
|
|
12
|
-
apply(exprs) {
|
|
13
|
-
this.parentNg.key = exprs[0];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
10
|
applySingle(expr) {
|
|
17
11
|
this.parentNg.key = expr;
|
|
18
12
|
}
|