saasufy-components 7.13.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 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
@@ -149,7 +149,11 @@ export class AppRouter extends SocketConsumer {
149
149
  location.replace(cleanPagePath);
150
150
  }
151
151
 
152
- renderCurrentPage() {
152
+ refreshCurrentPage() {
153
+ this.renderCurrentPage(true);
154
+ }
155
+
156
+ renderCurrentPage(force) {
153
157
  let routerViewport = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
154
158
  if (!routerViewport) return;
155
159
 
@@ -260,7 +264,7 @@ export class AppRouter extends SocketConsumer {
260
264
  routeArgs
261
265
  });
262
266
 
263
- if (pageState === this.lastPageState) return;
267
+ if (pageState === this.lastPageState && !force) return;
264
268
 
265
269
  this.lastPageState = pageState;
266
270
 
@@ -268,9 +272,10 @@ export class AppRouter extends SocketConsumer {
268
272
  window.scrollTo(0, 0);
269
273
  }
270
274
 
275
+ this.setCurrentState(routeArgs);
271
276
  routerViewport.innerHTML = renderTemplate(
272
277
  pageTemplate.innerHTML,
273
- routeArgs,
278
+ this.getStateContext(),
274
279
  this.socket
275
280
  );
276
281
  }
@@ -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
- { [`$${type}`]: { error } },
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
- { [type]: this.collection.value, [`$${type}`]: this.collection.meta },
194
+ this.getStateContext(),
193
195
  this.socket
194
196
  );
195
197
  viewportNode.innerHTML = itemString;
@@ -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
- { [`$${type}`]: { error } },
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
- { [`$${type}`]: this.collection.meta },
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
- { [`$${type}`]: this.collection.meta },
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
- { [`$${type}`]: this.collection.meta },
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
- { [`$${type}`]: { error } },
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
- { [type]: modelValue },
107
+ this.getStateContext(),
106
108
  this.socket
107
109
  );
108
110
  viewportNode.innerHTML = itemString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "7.13.0",
3
+ "version": "8.0.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {
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;