saasufy-components 7.14.0 → 8.0.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/README.md +10 -0
- package/app-router.js +2 -1
- package/collection-reducer.js +4 -2
- package/collection-viewer.js +38 -14
- package/model-viewer.js +4 -2
- package/package.json +1 -1
- package/socket.js +26 -0
package/README.md
CHANGED
|
@@ -65,6 +65,16 @@ You should always include the `socket.js` script and create a top-level `socket-
|
|
|
65
65
|
Linking the scripts directly from `saasufy.com` is the simplest way to get started as it doesn't require loading anything or hosting the scripts yourself.
|
|
66
66
|
An alternative approach is to download Saasufy components using `npm install saasufy-components` and self-host them.
|
|
67
67
|
|
|
68
|
+
### Templating
|
|
69
|
+
|
|
70
|
+
Many components allow you to substitute dynamic values or JavaScript expressions inside them using double brackets such as `{{someValue}}`. Using double brackets is the safest way to inject values into your components as it escapes all HTML characters to mitigate XSS attacks.
|
|
71
|
+
|
|
72
|
+
In order to render HTML which was generated dynamically, you need to use triple brackets such as `{{{someValue}}}` but you should not inject untrusted user-generated values directly in this way.
|
|
73
|
+
|
|
74
|
+
Saasufy components support nested expressions but you should not inject untrusted/unsanitized input via a nested expression as it can make your application vulerable to XSS attacks.
|
|
75
|
+
|
|
76
|
+
When an expression fails to render (I.e. throws an error), it will remain unchanged in its original expression format within the component/template. If an expression is nested inside a child component, the parent will attempt to evaluate it first. If it fails, then it will remain as-is and the child may later also attempt to evaluate it as part of its own rendering (e.g. once the child provides the missing state).
|
|
77
|
+
|
|
68
78
|
### Styling
|
|
69
79
|
|
|
70
80
|
This project does not impose any specific approach to styling components, however, it comes with an optional `styles.css` stylesheet which can be added inside your `<head>` tag like this:
|
package/app-router.js
CHANGED
|
@@ -272,9 +272,10 @@ export class AppRouter extends SocketConsumer {
|
|
|
272
272
|
window.scrollTo(0, 0);
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
this.setCurrentState(routeArgs);
|
|
275
276
|
routerViewport.innerHTML = renderTemplate(
|
|
276
277
|
pageTemplate.innerHTML,
|
|
277
|
-
|
|
278
|
+
this.getStateContext(),
|
|
278
279
|
this.socket
|
|
279
280
|
);
|
|
280
281
|
}
|
package/collection-reducer.js
CHANGED
|
@@ -147,9 +147,10 @@ class CollectionReducer extends SocketConsumer {
|
|
|
147
147
|
let errorTemplate = this.shadowRoot.querySelector('slot[name="error"]').assignedNodes()[0];
|
|
148
148
|
if (errorTemplate) {
|
|
149
149
|
let type = this.getAttribute('type-alias') || this.getAttribute('collection-type');
|
|
150
|
+
this.setCurrentState({ [`$${type}`]: { error } });
|
|
150
151
|
let errorItemString = renderTemplate(
|
|
151
152
|
errorTemplate.innerHTML,
|
|
152
|
-
|
|
153
|
+
this.getStateContext(),
|
|
153
154
|
this.socket
|
|
154
155
|
);
|
|
155
156
|
viewportNode.innerHTML = errorItemString;
|
|
@@ -187,9 +188,10 @@ class CollectionReducer extends SocketConsumer {
|
|
|
187
188
|
viewportNode.innerHTML = noItemTemplate.innerHTML;
|
|
188
189
|
} else if (itemTemplate) {
|
|
189
190
|
let type = this.getAttribute('type-alias') || this.collection.type;
|
|
191
|
+
this.setCurrentState({ [type]: this.collection.value, [`$${type}`]: this.collection.meta });
|
|
190
192
|
let itemString = renderTemplate(
|
|
191
193
|
itemTemplate.innerHTML,
|
|
192
|
-
|
|
194
|
+
this.getStateContext(),
|
|
193
195
|
this.socket
|
|
194
196
|
);
|
|
195
197
|
viewportNode.innerHTML = itemString;
|
package/collection-viewer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SocketConsumer } from './socket.js';
|
|
1
|
+
import { SocketConsumer, setElementState } from './socket.js';
|
|
2
2
|
import AGCollection from '/node_modules/ag-collection/ag-collection.js';
|
|
3
3
|
import { renderTemplate, convertStringToFieldParams } from './utils.js';
|
|
4
4
|
|
|
@@ -251,15 +251,30 @@ class CollectionViewer extends SocketConsumer {
|
|
|
251
251
|
let errorTemplate = this.shadowRoot.querySelector('slot[name="error"]').assignedNodes()[0];
|
|
252
252
|
if (errorTemplate) {
|
|
253
253
|
let type = this.getAttribute('type-alias') || this.getAttribute('collection-type');
|
|
254
|
+
this.setCurrentState({ [`$${type}`]: { error } });
|
|
254
255
|
let errorItemString = renderTemplate(
|
|
255
256
|
errorTemplate.innerHTML,
|
|
256
|
-
|
|
257
|
+
this.getStateContext(),
|
|
257
258
|
this.socket
|
|
258
259
|
);
|
|
259
260
|
viewportNode.innerHTML = errorItemString;
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
getNodesWithState(htmlString, state) {
|
|
265
|
+
let items = [];
|
|
266
|
+
let template = document.createElement('template');
|
|
267
|
+
template.innerHTML = htmlString;
|
|
268
|
+
let content = template.content;
|
|
269
|
+
for (let child of content.children) {
|
|
270
|
+
setElementState(child, state);
|
|
271
|
+
}
|
|
272
|
+
for (let node of content.childNodes) {
|
|
273
|
+
items.push(node);
|
|
274
|
+
}
|
|
275
|
+
return items;
|
|
276
|
+
}
|
|
277
|
+
|
|
263
278
|
renderList() {
|
|
264
279
|
let viewportSlot = this.shadowRoot.querySelector('slot[name="viewport"]');
|
|
265
280
|
if (!viewportSlot) return;
|
|
@@ -289,47 +304,56 @@ class CollectionViewer extends SocketConsumer {
|
|
|
289
304
|
let firstItemTemplate = this.shadowRoot.querySelector('slot[name="first-item"]').assignedNodes()[0];
|
|
290
305
|
let lastItemTemplate = this.shadowRoot.querySelector('slot[name="last-item"]').assignedNodes()[0];
|
|
291
306
|
|
|
307
|
+
viewportNode.innerHTML = '';
|
|
308
|
+
this.setCurrentState(null);
|
|
309
|
+
let stateContext = this.getStateContext();
|
|
292
310
|
let items = [];
|
|
293
311
|
|
|
294
312
|
let type = this.getAttribute('type-alias') || this.collection.type;
|
|
295
313
|
|
|
296
314
|
if (firstItemTemplate) {
|
|
315
|
+
let state = { [`$${type}`]: this.collection.meta };
|
|
297
316
|
let itemString = renderTemplate(
|
|
298
317
|
firstItemTemplate.innerHTML,
|
|
299
|
-
{
|
|
318
|
+
{...stateContext, ...state},
|
|
300
319
|
this.socket
|
|
301
320
|
);
|
|
302
|
-
items.push(itemString);
|
|
321
|
+
items.push(...this.getNodesWithState(itemString, state));
|
|
303
322
|
}
|
|
304
323
|
if (noItemTemplate && !this.collection.value.length) {
|
|
324
|
+
let state = { [`$${type}`]: this.collection.meta };
|
|
305
325
|
let itemString = renderTemplate(
|
|
306
326
|
noItemTemplate.innerHTML,
|
|
307
|
-
{
|
|
327
|
+
{...stateContext, ...state},
|
|
308
328
|
this.socket
|
|
309
329
|
);
|
|
310
|
-
items.push(itemString);
|
|
330
|
+
items.push(...this.getNodesWithState(itemString, state));
|
|
311
331
|
} else if (itemTemplate) {
|
|
312
332
|
for (let modelItem of this.collection.value) {
|
|
333
|
+
let state = {
|
|
334
|
+
[type]: modelItem,
|
|
335
|
+
[`$${type}`]: this.collection.meta
|
|
336
|
+
};
|
|
313
337
|
let itemString = renderTemplate(
|
|
314
338
|
itemTemplate.innerHTML,
|
|
315
|
-
{
|
|
316
|
-
[type]: modelItem,
|
|
317
|
-
[`$${type}`]: this.collection.meta
|
|
318
|
-
},
|
|
339
|
+
{...stateContext, ...state},
|
|
319
340
|
this.socket
|
|
320
341
|
);
|
|
321
|
-
items.push(itemString);
|
|
342
|
+
items.push(...this.getNodesWithState(itemString, state));
|
|
322
343
|
}
|
|
323
344
|
}
|
|
324
345
|
if (lastItemTemplate) {
|
|
346
|
+
let state = { [`$${type}`]: this.collection.meta };
|
|
325
347
|
let itemString = renderTemplate(
|
|
326
348
|
lastItemTemplate.innerHTML,
|
|
327
|
-
{
|
|
349
|
+
{...stateContext, ...state},
|
|
328
350
|
this.socket
|
|
329
351
|
);
|
|
330
|
-
items.push(itemString);
|
|
352
|
+
items.push(...this.getNodesWithState(itemString, state));
|
|
353
|
+
}
|
|
354
|
+
for (let item of items) {
|
|
355
|
+
viewportNode.appendChild(item);
|
|
331
356
|
}
|
|
332
|
-
viewportNode.innerHTML = items.join('');
|
|
333
357
|
}
|
|
334
358
|
|
|
335
359
|
render() {
|
package/model-viewer.js
CHANGED
|
@@ -63,9 +63,10 @@ class ModelViewer extends SocketConsumer {
|
|
|
63
63
|
let errorTemplate = this.shadowRoot.querySelector('slot[name="error"]').assignedNodes()[0];
|
|
64
64
|
if (errorTemplate) {
|
|
65
65
|
let type = this.getAttribute('type-alias') || this.getAttribute('model-type');
|
|
66
|
+
this.setCurrentState({ [`$${type}`]: { error } });
|
|
66
67
|
let errorItemString = renderTemplate(
|
|
67
68
|
errorTemplate.innerHTML,
|
|
68
|
-
|
|
69
|
+
this.getStateContext(),
|
|
69
70
|
this.socket
|
|
70
71
|
);
|
|
71
72
|
viewportNode.innerHTML = errorItemString;
|
|
@@ -100,9 +101,10 @@ class ModelViewer extends SocketConsumer {
|
|
|
100
101
|
viewportNode.innerHTML = noItemTemplate.innerHTML;
|
|
101
102
|
} else if (itemTemplate) {
|
|
102
103
|
let type = this.getAttribute('type-alias') || this.model.type;
|
|
104
|
+
this.setCurrentState({ [type]: modelValue });
|
|
103
105
|
let itemString = renderTemplate(
|
|
104
106
|
itemTemplate.innerHTML,
|
|
105
|
-
|
|
107
|
+
this.getStateContext(),
|
|
106
108
|
this.socket
|
|
107
109
|
);
|
|
108
110
|
viewportNode.innerHTML = itemString;
|
package/package.json
CHANGED
package/socket.js
CHANGED
|
@@ -151,7 +151,33 @@ export class SocketProvider extends HTMLElement {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
export function setElementState(element, currentState) {
|
|
155
|
+
element.saasufyState = currentState;
|
|
156
|
+
}
|
|
157
|
+
|
|
154
158
|
export class SocketConsumer extends HTMLElement {
|
|
159
|
+
setCurrentState(currentState) {
|
|
160
|
+
this.saasufyState = currentState;
|
|
161
|
+
}
|
|
162
|
+
getStateContext(currentState) {
|
|
163
|
+
let currentNode = this.parentNode;
|
|
164
|
+
let statefulParentNodes = [];
|
|
165
|
+
while (currentNode) {
|
|
166
|
+
if (currentNode.saasufyState) {
|
|
167
|
+
statefulParentNodes.push(currentNode);
|
|
168
|
+
}
|
|
169
|
+
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
170
|
+
}
|
|
171
|
+
statefulParentNodes.reverse();
|
|
172
|
+
let stateContext = {};
|
|
173
|
+
for (let parentNode of statefulParentNodes) {
|
|
174
|
+
Object.assign(stateContext, parentNode.saasufyState);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
...stateContext,
|
|
178
|
+
...(currentState || this.saasufyState)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
155
181
|
getSocket() {
|
|
156
182
|
let socket = null;
|
|
157
183
|
let currentNode = this.parentNode;
|