solarite 0.7.0 → 0.8.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 +5193 -4114
- package/dist/Solarite.js +4951 -3878
- package/dist/Solarite.min.js +2 -2
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-runtime.js +86 -0
- package/package.json +11 -10
- package/readme.md +8 -8
- 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 +182 -101
- package/src/PathToAttribs.js +10 -18
- package/src/PathToComponent.js +43 -19
- package/src/PathToEvent.js +18 -12
- package/src/PathToKey.js +0 -6
- package/src/PathToNodes.js +695 -155
- package/src/RootNodeGroup.js +23 -30
- package/src/Selector.js +207 -0
- package/src/Shell.js +276 -117
- package/src/Solarite.d.ts +89 -13
- package/src/Solarite.js +38 -25
- package/src/Template.js +20 -7
- package/src/Util.js +75 -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/h.js
CHANGED
|
@@ -3,6 +3,8 @@ import Globals from "./Globals.js";
|
|
|
3
3
|
import toEl from "./toEl.js";
|
|
4
4
|
import Util from "./Util.js";
|
|
5
5
|
import {jsxToTemplate, Fragment} from "./jsx.js";
|
|
6
|
+
import MappedList from "./MappedList.js";
|
|
7
|
+
import Selector from "./Selector.js";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Convert strings to HTMLNodes.
|
|
@@ -56,7 +58,11 @@ const renderTemplateKey = Symbol('solariteRender');
|
|
|
56
58
|
// Using `arguments` alongside rest params would force the engine to materialize both per call.
|
|
57
59
|
const noArg = Symbol();
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
// The /** @type {*} */ cast on the default keeps TypeScript from inferring the parameter as
|
|
62
|
+
// `symbol` from noArg: TS can't parse the closure-style @param type above (function() without
|
|
63
|
+
// a return type under noImplicitAny), falls back to the default's type, and then flags every
|
|
64
|
+
// h(this) / h`` call in the codebase as an error. JetBrains reads the @param fine either way.
|
|
65
|
+
export default function h(htmlStrings=/** @type {*} */(noArg), ...exprs) {
|
|
60
66
|
|
|
61
67
|
// 1. Tagged template: h`<div>...</div>`
|
|
62
68
|
if (Array.isArray(htmlStrings)) {
|
|
@@ -109,11 +115,14 @@ export default function h(htmlStrings=noArg, ...exprs) {
|
|
|
109
115
|
let parent = htmlStrings, options = exprs[0];
|
|
110
116
|
|
|
111
117
|
// The closure is cached on the element so repeated renders don't recreate it.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
// Options are cached with it: they only take effect when the element's
|
|
119
|
+
// RootNodeGroup is first created, so a later render passing different ones is
|
|
120
|
+
// ignored either way, and caching regardless of them saves an allocation on every
|
|
121
|
+
// render of a component that passes an options object — which is how render() is
|
|
122
|
+
// usually written.
|
|
123
|
+
let cached = parent[renderTemplateKey];
|
|
124
|
+
if (cached)
|
|
125
|
+
return cached;
|
|
117
126
|
|
|
118
127
|
// Return a tagged template function that applies the tagged template to parent.
|
|
119
128
|
let renderTemplate = (htmlStrings, ...exprs) => {
|
|
@@ -125,8 +134,7 @@ export default function h(htmlStrings=noArg, ...exprs) {
|
|
|
125
134
|
let template = new Template(htmlStrings, exprs);
|
|
126
135
|
return template.render(parent, options);
|
|
127
136
|
}
|
|
128
|
-
|
|
129
|
-
parent[renderTemplateKey] = renderTemplate;
|
|
137
|
+
parent[renderTemplateKey] = renderTemplate;
|
|
130
138
|
return renderTemplate;
|
|
131
139
|
}
|
|
132
140
|
}
|
|
@@ -143,11 +151,11 @@ export default function h(htmlStrings=noArg, ...exprs) {
|
|
|
143
151
|
// Intercepts the main h(this)`...` function call inside render().
|
|
144
152
|
// TODO: This path doesn't handle embeds like data-id="..."
|
|
145
153
|
else if (typeof htmlStrings === 'object' && Globals.objToEl.has(htmlStrings)) {
|
|
154
|
+
// The only thing that ever puts an object into objToEl is toEl(), and it rejects anything
|
|
155
|
+
// that isn't a plain object before it does so, so an object that reaches here has already
|
|
156
|
+
// been checked and re-checking it can never report anything.
|
|
146
157
|
let obj = htmlStrings;
|
|
147
158
|
|
|
148
|
-
if (obj.constructor.name !== 'Object')
|
|
149
|
-
throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
150
|
-
|
|
151
159
|
// Jsx with h(this, <jsx>)
|
|
152
160
|
if (exprs[0] instanceof Template) {
|
|
153
161
|
let template = exprs[0];
|
|
@@ -171,14 +179,6 @@ export default function h(htmlStrings=noArg, ...exprs) {
|
|
|
171
179
|
throw new Error('h() does not support argument of type: ' + (htmlStrings ? typeof htmlStrings : htmlStrings))
|
|
172
180
|
}
|
|
173
181
|
|
|
174
|
-
// h.map caches each item's Template keyed by the item's identity, so a re-render returns
|
|
175
|
-
// the SAME Template instance for any item whose reference is unchanged. The reconciler's
|
|
176
|
-
// `ng.template === item` fast path (PathToNodes.applyKeyed/applyDiff) then skips rebuilding
|
|
177
|
-
// and comparing that row. A WeakMap is used instead of a symbol property so the idiomatic
|
|
178
|
-
// immutable update `{...item, x}` yields a fresh object that ISN'T in the cache and re-renders;
|
|
179
|
-
// a symbol property would be copied by spread and silently reuse the stale Template.
|
|
180
|
-
const mapCache = new WeakMap();
|
|
181
|
-
|
|
182
182
|
/**
|
|
183
183
|
* Render a list, reusing each item's DOM for as long as the item is the SAME object.
|
|
184
184
|
*
|
|
@@ -195,26 +195,53 @@ const mapCache = new WeakMap();
|
|
|
195
195
|
*
|
|
196
196
|
* ${h.map(this.rows, row => h`<tr key=${row.id}>${row.label}</tr>`)}
|
|
197
197
|
*
|
|
198
|
+
* What comes back is a MappedList, not an array: it carries the items and the callback so
|
|
199
|
+
* the reconciler can match a row to its item by identity and call the callback only for the
|
|
200
|
+
* rows it can't match. Put it straight into a template expression, as above; nested inside
|
|
201
|
+
* an array, or returned from a function, it expands to Templates just the same.
|
|
202
|
+
*
|
|
198
203
|
* @param items {Array} The list to render.
|
|
199
204
|
* @param fn {function(item:*):Template} Builds an item's Template; called only for new items.
|
|
200
|
-
* @return {
|
|
201
|
-
h.map = (items, fn) =>
|
|
202
|
-
let result = new Array(items.length);
|
|
203
|
-
for (let i=0; i<items.length; i++) {
|
|
204
|
-
let item = items[i];
|
|
205
|
-
if (item !== null && typeof item === 'object') {
|
|
206
|
-
let template = mapCache.get(item);
|
|
207
|
-
if (template === undefined) {
|
|
208
|
-
template = fn(item);
|
|
209
|
-
mapCache.set(item, template);
|
|
210
|
-
}
|
|
211
|
-
result[i] = template;
|
|
212
|
-
}
|
|
213
|
-
else
|
|
214
|
-
result[i] = fn(item);
|
|
215
|
-
}
|
|
216
|
-
return result;
|
|
217
|
-
}
|
|
205
|
+
* @return {MappedList} */
|
|
206
|
+
h.map = (items, fn) => new MappedList(items, fn);
|
|
218
207
|
|
|
219
208
|
h.immutableMap = h.map;
|
|
220
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Create a selection that updates only the rows it affects.
|
|
212
|
+
*
|
|
213
|
+
* A highlight that moves from one row of a thousand to another changes two attributes.
|
|
214
|
+
* Expressing it as ordinary state means calling render() and letting the reconciler walk the
|
|
215
|
+
* list to rediscover that. A selector writes those two attributes directly instead:
|
|
216
|
+
*
|
|
217
|
+
* class Table extends Solarite {
|
|
218
|
+
* selected = h.selector();
|
|
219
|
+
*
|
|
220
|
+
* pick(row) {
|
|
221
|
+
* this.selected.set(row.id); // no render() call
|
|
222
|
+
* }
|
|
223
|
+
*
|
|
224
|
+
* render() {
|
|
225
|
+
* h(this)`<tbody>${h.map(this.rows, row =>
|
|
226
|
+
* h`<tr key=${row.id} class=${this.selected.when(row.id, 'danger')}
|
|
227
|
+
* onclick=${[this.pick, row]}>${row.label}</tr>`)}</tbody>`;
|
|
228
|
+
* }
|
|
229
|
+
* }
|
|
230
|
+
*
|
|
231
|
+
* when() must be a whole attribute value, not part of one and not element content, since it
|
|
232
|
+
* owns that attribute for as long as the row exists. An off value of '' leaves no attribute
|
|
233
|
+
* behind at all. Selection state lives on the selector, so it survives re-renders, and
|
|
234
|
+
* set() is safe to call whether or not the rows are currently rendered.
|
|
235
|
+
*
|
|
236
|
+
* Two rules follow from how set() finds a row, and both throw a clear error rather than
|
|
237
|
+
* misbehaving quietly. **The rows must be keyed** — set() locates a row by looking its key
|
|
238
|
+
* up in the list, so the row template needs a key=${...}. And **the attribute must sit on
|
|
239
|
+
* the row's own root element**, the same one that carries the key, because that is the
|
|
240
|
+
* element set() writes. Drawing a row costs nothing either way: when() hands back one of
|
|
241
|
+
* two shared objects rather than allocating anything per row, so a selector is free to
|
|
242
|
+
* render over a list of any size and only a change of selection does any work.
|
|
243
|
+
*
|
|
244
|
+
* @param key {*} The initially selected key, or null for none.
|
|
245
|
+
* @return {Selector} */
|
|
246
|
+
h.selector = (key = null) => new Selector(key);
|
|
247
|
+
|
package/src/jsx.js
CHANGED
|
@@ -115,13 +115,10 @@ export function jsxToTemplate(tag, props, children=[], key=undefined) {
|
|
|
115
115
|
|
|
116
116
|
// 2a. Custom element class => emit <tag-name ...props>children</tag-name>; PathToComponent
|
|
117
117
|
// instantiates it exactly like a tagged-template component.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
tagName += '-element';
|
|
123
|
-
return buildIntrinsic(tagName, props, children, key);
|
|
124
|
-
}
|
|
118
|
+
// defineClass() hands back the name it registered, or the name the class was already
|
|
119
|
+
// registered under, so we never have to guess it a second time.
|
|
120
|
+
if (tag.prototype instanceof HTMLElement)
|
|
121
|
+
return buildIntrinsic(Util.defineClass(tag), props, children, key);
|
|
125
122
|
|
|
126
123
|
// 2b. Plain function component: call it with props (+ children) and expect a Template back.
|
|
127
124
|
let p = {};
|
package/src/toEl.js
CHANGED
|
@@ -41,7 +41,7 @@ export default function toEl(arg) {
|
|
|
41
41
|
let obj = arg;
|
|
42
42
|
|
|
43
43
|
if (obj.constructor.name !== 'Object')
|
|
44
|
-
throw new Error(`
|
|
44
|
+
throw new Error(`Solarite web component class ${obj.constructor?.name} must extend HTMLElement.`);
|
|
45
45
|
|
|
46
46
|
// Normal path
|
|
47
47
|
if (!Globals.objToEl.has(obj)) {
|
package/src/HtmlParser.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export default class HtmlParser {
|
|
3
|
-
constructor() {
|
|
4
|
-
this.defaultState = {
|
|
5
|
-
context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
|
|
6
|
-
quote: null, // possible values: null, '"', "'"
|
|
7
|
-
buffer: '',
|
|
8
|
-
lastChar: null
|
|
9
|
-
};
|
|
10
|
-
this.state = {...this.defaultState};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
reset() {
|
|
14
|
-
this.state = {...this.defaultState};
|
|
15
|
-
return this.state.context;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
|
|
20
|
-
* @param html {string}
|
|
21
|
-
* @param onContextChange {?function(html:string, index:int, prevContext:string, nextContext:string)}
|
|
22
|
-
* Called every time the context changes, and again at the last context.
|
|
23
|
-
* @return {('Attribute','Text','Tag')} The context at the end of html. */
|
|
24
|
-
parse(html, onContextChange=null) {
|
|
25
|
-
if (html === null)
|
|
26
|
-
return this.reset();
|
|
27
|
-
|
|
28
|
-
for (let i = 0; i < html.length; i++) {
|
|
29
|
-
const char = html[i];
|
|
30
|
-
switch (this.state.context) {
|
|
31
|
-
case HtmlParser.Text:
|
|
32
|
-
if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
|
|
33
|
-
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
34
|
-
this.state.context = HtmlParser.Tag;
|
|
35
|
-
this.state.buffer = '';
|
|
36
|
-
}
|
|
37
|
-
break;
|
|
38
|
-
case HtmlParser.Tag:
|
|
39
|
-
if (char === '>') {
|
|
40
|
-
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
41
|
-
this.state.context = HtmlParser.Text;
|
|
42
|
-
this.state.quote = null;
|
|
43
|
-
this.state.buffer = '';
|
|
44
|
-
}
|
|
45
|
-
else if (char === ' ' && !this.state.buffer) {
|
|
46
|
-
// No attribute name is present. Skipping the space.
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
else if (char === ' ' || char === '/' || char === '?') {
|
|
50
|
-
this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
|
|
51
|
-
}
|
|
52
|
-
else if (char === '"' || char === "'" || char === '=') {
|
|
53
|
-
onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
|
|
54
|
-
this.state.context = HtmlParser.Attribute;
|
|
55
|
-
this.state.quote = char === '=' ? null : char;
|
|
56
|
-
this.state.buffer = '';
|
|
57
|
-
}
|
|
58
|
-
else
|
|
59
|
-
this.state.buffer += char;
|
|
60
|
-
break;
|
|
61
|
-
case HtmlParser.Attribute:
|
|
62
|
-
// Start an attribute quote.
|
|
63
|
-
if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
|
|
64
|
-
this.state.quote = char;
|
|
65
|
-
}
|
|
66
|
-
else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
|
|
67
|
-
onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
|
|
68
|
-
this.state.context = HtmlParser.Tag;
|
|
69
|
-
this.state.quote = null;
|
|
70
|
-
this.state.buffer = '';
|
|
71
|
-
}
|
|
72
|
-
else if (!this.state.quote && char === '>') {
|
|
73
|
-
onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
|
|
74
|
-
this.state.context = HtmlParser.Text;
|
|
75
|
-
this.state.quote = null;
|
|
76
|
-
this.state.buffer = '';
|
|
77
|
-
}
|
|
78
|
-
else if (char !== ' ')
|
|
79
|
-
this.state.buffer += char;
|
|
80
|
-
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
onContextChange?.(html, html.length, this.state.context, null);
|
|
85
|
-
return this.state.context;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
HtmlParser.Attribute = 'Attribute';
|
|
90
|
-
HtmlParser.Text = 'Text';
|
|
91
|
-
HtmlParser.Tag = 'Tag';
|