saasufy-components 1.0.6 → 1.1.1

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/app-router.js ADDED
@@ -0,0 +1,89 @@
1
+ import { getSafeHTML } from './utils.js';
2
+ import { renderTemplate } from './utils.js';
3
+
4
+ export class AppRouter extends HTMLElement {
5
+ constructor() {
6
+ super();
7
+ this.attachShadow({ mode: 'open' });
8
+
9
+ this.pages = {};
10
+ this.pageRouteInfos = [];
11
+
12
+ this.hashStartRegex = /^#/;
13
+
14
+ this.handleHashChangeEvent = () => {
15
+ this.renderCurrentPage();
16
+ };
17
+
18
+ this.handleSlotChangeEvent = () => {
19
+ let pageTemplates = this.shadowRoot.querySelector('slot[name="page"]').assignedNodes();
20
+
21
+ for (let template of pageTemplates) {
22
+ let routePath = template.getAttribute('route-path');
23
+ this.pages[routePath] = template;
24
+ }
25
+
26
+ this.pageRouteInfos = Object.keys(this.pages).map((route) => {
27
+ return {
28
+ regExp: new RegExp(`^${route.replace(/\/:[^\/]+/g, '/([^/]*)').replace(/\//g, '\\/')}$`, 'g'),
29
+ route,
30
+ params: [ ...route.matchAll(/:[^\/]*/g) ].map(paramMatch => paramMatch[0].replace(':', ''))
31
+ };
32
+ });
33
+
34
+ this.renderCurrentPage();
35
+ };
36
+ }
37
+
38
+ computeRouteArgs(page, regExp, params) {
39
+ let match = page.matchAll(regExp).next().value || [];
40
+ let argEntries = match
41
+ .slice(1)
42
+ .map((groupMatch, i) => [ params[i], groupMatch ]);
43
+ return Object.fromEntries(argEntries);
44
+ }
45
+
46
+ connectedCallback() {
47
+ window.addEventListener('hashchange', this.handleHashChangeEvent);
48
+ this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
49
+ this.render();
50
+ }
51
+
52
+ disconnectedCallback() {
53
+ window.removeEventListener('hashchange', this.handleHashChangeEvent);
54
+ this.shadowRoot.removeEventListener('slotchange', this.handleSlotChangeEvent);
55
+ }
56
+
57
+ renderCurrentPage() {
58
+ let routerViewport = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
59
+ if (!routerViewport) return;
60
+
61
+ let { hash } = location;
62
+ let pagePath = hash.replace(this.hashStartRegex, '');
63
+
64
+ let { route, regExp, params } = this.pageRouteInfos.find(
65
+ ({ regExp }) => pagePath.match(regExp)
66
+ ) || {};
67
+
68
+ let pageTemplate = this.pages[route];
69
+ if (!pageTemplate) return;
70
+
71
+ let routeArgs;
72
+ if (route) {
73
+ routeArgs = this.computeRouteArgs(pagePath, regExp, params);
74
+ } else {
75
+ routeArgs = {};
76
+ }
77
+
78
+ routerViewport.innerHTML = renderTemplate(pageTemplate.innerHTML, routeArgs);
79
+ }
80
+
81
+ render() {
82
+ this.shadowRoot.innerHTML = `
83
+ <slot name="page"></slot>
84
+ <slot name="viewport"></slot>
85
+ `;
86
+ }
87
+ }
88
+
89
+ window.customElements.define('app-router', AppRouter);
@@ -1,11 +1,11 @@
1
- import { getSocket } from './socket.js';
1
+ import { SocketConsumer } from './socket.js';
2
2
  import AGCollection from '/node_modules/ag-collection/ag-collection.js';
3
3
 
