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.
@@ -1,6 +1,5 @@
1
1
  import Path from "./Path.js";
2
2
  import Util from "./Util.js";
3
- import assert from "./assert.js";
4
3
  import PathToAttribValue from "./PathToAttribValue.js";
5
4
  import PathToEvent from "./PathToEvent.js";
6
5
  import {JsxAttr, styleToCss} from "./jsx.js";
@@ -11,24 +10,21 @@ export default class PathToAttribs extends Path {
11
10
  * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
12
11
  attrNames;
13
12
 
14
- /** @type {boolean} Provides one or more attributes on a component. */
15
- isComponent;
13
+ /** @type {PathToEvent|PathToAttribValue|undefined} Cached sub-path for the JSX
14
+ * whole-attribute fast path; see applyJsxAttr(). Declared so the first assignment
15
+ * doesn't transition the hidden class. */
16
+ jsxSub;
17
+
18
+ /** @type {?string} The attribute name jsxSub was built for. */
19
+ jsxSubName;
16
20
 
17
21
  constructor(nodeBefore, nodeMarker) {
18
- super(null, null);
19
- this.nodeMarker = nodeMarker;
22
+ // nodeBefore is discarded: an attribute path has no nodes of its own. The marker goes
23
+ // straight through the base constructor rather than being stored a second time after it.
24
+ super(null, nodeMarker);
20
25
  this.attrNames = new Set();
21
26
  }
22
27
 
23
- /**
24
- * @param exprs {Expr[][]} Only the first is used. */
25
- apply(exprs) {
26
- //#IFDEV
27
- assert(Array.isArray(exprs));
28
- //#ENDIF
29
- this.applySingle(exprs[0]);
30
- }
31
-
32
28
  /**
33
29
  * @param expr {Expr} */
34
30
  applySingle(expr) {
@@ -107,8 +103,4 @@ export default class PathToAttribs extends Path {
107
103
  value = styleToCss(value);
108
104
  sub.applySingle(value);
109
105
  }
110
-
111
-
112
- getExpressionCount() { return 1 }
113
- getValue(exprs) { return exprs[0]; }
114
106
  }
@@ -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.apply() functions which only receive Expr[] and not Expr[][].
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
- apply(exprs) {
29
- //#IFDEV
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
- //#IFDEV
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.attrName);
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]);
@@ -78,8 +86,27 @@ export default class PathToComponent extends Path {
78
86
  // 2a. Instantiate component
79
87
  let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
80
88
  let Constructor = customElements.get(tagName);
81
- if (!Constructor)
82
- throw new Error(`Must call customElements.define('${tagName}', Class) before using it.`);
89
+
90
+ // Not defined yet (e.g. the module is being lazily imported): keep the placeholder
91
+ // and instantiate when the definition lands, like a native custom-element upgrade.
92
+ // deferredExprs always holds the LATEST exprs so re-renders while undefined win.
93
+ if (!Constructor) {
94
+ this.deferredExprs = exprs;
95
+ if (!this.whenDefinedPending) {
96
+ this.whenDefinedPending = true;
97
+ console.warn(`Solarite: <${tagName}> is not defined yet; waiting for customElements.define().`);
98
+ customElements.whenDefined(tagName).then(() => {
99
+ this.whenDefinedPending = false;
100
+ let deferred = this.deferredExprs;
101
+ this.deferredExprs = null;
102
+ // Skip if a newer render already instantiated or replaced the placeholder.
103
+ if (deferred && this.nodeMarker === el && el.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
104
+ this.applyAll(deferred);
105
+ });
106
+ }
107
+ Globals.currentSlotChildren = null;
108
+ return;
109
+ }
83
110
 
84
111
  Globals.currentSlotChildren = [...el.childNodes]; // TODO: Does this need to be a stack?
85
112
  let newEl = new Constructor(attribs);
@@ -97,8 +124,10 @@ export default class PathToComponent extends Path {
97
124
  for (let name in attribs) {
98
125
  let val = attribs[name];
99
126
  let valType = typeof val;
127
+ // Only true and false can reach here, so the undefined/null halves of the
128
+ // falsy test this used to spell out could never have decided anything.
100
129
  if (valType === 'boolean') {
101
- if (val !== false && val !== undefined && val !== null) // Util.isFalsy() inlined
130
+ if (val)
102
131
  newEl.setAttribute(name, '');
103
132
  }
104
133
 
@@ -111,7 +140,7 @@ export default class PathToComponent extends Path {
111
140
  // 2c. If an id pointed at the placeholder, update it to point to the new element.
112
141
  let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
113
142
  if (id)
114
- delve(this.parentNg.getRootNode(), id.split(/\./g), newEl);
143
+ delve(this.parentNg.getRootEl(), id.split(/\./g), newEl);
115
144
 
116
145
  // 2d. Update paths to use replaced element.
117
146
  let ng = this.parentNg;
@@ -137,7 +166,7 @@ export default class PathToComponent extends Path {
137
166
  for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
138
167
  attribPath.parentNg = this.parentNg;
139
168
  attribPath.nodeMarker = newEl;
140
- attribPath.apply(exprs[i]);
169
+ attribPath.applyAll(exprs[i]);
141
170
  }
142
171
 
143
172
  // 2e. Swap it to the DOM.
@@ -156,21 +185,16 @@ export default class PathToComponent extends Path {
156
185
  * @param pathOffset {int}
157
186
  * @return {Path} */
158
187
  clone(newRoot, pathOffset=0) {
159
- /*#IFDEV*/this.verify();/*#ENDIF*/
160
- let nodeMarker = this.getNewNodeMarker(newRoot, pathOffset);
161
- let result = new PathToComponent(null, nodeMarker);
188
+ // A component path's nodeBefore is always null (the constructor discards it), so the
189
+ // base clone() resolves only the nodeMarker and hands back a new PathToComponent.
190
+ let result = super.clone(newRoot, pathOffset);
162
191
  result.attribPaths = this.attribPaths.map(path => path.clone(newRoot, pathOffset));
163
-
164
- //#IFDEV
165
- result.verify();
166
- //#ENDIF
167
-
168
192
  return result;
169
193
  }
170
194
 
171
195
  getExpressionCount() { return 0 }
172
196
 
173
- //#IFDEV
197
+ //#IFDEBUG
174
198
  verify() {
175
199
  super.verify();
176
200
  assert(this.nodeMarker.nodeType === Node.ELEMENT_NODE);
@@ -1,15 +1,21 @@
1
1
  import assert from "./assert.js";
2
- import PathToAttribValue from "./PathToAttribValue.js";
2
+ import PathToAttribValue, {delegatedKeyFor} from "./PathToAttribValue.js";
3
3
 
4
4
  // TODO: Merge this into PathToAttribValue?
5
5
  export default class PathToEvent extends PathToAttribValue {
6
6
 
7
- /** @type {string} The attrName without the "on" prefix. */
7
+ /** @type {string} The attribName without the "on" prefix. */
8
8
  eventName;
9
9
 
10
- constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
11
- super(null, nodeMarker, attrName, attrValue);
12
- this.eventName = attrName ? attrName.slice(2) : null;
10
+ /** @type {symbol|undefined} Expando key nodes store this event's delegated handler under.
11
+ * Undefined for non-delegatable (non-bubbling) events; bindEvent() then binds directly. */
12
+ delegatedKey;
13
+
14
+ constructor(nodeBefore, nodeMarker, attribName=null, attrValue=null) {
15
+ super(null, nodeMarker, attribName, attrValue);
16
+ this.skipIfSame = true;
17
+ this.eventName = attribName ? attribName.slice(2) : null;
18
+ this.delegatedKey = this.eventName !== null ? delegatedKeyFor(this.eventName) : undefined;
13
19
  }
14
20
 
15
21
  /**
@@ -19,8 +25,8 @@ export default class PathToEvent extends PathToAttribValue {
19
25
  * onclick=${[this, 'doSomething', 'meow']}
20
26
  *
21
27
  * @param exprs {Expr[]} Only the first is used.*/
22
- apply(exprs) {
23
- //#IFDEV
28
+ applyAll(exprs) {
29
+ //#IFDEBUG
24
30
  assert(Array.isArray(exprs));
25
31
  //#ENDIF
26
32
 
@@ -28,7 +34,7 @@ export default class PathToEvent extends PathToAttribValue {
28
34
  // We have expressions within a string attribute value that's not a Solarite event. E.g.
29
35
  // <div onclick="alert(${1});"
30
36
  if (this.attrValue?.length > 1) {
31
- super.apply(exprs);
37
+ super.applyAll(exprs);
32
38
  return;
33
39
  }
34
40
 
@@ -40,16 +46,16 @@ export default class PathToEvent extends PathToAttribValue {
40
46
  applySingle(expr) {
41
47
  // Expressions within a string attribute value that's not a Solarite event.
42
48
  if (this.attrValue?.length > 1)
43
- return super.apply([expr]);
49
+ return super.applyAll([expr]);
44
50
 
45
51
  // Don't bind events to component placeholders.
46
52
  // PathToComponent will do the binding later when it instantiates the component.
47
53
  if (this.isComponentAttrib && this.nodeMarker.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
48
54
  return;
49
55
 
50
- let root = this.parentNg.rootNg.root
56
+ let root = this.parentNg.rootNg.rootEl
51
57
 
52
- /*#IFDEV*/
58
+ /*#IFDEBUG*/
53
59
  assert(root?.nodeType === 1);
54
60
  /*#ENDIF*/
55
61
 
@@ -67,7 +73,7 @@ export default class PathToEvent extends PathToAttribValue {
67
73
  expr = null;
68
74
  }
69
75
  else
70
- throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
76
+ throw new Error(`Solarite: ${this.attribName}=\${...} is not a function.`);
71
77
 
72
78
  this.bindEvent(node, root, eventName, eventName, func, expr);
73
79
  }
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
  }