solarite 0.4.0 → 0.5.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.
@@ -0,0 +1,169 @@
1
+ import Path from "./Path.js";
2
+ import Util from "./Util.js";
3
+ import delve from "./delve.js";
4
+ import assert from "./assert.js";
5
+ import Globals from "./Globals.js";
6
+
7
+ export default class PathToComponent extends Path {
8
+
9
+ /** @type {PathToAttribValue[]} Paths to dynamics attributes that will be set on the component.*/
10
+ attribPaths;
11
+
12
+ constructor(nodeBefore, nodeMarker) {
13
+ super(null, nodeMarker);
14
+ }
15
+
16
+ /**
17
+ * Call render() on the component pointed to by this Path.
18
+ * And instantiate it (from a -solarite-placeholder element) if it hasn't been done yet.
19
+ * @param exprs {Expr[][]} Expressions to evaluate for each attribute to pass to the constructor.
20
+ * This is different than other Path.apply() functions which only receive Expr[] and not Expr[][].
21
+ * Because here we're receiving an array of arrays of expressions, one for each dynamic attribute.
22
+ * @param freeNodeGroups {boolean} Used only by watch.js.
23
+ * @param changed {boolean} True if the exprs have changed since the last time render() was called.*/
24
+ apply(exprs, freeNodeGroups=true, changed=true) {
25
+ //#IFDEV
26
+ assert(Array.isArray(exprs));
27
+ assert(!exprs.length || Array.isArray(exprs[0]));
28
+ //#ENDIF
29
+
30
+ //#IFDEV
31
+ assert(exprs.length === this.attribPaths.length);
32
+ //#ENDIF
33
+
34
+ let el = this.nodeMarker;
35
+
36
+ // 1. Attributes
37
+ let attribs = Util.attribsToObject(el, '_is');
38
+ for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
39
+ let name = Util.dashesToCamel(attribPath.attrName);
40
+ attribs[name] = attribPath.getValue(exprs[i]);
41
+ }
42
+
43
+ // 2. Instantiate component on first time.
44
+ let isAttrib = el.getAttribute('_is');
45
+ if (el.tagName.endsWith('-SOLARITE-PLACEHOLDER') || isAttrib) {
46
+
47
+
48
+ // 2a. Instantiate component
49
+ let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
50
+ let Constructor = customElements.get(tagName);
51
+ if (!Constructor)
52
+ throw new Error(`Must call customElements.define('${tagName}', Class) before using it.`);
53
+
54
+ Globals.currentSlotChildren = [...el.childNodes]; // TODO: Does this need to be a stack?
55
+ let newEl = new Constructor(attribs);
56
+
57
+ // 2b. Copy attributes over.
58
+ if (isAttrib) {
59
+ newEl.setAttribute('is', isAttrib);
60
+ // el.removeAttribute('_is');
61
+ }
62
+ for (let attrib of el.attributes)
63
+ if (attrib.name !== '_is')
64
+ newEl.setAttribute(attrib.name, attrib.value);
65
+
66
+ // Set dynamic attributes if they are primitive types.
67
+ for (let name in attribs) {
68
+ let val = attribs[name];
69
+ let valType = typeof val;
70
+ if (valType === 'boolean') {
71
+ if (val !== false && val !== undefined && val !== null) // Util.isFalsy() inlined
72
+ newEl.setAttribute(name, '');
73
+ }
74
+
75
+ // If type is a non-boolean primitive, set the attribute value.
76
+ else if (valType==='string' || valType === 'number' || valType==='bigint')
77
+ newEl.setAttribute(name, val);
78
+ }
79
+
80
+
81
+ // 2c. If an id pointed at the placeholder, update it to point to the new element.
82
+ let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
83
+ if (id)
84
+ delve(this.parentNg.getRootNode(), id.split(/\./g), newEl);
85
+
86
+ // 2d. Update paths to use replaced element.
87
+ let ng = this.parentNg;
88
+ this.nodeMarker = newEl;
89
+ for (let path of ng.paths) {
90
+ if (path.nodeMarker === el)
91
+ path.nodeMarker = newEl;
92
+ if (path.nodeBefore === el)
93
+ path.nodeBefore = newEl;
94
+ }
95
+ if (ng.startNode === el)
96
+ ng.startNode = newEl;
97
+ if (ng.endNode === el)
98
+ ng.endNode = newEl;
99
+
100
+ // 2f. Call render() if it wasn't called by the constructor.
101
+ // This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
102
+ // Because that path renders it without the attribute expressions.
103
+ if (typeof newEl.render === 'function' && !Globals.rendered.has(newEl))
104
+ newEl.render(attribs, changed);
105
+
106
+ // 2g. Update attribute paths to use the new element and re-apply them.
107
+ for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
108
+ attribPath.parentNg = this.parentNg;
109
+ attribPath.nodeMarker = newEl;
110
+ attribPath.apply(exprs[i]);
111
+ }
112
+
113
+ // 2e. Swap it to the DOM.
114
+ el.replaceWith(newEl);
115
+ }
116
+
117
+ // 2f. Render
118
+ else if (typeof el.render === 'function')
119
+ el.render(attribs, changed);
120
+
121
+ Globals.currentSlotChildren = null;
122
+ }
123
+
124
+ /**
125
+ * @param newRoot {HTMLElement}
126
+ * @param pathOffset {int}
127
+ * @return {Path} */
128
+ clone(newRoot, pathOffset=0) {
129
+ /*#IFDEV*/this.verify();/*#ENDIF*/
130
+ let nodeMarker = this.getNewNodeMarker(newRoot, pathOffset);
131
+ let result = new PathToComponent(null, nodeMarker);
132
+ result.attribPaths = this.attribPaths.map(path => path.clone(newRoot, pathOffset));
133
+
134
+ //#IFDEV
135
+ result.verify();
136
+ //#ENDIF
137
+
138
+ return result;
139
+ }
140
+
141
+ getExpressionCount() { return 0 }
142
+
143
+ //#IFDEV
144
+ verify() {
145
+ super.verify();
146
+ assert(this.nodeMarker.nodeType === Node.ELEMENT_NODE);
147
+ assert(this.nodeMarker.tagName.includes('-') || this.nodeMarker.hasAttribute('is') || this.nodeMarker.hasAttribute('_is'));
148
+ if (this.attribPaths)
149
+ for (let path of this.attribPaths)
150
+ path.verify();
151
+ }
152
+
153
+ //#ENDIF
154
+ }
155
+
156
+
157
+ // TODO: Conver this to an iterator, to make it faster?
158
+ // Especially since it's only used on the first render?
159
+ function getNodes(startNode, endNode) {
160
+ let result = [];
161
+ let current = startNode
162
+ let afterLast = endNode?.nextSibling
163
+ while (current && current !== afterLast) {
164
+ result.push(current)
165
+ current = current.nextSibling
166
+ }
167
+
168
+ return result;
169
+ }
@@ -0,0 +1,57 @@
1
+ import assert from "./assert.js";
2
+ import PathToAttribValue from "./PathToAttribValue.js";
3
+
4
+ // TODO: Merge this into PathToAttribValue?
5
+ export default class PathToEvent extends PathToAttribValue {
6
+
7
+ constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
8
+ super(null, nodeMarker, attrName, attrValue);
9
+ }
10
+
11
+ /**
12
+ * Handle attributes for event binding, such as:
13
+ * onclick=${(e, el) => this.doSomething(el, 'meow')}
14
+ * oninput=${[this.doSomething, 'meow']}
15
+ * onclick=${[this, 'doSomething', 'meow']}
16
+ *
17
+ * @param exprs {Expr[]} Only the first is used.*/
18
+ apply(exprs) {
19
+ //#IFDEV
20
+ assert(Array.isArray(exprs));
21
+ //#ENDIF
22
+
23
+ // Don't bind events to component placeholders.
24
+ // PathToComponent will do the binding later when it instantiates the component.
25
+ if (this.isComponentAttrib && this.nodeMarker.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
26
+ return;
27
+
28
+ let expr = exprs[0];
29
+ let root = this.parentNg.rootNg.root
30
+
31
+ /*#IFDEV*/
32
+ assert(root?.nodeType === 1);
33
+ /*#ENDIF*/
34
+
35
+ let node = this.nodeMarker;
36
+
37
+ let eventName = this.attrName.slice(2); // remove "on-" prefix.
38
+ let func;
39
+ let args = [];
40
+
41
+ // Convert array to function.
42
+ // oninput=${[this.doSomething, 'meow']}
43
+ if (Array.isArray(expr) && typeof expr[0] === 'function') {
44
+ func = expr[0];
45
+ args = expr.slice(1);
46
+ }
47
+ else if (typeof expr === 'function')
48
+ func = expr;
49
+ else
50
+ throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
51
+
52
+ this.bindEvent(node, root, eventName, eventName, func, args);
53
+ }
54
+
55
+
56
+
57
+ }