4
- class CollectionAdder extends HTMLElement {
4
+ class CollectionAdder extends SocketConsumer {
5
5
  constructor(options) {
6
6
  super();
7
- options = options || {};
8
- this.socket = getSocket(options.socketOptions);
7
+ this.options = options || {};
8
+
9
9
  this.typeCastFunctions = {
10
10
  text: String,
11
11
  checkbox: Boolean,
@@ -16,6 +16,7 @@ class CollectionAdder extends HTMLElement {
16
16
  }
17
17
 
18
18
  connectedCallback() {
19
+ this.socket = this.getSocket();
19
20
  this.isReady = true;
20
21
  this.render();
21
22
  }
@@ -1,14 +1,13 @@
1
- import { getSocket } from './socket.js';
1
+ import { SocketConsumer } from './socket.js';
2
2
  import AGCollection from '/node_modules/ag-collection/ag-collection.js';
3
- import { getSafeHTML } from './utils.js';
3
+ import { renderTemplate } from './utils.js';
4
4
 
5
5
  const DEFAULT_RELOAD_DELAY = 0;
6
6
 
7
- class CollectionBrowser extends HTMLElement {
7
+ class CollectionBrowser extends SocketConsumer {
8
8
  constructor() {
9
9
  super();
10
10
  this.attachShadow({ mode: 'open' });
11
- this.socket = getSocket();
12
11
 
13
12
  this.handleShowModalEvent = (event) => {
14
13
  let modal = this.shadowRoot.querySelector('slot[name="modal"]').assignedNodes()[0];
@@ -40,12 +39,13 @@ class CollectionBrowser extends HTMLElement {
40
39
  };
41
40
 
42
41
  this.handleSlotChangeEvent = () => {
43
- this.renderTable();
42
+ this.renderList();
44
43
  };
45
44
  }
46
45
 
47
46
  connectedCallback() {
48
47
  this.isReady = true;
48
+ this.socket = this.getSocket();
49
49
  this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
50
50
  this.addEventListener('showModal', this.handleShowModalEvent);
51
51
  this.addEventListener('crudCreate', this.handleCRUDCreateEvent);
@@ -146,28 +146,23 @@ class CollectionBrowser extends HTMLElement {
146
146
  }
147
147
  }
148
148
 
149
- renderTable() {
149
+ renderList() {
150
150
  if (!this.collection || !this.collection.isLoaded) return;
151
151
  let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
152
152
  if (!itemTemplate) return;
153
- let listNode = this.shadowRoot.querySelector('slot[name="list"]').assignedNodes()[0];
154
- if (!listNode) return;
153
+ let viewportNode = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
154
+ if (!viewportNode) return;
155
155
  let items = [];
156
156
 
157
157
  for (let modelItem of this.collection.value) {
158
- let itemString = itemTemplate.innerHTML;
159
-
160
- for (let [ field, value ] of Object.entries(modelItem)) {
161
- let regExp = new RegExp(`{{${field}}}`, 'g');
162
- itemString = itemString.replace(regExp, getSafeHTML(value));
163
- }
158
+ let itemString = renderTemplate(itemTemplate.innerHTML, modelItem, this.collection.type);
164
159
  items.push(itemString);
165
160
  }
166
161
  let noItemTemplate = this.shadowRoot.querySelector('slot[name="no-item"]').assignedNodes()[0];
167
162
  if (noItemTemplate && !items.length) {
168
- listNode.innerHTML = noItemTemplate.innerHTML;
163
+ viewportNode.innerHTML = noItemTemplate.innerHTML;
169
164
  } else {
170
- listNode.innerHTML = items.join('');
165
+ viewportNode.innerHTML = items.join('');
171
166
  }
172
167
  }
173
168
 
@@ -178,6 +173,7 @@ class CollectionBrowser extends HTMLElement {
178
173
  let collectionPageSize = this.getAttribute('collection-page-size');
179
174
  let collectionViewParams = this.getAttribute('collection-view-params');
180
175
  let collectionPageOffset = this.getAttribute('collection-page-offset');
176
+ let hideErrorLogs = this.hasAttribute('hide-error-logs');
181
177
  let collectionReloadDelay = Number(
182
178
  this.getAttribute('collection-reload-delay') || DEFAULT_RELOAD_DELAY
183
179
  );
@@ -198,7 +194,7 @@ class CollectionBrowser extends HTMLElement {
198
194
  this.shadowRoot.innerHTML = `
199
195
  <slot name="item"></slot>
200
196
  <slot name="no-item"></slot>
201
- <slot name="list"></slot>
197
+ <slot name="viewport"></slot>
202
198
  <slot name="previous-page"></slot>
203
199
  <slot name="page-number"></slot>
204
200
  <slot name="next-page"></slot>
@@ -221,7 +217,7 @@ class CollectionBrowser extends HTMLElement {
221
217
  (async () => {
222
218
  await this.collection.listener('load').once();
223
219
  if (!this.collection.value.length) {
224
- this.renderTable();
220
+ this.renderList();
225
221
  }
226
222
  this.setAttribute('is-loaded', '');
227
223
  this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
@@ -229,10 +225,20 @@ class CollectionBrowser extends HTMLElement {
229
225
 
230
226
  (async () => {
231
227
  for await (let event of this.collection.listener('change')) {
232
- this.renderTable();
228
+ this.renderList();
233
229
  this.updatePageButtons();
234
230
  }
235
231
  })();
232
+
233
+ if (!hideErrorLogs) {
234
+ (async () => {
235
+ for await (let { error } of this.collection.listener('error')) {
236
+ console.error(
237
+ `Collection browser encountered an error: ${error.message}`
238
+ );
239
+ }
240
+ })();
241
+ }
236
242
  }
237
243
  }
238
244
 
package/model-input.js CHANGED
@@ -1,13 +1,12 @@
1
- import { getSocket } from './socket.js';
1
+ import { SocketConsumer } from './socket.js';
2
2
  import { getSafeHTML, debouncer } from './utils.js';
3
3
  import AGModel from '/node_modules/ag-model/ag-model.js';
4
4
 
5
- class ModelInput extends HTMLElement {
5
+ class ModelInput extends SocketConsumer {
6
6
  constructor() {
7
7
  super();
8
8
  this.isReady = false;
9
9
  this.inputElement = null;
10
- this.socket = null;
11
10
  }
12
11
  connectedCallback() {
13
12
  this.isReady = true;
@@ -73,6 +72,7 @@ class ModelInput extends HTMLElement {
73
72
  let modelField = this.getAttribute('model-field');
74
73
  let options = this.getAttribute('options');
75
74
  let height = this.getAttribute('height');
75
+ let hideErrorLogs = this.hasAttribute('hide-error-logs');
76
76
  let currentNode = this.parentNode;
77
77
  let model;
78
78
  let isModelLocal = false;
@@ -80,17 +80,26 @@ class ModelInput extends HTMLElement {
80
80
  while (currentNode) {
81
81
  model = currentNode[modelInstanceProperty];
82
82
  if (model) break;
83
- currentNode = currentNode.parentNode;
83
+ currentNode = currentNode.getRootNode().host || currentNode.parentNode;
84
84
  }
85
85
  if (!model) return;
86
86
  } else {
87
- this.socket = getSocket();
87
+ this.socket = this.getSocket();
88
88
  model = new AGModel({
89
89
  socket: this.socket,
90
90
  type: modelType,
91
91
  id: modelId,
92
92
  fields: [ modelField ]
93
93
  });
94
+ if (!hideErrorLogs) {
95
+ (async () => {
96
+ for await (let { error } of model.listener('error')) {
97
+ console.error(
98
+ `Model input encountered an error: ${error.message}`
99
+ );
100
+ }
101
+ })();
102
+ }
94
103
  isModelLocal = true;
95
104
  }
96
105
  if (model.isLoaded) {
package/model-text.js CHANGED
@@ -1,11 +1,11 @@
1
- import { getSocket } from './socket.js';
1
+ import { SocketConsumer } from './socket.js';
2
2
  import { getSafeHTML } from './utils.js';
3
+ import AGModel from '/node_modules/ag-model/ag-model.js';
3
4
 
4
- class ModelText extends HTMLElement {
5
+ class ModelText extends SocketConsumer {
5
6
  connectedCallback() {
6
7
  this.isReady = true;
7
8
  this.destroy = () => {};
8
- this.socket = null;
9
9
  this.render();
10
10
  }
11
11
 
@@ -34,6 +34,7 @@ class ModelText extends HTMLElement {
34
34
  let modelType = this.getAttribute('model-type');
35
35
  let modelId = this.getAttribute('model-id');
36
36
  let modelField = this.getAttribute('model-field');
37
+ let hideErrorLogs = this.hasAttribute('hide-error-logs');
37
38
  let currentNode = this.parentNode;
38
39
  let model;
39
40
  let isModelLocal = false;
@@ -41,17 +42,26 @@ class ModelText extends HTMLElement {
41
42
  while (currentNode) {
42
43
  model = currentNode[modelInstanceProperty];
43
44
  if (model) break;
44
- currentNode = currentNode.parentNode;
45
+ currentNode = currentNode.getRootNode().host || currentNode.parentNode;
45
46
  }
46
47
  if (!model) return;
47
48
  } else {
48
- this.socket = getSocket();
49
+ this.socket = this.getSocket();
49
50
  model = new AGModel({
50
51
  socket: this.socket,
51
52
  type: modelType,
52
53
  id: modelId,
53
54
  fields: [ modelField ]
54
55
  });
56
+ if (!hideErrorLogs) {
57
+ (async () => {
58
+ for await (let { error } of model.listener('error')) {
59
+ console.error(
60
+ `Model input encountered an error: ${error.message}`
61
+ );
62
+ }
63
+ })();
64
+ }
55
65
  isModelLocal = true;
56
66
  }
57
67
  if (model.isLoaded) {
@@ -69,7 +79,7 @@ class ModelText extends HTMLElement {
69
79
  (async () => {
70
80
  for await (let event of changeConsumer) {
71
81
  if (event.resourceField !== modelField || !event.isRemote) continue;
72
- fieldValue = getSafeHTML(model.value[modelField]);
82
+ let fieldValue = getSafeHTML(model.value[modelField]);
73
83
  this.innerHTML = fieldValue;
74
84
  }
75
85
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "1.0.6",
3
+ "version": "1.1.1",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/socket.js CHANGED
@@ -2,19 +2,107 @@ import { create } from '/node_modules/socketcluster-client/socketcluster-client.
2
2
 
3
3
  const DEFAULT_AUTH_TOKEN_NAME = 'saasufy.authToken';
4
4
 
5
- let socket;
5
+ let globalSocket;
6
6
 
7
- export function createSocket(options) {
8
- socket = create({
7
+ let urlPartsRegExp = /(^[^:]+):\/\/([^:\/]*)(:[0-9]*)?(\/.*)/;
8
+
9
+ export function createGlobalSocket(options) {
10
+ globalSocket = create({
9
11
  authTokenName: DEFAULT_AUTH_TOKEN_NAME,
10
12
  ...options
11
13
  });
12
- return socket;
14
+ return globalSocket;
15
+ }
16
+
17
+ export function getGlobalSocket(options) {
18
+ if (globalSocket) {
19
+ return globalSocket;
20
+ }
21
+ return createGlobalSocket(options);
13
22
  }
14
23
 
15
- export function getSocket(options) {
16
- if (socket) {
24
+ export class SocketProvider extends HTMLElement {
25
+ constructor() {
26
+ super();
27
+ this.saasufySocket = null;
28
+ this.isReady = false;
29
+ }
30
+
31
+ connectedCallback() {
32
+ this.isReady = true;
33
+ this.getSocket();
34
+ }
35
+
36
+ disconnectedCallback() {
37
+ this.destroySocket();
38
+ }
39
+
40
+ static get observedAttributes() {
41
+ return [
42
+ 'url',
43
+ 'auth-token-name'
44
+ ];
45
+ }
46
+
47
+ attributeChangedCallback(name, oldValue, newValue) {
48
+ if (!this.isReady) return;
49
+ this.destroySocket();
50
+ this.getSocket();
51
+ }
52
+
53
+ destroySocket() {
54
+ if (this.saasufySocket) {
55
+ this.saasufySocket.disconnect();
56
+ }
57
+ }
58
+
59
+ getSocket() {
60
+ if (this.saasufySocket) {
61
+ return this.saasufySocket;
62
+ }
63
+ return this.createSocket();
64
+ }
65
+
66
+ createSocket() {
67
+ let authTokenName = this.getAttribute('auth-token-name') || DEFAULT_AUTH_TOKEN_NAME;
68
+ let url = this.getAttribute('url') || '';
69
+ let urlOptions;
70
+ let matchedList = url.match(urlPartsRegExp);
71
+ if (matchedList) {
72
+ let [ fullMatch, protocolScheme, hostname, port, path ] = matchedList;
73
+ if (port) {
74
+ port = port.replace(/^:/, '');
75
+ }
76
+ urlOptions = {
77
+ protocolScheme,
78
+ hostname,
79
+ port,
80
+ path
81
+ };
82
+ } else {
83
+ urlOptions = {};
84
+ }
85
+ this.saasufySocket = create({
86
+ authTokenName,
87
+ ...urlOptions
88
+ });
89
+
90
+ return this.saasufySocket;
91
+ }
92
+ }
93
+
94
+ export class SocketConsumer extends HTMLElement {
95
+ getSocket() {
96
+ let socket = null;
97
+ let currentNode = this.parentNode;
98
+ while (currentNode) {
99
+ socket = currentNode.saasufySocket;
100
+ if (socket) break;
101
+ currentNode = currentNode.getRootNode().host || currentNode.parentNode;
102
+ }
17
103
  return socket;
18
104
  }
19
- return createSocket(options);
20
105
  }
106
+
107
+ window.customElements.define('socket-provider', SocketProvider);
108
+ window.customElements.define('socket-consumer', SocketConsumer);
package/utils.js CHANGED
@@ -155,6 +155,29 @@ export function createModel(modelOptions) {
155
155
  return model;
156
156
  }
157
157
 
158
+ let templateFormatters = {
159
+ url: (value) => String(value).toLowerCase().replace(/ /g, '-'),
160
+ lowerCase: (value) => String(value).toLowerCase(),
161
+ upperCase: (value) => String(value).toUpperCase(),
162
+ capitalize: (value) => {
163
+ let valueString = String(value);
164
+ return `${valueString.slice(0, 1).toUpperCase()}${valueString.slice(1)}`;
165
+ }
166
+ };
167
+
168
+ export function renderTemplate(templateString, data, dataType) {
169
+ for (let [ field, value ] of Object.entries(data)) {
170
+ let safeValue = getSafeHTML(value);
171
+ for (let [ formatName, formatFunction ] of Object.entries(templateFormatters)) {
172
+ let formatRegExp = new RegExp(`{{${formatName}:${dataType ? dataType + '.' : ''}${field}}}`, 'g');
173
+ templateString = templateString.replace(formatRegExp, formatFunction(safeValue));
174
+ }
175
+ let regExp = new RegExp(`{{${dataType ? dataType + '.' : ''}${field}}}`, 'g');
176
+ templateString = templateString.replace(regExp, safeValue);
177
+ }
178
+ return templateString;
179
+ }
180
+
158
181
  export function wait(duration) {
159
182
  return new Promise((resolve) => setTimeout(resolve, duration));
160
183
  }