saasufy-components 2.15.2 → 3.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 +3 -6
- package/input-provider.js +7 -5
- package/model-input.js +49 -83
- package/model-text.js +34 -74
- package/model-viewer.js +20 -30
- package/package.json +3 -3
- package/utils.js +26 -3
package/README.md
CHANGED
|
@@ -348,14 +348,12 @@ Used for displaying and editing a single field of a model instance in real time.
|
|
|
348
348
|
- `model-id` (required): The id of the model instance/record to bind to.
|
|
349
349
|
- `model-field` (required): The field of the model instance/record to bind to for reading and updating.
|
|
350
350
|
- `debounce-delay`: The delay in milliseconds to wait before sending an update to the server. This is useful to batch multiple updates together (as is common when user is typing). Default is 300ms.
|
|
351
|
-
- `enable-rebound`: By default, real-time updates performed via `model-input` components are not sent back to the publishing client as it is assumed that state changes are either localized to a single `model-input` component or shared with other components on the same page via the use of `consumers`, `bind-to-collection` or `bind-to-model` attributes. There may be situations where you have a specific value displayed in multiple places on the same page/screen and want them to be updated in real-time to reflect the latest value of the model field. For convenience, you can add the `enable-rebound` attribute to the `model-input` component to force real-time updates to rebound back to the publisher and this will ensure that all components on the page will receive real-time updates (will sync with each other) without having to explicitly bind them together via the front end. Note that enabling rebound comes with additional performance overheads.
|
|
352
351
|
- `input-id`: Can be used to set an `id` attribute on the inner `input` element.
|
|
353
352
|
- `list`: If specified, this sets the list attribute of the inner `input` element to provide input suggestions (only works with standard input elements).
|
|
354
|
-
- `bind-to-model`: If this attribute exists, the `model-input` component will attempt to host its model data within the nearest compatible `model-viewer` parent component instead of creating its own model internally. This can improve efficiency/reuse of model data and provide a better user experience. For example, if the `model-input` element is re-rendered, it will not have to re-fetch its data as it will be kept inside its parent `model-viewer` component.
|
|
355
|
-
- `bind-to-collection`: If this attribute exists, the `model-input` component will attempt to host its model data within the nearest compatible `collection-browser` or `collection-reducer` parent component instead of creating its own model internally. This can improve efficiency/reuse of model data and provide a better user experience. For example, if the `model-input` element is re-rendered, it will not have to re-fetch its data as it will be kept inside its parent `collection-browser` or `collection-reducer` component. Note that the parent component will take over the responsibility for cleaning up the model; this includes scenarios where the user loads a different page of the collection/view where the specified model instance is no longer present.
|
|
356
353
|
- `type`: The type of the input component; can be `text`, `number`, `select`, `textarea` or `checkbox`.
|
|
357
354
|
- `placeholder`: Can be used to set the placeholder text on the inner input element.
|
|
358
355
|
- `consumers`: This allows you to connect this `model-input` to other elements on your page which share the same parent element. It takes a list of selectors with optional attributes to target. For example `.my-input:value` will find all elements with a `my-input` class and update their `value` attributes with the value of the `model-input` component in real-time. You can specify multiple selectors separated by commas such as `.my-input,my-div` - In this case, because attribute names are not specified, values will be injected into the `value` attribute (for input elements) or into the `innerHTML` property (for other kinds of elements). The default attribute/property depends on the element type.
|
|
356
|
+
- `provider-template`: A template string within which the `{{value}}` can be injected before passing to consumers.
|
|
359
357
|
- `show-error-message`: If this attribute is present, an error message will be displayed above the input if it fails to update the value.
|
|
360
358
|
- `options`: A comma-separated list of options to provide - This only works if the input type is `select`.
|
|
361
359
|
- `height`: Allows you to set the height of the inner `input` element programmatically.
|
|
@@ -364,6 +362,7 @@ Used for displaying and editing a single field of a model instance in real time.
|
|
|
364
362
|
- `hide-error-logs`: By default, this component will log errors to the console. If set, this attribute will suppress such errors from showing up on the console.
|
|
365
363
|
- `autocapitalize`: Can be `on` or `off` - It will set the auto-capitalize attribute on the inner input element. This is useful for mobile devices to enable or disable auto-capitalization of the first character which is typed into the input element.
|
|
366
364
|
- `autocorrect`: Can be `on` or `off` - It will set the auto-correct attribute on the inner input element. This is useful for mobile devices to enable or disable auto-correct.
|
|
365
|
+
- `enable-rebound`: By default, for efficiency reasons, real-time updates performed via `model-input` components are not sent back to the publishing client; it is usually unnecessary because state changes are shared locally between all components which are bound to the same `socket-provider`. You can add the `enable-rebound` attribute to the `model-input` component to force real-time updates to rebound back to the publisher to support more advanced scenarios. Note that enabling rebound comes with additional performance overheads.
|
|
367
366
|
|
|
368
367
|
### model-text
|
|
369
368
|
|
|
@@ -385,8 +384,6 @@ Used to displaying a field of a model instance in real time.
|
|
|
385
384
|
- `model-type` (required): Specifies the type of model to bind to. This should match a `Model` available in your Saasufy service.
|
|
386
385
|
- `model-id` (required): The id of the model instance/record to bind to.
|
|
387
386
|
- `model-field` (required): The field of the model instance/record to bind to for reading.
|
|
388
|
-
- `bind-to-model`: If this attribute exists, the `model-text` component will attempt to fetch its model data from the nearest compatible `model-viewer` parent component instead of creating its own model internally. This can improve efficiency/reuse of model data and provide a better user experience. For example, if the `model-text` element is re-rendered, it will not have to re-fetch its data as it will be kept inside its parent `model-viewer` component.
|
|
389
|
-
- `bind-to-collection`: If this attribute exists, the `model-text` component will attempt to fetch its model data from the nearest compatible `collection-browser` or `collection-reducer` parent component instead of creating its own model internally. This can improve efficiency/reuse of model data and provide a better user experience. For example, if the `model-text` element is re-rendered, it will not have to re-fetch its data as it will be kept inside its parent `collection-browser` or `collection-reducer` component. Note that the parent component will take over the responsibility for cleaning up the model; this includes scenarios where the user loads a different page of the collection/view where the specified model instance is no longer present.
|
|
390
387
|
- `hide-error-logs`: By default, this component will log errors to the console. If set, this attribute will suppress such errors from showing up on the console.
|
|
391
388
|
|
|
392
389
|
### model-viewer
|
|
@@ -426,7 +423,6 @@ Used for rendering a single model resource using a template. This is the single-
|
|
|
426
423
|
An input component which can pass data to other components (or HTML elements) via custom attributes.
|
|
427
424
|
A common use case for it is to pass user input to `collection-browser` or `model-viewer` components to then fetch data from Saasufy.
|
|
428
425
|
This component can be configured to provide its data to multiple components (consumers) via custom attributes.
|
|
429
|
-
Note that an `input-provider` only targets elements which are downstream from its parent element in the HTML hierarchy.
|
|
430
426
|
|
|
431
427
|
**Example usage**
|
|
432
428
|
|
|
@@ -446,6 +442,7 @@ Note that an `input-provider` only targets elements which are downstream from it
|
|
|
446
442
|
- `type`: The type of the input component; can be `text`, `number`, `select`, `textarea` or `checkbox`.
|
|
447
443
|
- `placeholder`: Can be used to set the placeholder text on the inner input element.
|
|
448
444
|
- `consumers`: This allows you to connect this `input-provider` to other elements on your page which share the same parent element. It takes a list of selectors with optional attributes to target. For example `.my-input:value` will find all elements with a `my-input` class and update their `value` attributes with the value of the `input-provider` component in real-time. You can specify multiple selectors separated by commas such as `.my-input,my-div` - In this case, because attribute names are not specified, values will be injected into the `value` attribute (for input elements) or into the `innerHTML` property (for other kinds of elements). The default attribute/property depends on the element type.
|
|
445
|
+
- `provider-template`: A template string within which the `{{value}}` can be injected before passing to consumers.
|
|
449
446
|
- `debounce-delay`: The delay in milliseconds to wait before triggering a change event. This is useful to batch multiple updates together (as is common when user is typing). Default is 800ms.
|
|
450
447
|
- `options`: A comma-separated list of options to provide - This only works if the input type is `select`.
|
|
451
448
|
- `height`: Allows you to set the height of the inner `input` element programmatically.
|
package/input-provider.js
CHANGED
|
@@ -135,20 +135,21 @@ class InputProvider extends HTMLElement {
|
|
|
135
135
|
|
|
136
136
|
let onInputChange = (event) => {
|
|
137
137
|
debounce(async () => {
|
|
138
|
+
let providerTemplate = this.getAttribute('provider-template');
|
|
138
139
|
if (event.target.value === this.lastValue) return;
|
|
139
140
|
if (inputElement.type === 'checkbox') {
|
|
140
141
|
let checked = !!inputElement.checked;
|
|
141
142
|
this.lastValue = checked;
|
|
142
|
-
updateConsumerElements(
|
|
143
|
+
updateConsumerElements(consumers, checked, providerTemplate);
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
145
146
|
this.lastValue = event.target.value;
|
|
146
147
|
if (event.target.value === '') {
|
|
147
|
-
updateConsumerElements(
|
|
148
|
+
updateConsumerElements(consumers, '', providerTemplate);
|
|
148
149
|
} else {
|
|
149
150
|
let targetValue = inputElement.type === 'number' ?
|
|
150
151
|
Number(event.target.value) : event.target.value;
|
|
151
|
-
updateConsumerElements(
|
|
152
|
+
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
152
153
|
}
|
|
153
154
|
}, debounceDelay);
|
|
154
155
|
};
|
|
@@ -158,14 +159,15 @@ class InputProvider extends HTMLElement {
|
|
|
158
159
|
if (inputElement.type !== 'checkbox') {
|
|
159
160
|
onInputKeyUp = async (event) => {
|
|
160
161
|
debounce(async () => {
|
|
162
|
+
let providerTemplate = this.getAttribute('provider-template');
|
|
161
163
|
if (event.target.value === this.lastValue) return;
|
|
162
164
|
this.lastValue = event.target.value;
|
|
163
165
|
if (event.target.value === '') {
|
|
164
|
-
updateConsumerElements(
|
|
166
|
+
updateConsumerElements(consumers, '', providerTemplate);
|
|
165
167
|
} else {
|
|
166
168
|
let targetValue = inputElement.type === 'number' ?
|
|
167
169
|
Number(event.target.value) : event.target.value;
|
|
168
|
-
updateConsumerElements(
|
|
170
|
+
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
169
171
|
}
|
|
170
172
|
}, debounceDelay);
|
|
171
173
|
};
|
package/model-input.js
CHANGED
|
@@ -8,6 +8,7 @@ class ModelInput extends SocketConsumer {
|
|
|
8
8
|
this.isReady = false;
|
|
9
9
|
this.inputElement = null;
|
|
10
10
|
}
|
|
11
|
+
|
|
11
12
|
connectedCallback() {
|
|
12
13
|
this.isReady = true;
|
|
13
14
|
this.destroy = () => {};
|
|
@@ -22,10 +23,7 @@ class ModelInput extends SocketConsumer {
|
|
|
22
23
|
return [
|
|
23
24
|
'input-id',
|
|
24
25
|
'list',
|
|
25
|
-
'
|
|
26
|
-
'collection-instance-property',
|
|
27
|
-
'bind-to-model',
|
|
28
|
-
'bind-to-collection',
|
|
26
|
+
'socket-instance-property',
|
|
29
27
|
'type',
|
|
30
28
|
'placeholder',
|
|
31
29
|
'consumers',
|
|
@@ -89,10 +87,7 @@ class ModelInput extends SocketConsumer {
|
|
|
89
87
|
|
|
90
88
|
render() {
|
|
91
89
|
this.innerHTML = '';
|
|
92
|
-
let
|
|
93
|
-
let modelInstanceProperty = this.getAttribute('model-instance-property');
|
|
94
|
-
let bindToCollection = this.hasAttribute('bind-to-collection');
|
|
95
|
-
let collectionInstanceProperty = this.getAttribute('collection-instance-property');
|
|
90
|
+
let socketInstanceProperty = this.getAttribute('socket-instance-property');
|
|
96
91
|
let showErrorMessage = this.hasAttribute('show-error-message');
|
|
97
92
|
let inputId = this.getAttribute('input-id');
|
|
98
93
|
let autocapitalize = this.getAttribute('autocapitalize');
|
|
@@ -107,76 +102,43 @@ class ModelInput extends SocketConsumer {
|
|
|
107
102
|
let height = this.getAttribute('height');
|
|
108
103
|
let hideErrorLogs = this.hasAttribute('hide-error-logs');
|
|
109
104
|
let enableRebound = this.hasAttribute('enable-rebound');
|
|
110
|
-
|
|
111
|
-
let
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
collectionInstanceProperty = 'collection';
|
|
115
|
-
}
|
|
116
|
-
if (collectionInstanceProperty) {
|
|
117
|
-
let collection;
|
|
105
|
+
|
|
106
|
+
let socket;
|
|
107
|
+
if (socketInstanceProperty) {
|
|
108
|
+
let currentNode = this.parentNode;
|
|
118
109
|
while (currentNode) {
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
collection = null;
|
|
122
|
-
}
|
|
123
|
-
if (collection && (!collection.agModels || !collection.agModels[modelId])) {
|
|
124
|
-
collection = null;
|
|
125
|
-
}
|
|
126
|
-
if (collection) break;
|
|
110
|
+
socket = currentNode[socketInstanceProperty];
|
|
111
|
+
if (socket) break;
|
|
127
112
|
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
128
113
|
}
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
114
|
+
if (!socket) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`The ${
|
|
117
|
+
this.nodeName.toLowerCase()
|
|
118
|
+
} element failed to bind to a socket via the ${
|
|
119
|
+
socketInstanceProperty
|
|
120
|
+
} socket-instance-property`
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
} else {
|
|
124
|
+
socket = this.getSocket();
|
|
135
125
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
if (model) break;
|
|
151
|
-
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
152
|
-
}
|
|
153
|
-
if (!model) {
|
|
154
|
-
throw new Error(
|
|
155
|
-
`The ${
|
|
156
|
-
this.nodeName.toLowerCase()
|
|
157
|
-
} element failed to bind to a model - Ensure that the element is nested inside a parent element which exposes a model instance of the same type with a ${modelField} field`
|
|
126
|
+
this.socket = socket;
|
|
127
|
+
let model = new AGModel({
|
|
128
|
+
socket: this.socket,
|
|
129
|
+
type: modelType,
|
|
130
|
+
id: modelId,
|
|
131
|
+
fields: [ modelField ],
|
|
132
|
+
enableRebound
|
|
133
|
+
});
|
|
134
|
+
if (!hideErrorLogs) {
|
|
135
|
+
(async () => {
|
|
136
|
+
for await (let { error } of model.listener('error')) {
|
|
137
|
+
console.error(
|
|
138
|
+
`Model input encountered an error: ${error.message}`
|
|
158
139
|
);
|
|
159
|
-
};
|
|
160
|
-
} else {
|
|
161
|
-
this.socket = this.getSocket();
|
|
162
|
-
model = new AGModel({
|
|
163
|
-
socket: this.socket,
|
|
164
|
-
type: modelType,
|
|
165
|
-
id: modelId,
|
|
166
|
-
fields: [ modelField ],
|
|
167
|
-
enableRebound
|
|
168
|
-
});
|
|
169
|
-
if (!hideErrorLogs) {
|
|
170
|
-
(async () => {
|
|
171
|
-
for await (let { error } of model.listener('error')) {
|
|
172
|
-
console.error(
|
|
173
|
-
`Model input encountered an error: ${error.message}`
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
})();
|
|
177
140
|
}
|
|
178
|
-
|
|
179
|
-
}
|
|
141
|
+
})();
|
|
180
142
|
}
|
|
181
143
|
if (model.isLoaded) {
|
|
182
144
|
this.setAttribute('is-loaded', '');
|
|
@@ -240,22 +202,21 @@ class ModelInput extends SocketConsumer {
|
|
|
240
202
|
this.destroy = () => {
|
|
241
203
|
destroyInputSync();
|
|
242
204
|
this.isLoadedConsumer && this.isLoadedConsumer.kill();
|
|
243
|
-
|
|
244
|
-
model.destroy();
|
|
245
|
-
}
|
|
205
|
+
model.destroy();
|
|
246
206
|
};
|
|
247
207
|
}
|
|
248
208
|
|
|
249
209
|
updateInputElement(inputElement, fieldValue) {
|
|
250
210
|
let consumers = this.getAttribute('consumers');
|
|
211
|
+
let providerTemplate = this.getAttribute('provider-template');
|
|
251
212
|
if (inputElement.type === 'checkbox') {
|
|
252
213
|
let checked = !!fieldValue;
|
|
253
214
|
inputElement.checked = checked;
|
|
254
|
-
updateConsumerElements(
|
|
215
|
+
updateConsumerElements(consumers, checked, providerTemplate);
|
|
255
216
|
} else {
|
|
256
217
|
let defaultValue = this.getAttribute('default-value') || '';
|
|
257
218
|
inputElement.value = fieldValue == null ? defaultValue : fieldValue;
|
|
258
|
-
updateConsumerElements(
|
|
219
|
+
updateConsumerElements(consumers, inputElement.value, providerTemplate);
|
|
259
220
|
}
|
|
260
221
|
}
|
|
261
222
|
|
|
@@ -303,12 +264,14 @@ class ModelInput extends SocketConsumer {
|
|
|
303
264
|
let debounce = debouncer();
|
|
304
265
|
|
|
305
266
|
let onInputChange = (event) => {
|
|
306
|
-
if (event.target.value === String(model.value[fieldName])) return;
|
|
307
267
|
debounce(async () => {
|
|
268
|
+
if (event.target.value === String(model.value[fieldName] || '')) return;
|
|
269
|
+
|
|
270
|
+
let providerTemplate = this.getAttribute('provider-template');
|
|
308
271
|
try {
|
|
309
272
|
if (inputElement.type === 'checkbox') {
|
|
310
273
|
let checked = !!inputElement.checked;
|
|
311
|
-
updateConsumerElements(
|
|
274
|
+
updateConsumerElements(consumers, checked, providerTemplate);
|
|
312
275
|
await model.update(fieldName, checked);
|
|
313
276
|
inputElement.classList.remove(errorStyleClass);
|
|
314
277
|
inputElement.classList.add(successStyleClass);
|
|
@@ -316,12 +279,12 @@ class ModelInput extends SocketConsumer {
|
|
|
316
279
|
return;
|
|
317
280
|
}
|
|
318
281
|
if (event.target.value === '') {
|
|
319
|
-
updateConsumerElements(
|
|
282
|
+
updateConsumerElements(consumers, '', providerTemplate);
|
|
320
283
|
await model.delete(fieldName);
|
|
321
284
|
} else {
|
|
322
285
|
let targetValue = inputElement.type === 'number' ?
|
|
323
286
|
Number(event.target.value) : event.target.value;
|
|
324
|
-
updateConsumerElements(
|
|
287
|
+
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
325
288
|
await model.update(fieldName, targetValue);
|
|
326
289
|
}
|
|
327
290
|
inputElement.classList.remove(errorStyleClass);
|
|
@@ -340,14 +303,17 @@ class ModelInput extends SocketConsumer {
|
|
|
340
303
|
if (inputElement.type !== 'checkbox' && inputElement.type !== 'select') {
|
|
341
304
|
onInputKeyUp = async (event) => {
|
|
342
305
|
debounce(async () => {
|
|
306
|
+
if (event.target.value === String(model.value[fieldName] || '')) return;
|
|
307
|
+
|
|
308
|
+
let providerTemplate = this.getAttribute('provider-template');
|
|
343
309
|
try {
|
|
344
310
|
if (event.target.value === '') {
|
|
345
311
|
await model.delete(fieldName);
|
|
346
|
-
updateConsumerElements(
|
|
312
|
+
updateConsumerElements(consumers, '', providerTemplate);
|
|
347
313
|
} else {
|
|
348
314
|
let targetValue = inputElement.type === 'number' ?
|
|
349
315
|
Number(event.target.value) : event.target.value;
|
|
350
|
-
updateConsumerElements(
|
|
316
|
+
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
351
317
|
await model.update(fieldName, targetValue);
|
|
352
318
|
}
|
|
353
319
|
inputElement.classList.remove(errorStyleClass);
|
package/model-text.js
CHANGED
|
@@ -20,10 +20,7 @@ class ModelText extends SocketConsumer {
|
|
|
20
20
|
|
|
21
21
|
static get observedAttributes() {
|
|
22
22
|
return [
|
|
23
|
-
'
|
|
24
|
-
'collection-instance-property',
|
|
25
|
-
'bind-to-collection',
|
|
26
|
-
'bind-to-model',
|
|
23
|
+
'socket-instance-property',
|
|
27
24
|
'model-type',
|
|
28
25
|
'model-id',
|
|
29
26
|
'model-field',
|
|
@@ -39,84 +36,49 @@ class ModelText extends SocketConsumer {
|
|
|
39
36
|
|
|
40
37
|
render() {
|
|
41
38
|
this.innerHTML = '';
|
|
42
|
-
let
|
|
43
|
-
let modelInstanceProperty = this.getAttribute('model-instance-property');
|
|
44
|
-
let bindToCollection = this.hasAttribute('bind-to-collection');
|
|
45
|
-
let collectionInstanceProperty = this.getAttribute('collection-instance-property');
|
|
39
|
+
let socketInstanceProperty = this.getAttribute('socket-instance-property');
|
|
46
40
|
let modelType = this.getAttribute('model-type');
|
|
47
41
|
let modelId = this.getAttribute('model-id');
|
|
48
42
|
let modelField = this.getAttribute('model-field');
|
|
49
43
|
let hideErrorLogs = this.hasAttribute('hide-error-logs');
|
|
50
|
-
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
collectionInstanceProperty = 'collection';
|
|
55
|
-
}
|
|
56
|
-
if (collectionInstanceProperty) {
|
|
57
|
-
let collection;
|
|
44
|
+
|
|
45
|
+
let socket;
|
|
46
|
+
if (socketInstanceProperty) {
|
|
47
|
+
let currentNode = this.parentNode;
|
|
58
48
|
while (currentNode) {
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
collection = null;
|
|
62
|
-
}
|
|
63
|
-
if (collection && (!collection.agModels || !collection.agModels[modelId])) {
|
|
64
|
-
collection = null;
|
|
65
|
-
}
|
|
66
|
-
if (collection) break;
|
|
49
|
+
socket = currentNode[socketInstanceProperty];
|
|
50
|
+
if (socket) break;
|
|
67
51
|
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
68
52
|
}
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
if (!socket) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`The ${
|
|
56
|
+
this.nodeName.toLowerCase()
|
|
57
|
+
} element failed to bind to a socket via the ${
|
|
58
|
+
socketInstanceProperty
|
|
59
|
+
} socket-instance-property`
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
socket = this.getSocket();
|
|
75
64
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
model = null;
|
|
89
|
-
}
|
|
90
|
-
if (model) break;
|
|
91
|
-
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
92
|
-
}
|
|
93
|
-
if (!model) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`The ${
|
|
96
|
-
this.nodeName.toLowerCase()
|
|
97
|
-
} element failed to bind to a model - Ensure that the element is nested inside a parent element which exposes a model instance of the same type with a ${modelField} field`
|
|
65
|
+
this.socket = socket;
|
|
66
|
+
let model = new AGModel({
|
|
67
|
+
socket: this.socket,
|
|
68
|
+
type: modelType,
|
|
69
|
+
id: modelId,
|
|
70
|
+
fields: [ modelField ]
|
|
71
|
+
});
|
|
72
|
+
if (!hideErrorLogs) {
|
|
73
|
+
(async () => {
|
|
74
|
+
for await (let { error } of model.listener('error')) {
|
|
75
|
+
console.error(
|
|
76
|
+
`Model text encountered an error: ${error.message}`
|
|
98
77
|
);
|
|
99
|
-
};
|
|
100
|
-
} else {
|
|
101
|
-
this.socket = this.getSocket();
|
|
102
|
-
model = new AGModel({
|
|
103
|
-
socket: this.socket,
|
|
104
|
-
type: modelType,
|
|
105
|
-
id: modelId,
|
|
106
|
-
fields: [ modelField ]
|
|
107
|
-
});
|
|
108
|
-
if (!hideErrorLogs) {
|
|
109
|
-
(async () => {
|
|
110
|
-
for await (let { error } of model.listener('error')) {
|
|
111
|
-
console.error(
|
|
112
|
-
`Model text encountered an error: ${error.message}`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
})();
|
|
116
78
|
}
|
|
117
|
-
|
|
118
|
-
}
|
|
79
|
+
})();
|
|
119
80
|
}
|
|
81
|
+
|
|
120
82
|
if (model.isLoaded) {
|
|
121
83
|
this.setAttribute('is-loaded', '');
|
|
122
84
|
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
@@ -142,9 +104,7 @@ class ModelText extends SocketConsumer {
|
|
|
142
104
|
this.destroy = () => {
|
|
143
105
|
changeConsumer.kill();
|
|
144
106
|
this.isLoadedConsumer && this.isLoadedConsumer.kill();
|
|
145
|
-
|
|
146
|
-
model.destroy();
|
|
147
|
-
}
|
|
107
|
+
model.destroy();
|
|
148
108
|
};
|
|
149
109
|
}
|
|
150
110
|
}
|
package/model-viewer.js
CHANGED
|
@@ -16,19 +16,14 @@ class ModelViewer extends SocketConsumer {
|
|
|
16
16
|
|
|
17
17
|
connectedCallback() {
|
|
18
18
|
this.isReady = true;
|
|
19
|
-
this.socket = this.getSocket();
|
|
20
19
|
this.activeLoader = null;
|
|
21
20
|
this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
|
|
22
21
|
this.render();
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
disconnectedCallback() {
|
|
26
|
-
if (this.model
|
|
25
|
+
if (this.model) {
|
|
27
26
|
this.model.destroy();
|
|
28
|
-
} else {
|
|
29
|
-
if (this.modelLoadConsumer) this.modelLoadConsumer.kill();
|
|
30
|
-
if (this.modelChangeConsumer) this.modelChangeConsumer.kill();
|
|
31
|
-
if (this.modelErrorConsumer) this.modelErrorConsumer.kill();
|
|
32
27
|
}
|
|
33
28
|
this.shadowRoot.removeEventListener('slotchange', this.handleSlotChangeEvent);
|
|
34
29
|
}
|
|
@@ -93,43 +88,38 @@ class ModelViewer extends SocketConsumer {
|
|
|
93
88
|
let modelType = this.getAttribute('model-type');
|
|
94
89
|
let modelId = this.getAttribute('model-id');
|
|
95
90
|
let modelFields = this.getAttribute('model-fields') || '';
|
|
96
|
-
let
|
|
91
|
+
let socketInstanceProperty = this.getAttribute('socket-instance-property');
|
|
97
92
|
let hideErrorLogs = this.hasAttribute('hide-error-logs');
|
|
98
93
|
|
|
99
|
-
if (this.model
|
|
94
|
+
if (this.model) this.model.destroy();
|
|
100
95
|
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
let socket;
|
|
97
|
+
if (socketInstanceProperty) {
|
|
98
|
+
let currentNode = this.parentNode;
|
|
104
99
|
while (currentNode) {
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
model = null;
|
|
108
|
-
}
|
|
109
|
-
if (model && !model.agFields) {
|
|
110
|
-
model = null;
|
|
111
|
-
}
|
|
112
|
-
if (model) break;
|
|
100
|
+
socket = currentNode[socketInstanceProperty];
|
|
101
|
+
if (socket) break;
|
|
113
102
|
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
114
103
|
}
|
|
115
|
-
if (!
|
|
104
|
+
if (!socket) {
|
|
116
105
|
throw new Error(
|
|
117
106
|
`The ${
|
|
118
107
|
this.nodeName.toLowerCase()
|
|
119
|
-
} element failed to
|
|
108
|
+
} element failed to bind to a socket via the ${
|
|
109
|
+
socketInstanceProperty
|
|
110
|
+
} socket-instance-property`
|
|
120
111
|
);
|
|
121
112
|
};
|
|
122
|
-
this.model = model;
|
|
123
|
-
this.isModelLocal = false;
|
|
124
113
|
} else {
|
|
125
|
-
|
|
126
|
-
socket: this.socket,
|
|
127
|
-
type: modelType,
|
|
128
|
-
id: modelId,
|
|
129
|
-
fields: modelFields.split(',').map(field => field.trim()).filter(field => field)
|
|
130
|
-
});
|
|
131
|
-
this.isModelLocal = true;
|
|
114
|
+
socket = this.getSocket();
|
|
132
115
|
}
|
|
116
|
+
this.socket = socket;
|
|
117
|
+
this.model = new AGModel({
|
|
118
|
+
socket: this.socket,
|
|
119
|
+
type: modelType,
|
|
120
|
+
id: modelId,
|
|
121
|
+
fields: modelFields.split(',').map(field => field.trim()).filter(field => field)
|
|
122
|
+
});
|
|
133
123
|
|
|
134
124
|
this.shadowRoot.innerHTML = `
|
|
135
125
|
<slot name="loader"></slot>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saasufy-components",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Components for Saasufy.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"author": "Jonathan Gros-Dubois",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"ag-collection": "^
|
|
21
|
-
"ag-model": "^
|
|
20
|
+
"ag-collection": "^5.0.0",
|
|
21
|
+
"ag-model": "^3.0.0",
|
|
22
22
|
"capitalisk-auth-client": "^1.5.1",
|
|
23
23
|
"socketcluster-client": "^19.1.0"
|
|
24
24
|
},
|
package/utils.js
CHANGED
|
@@ -89,6 +89,10 @@ export function createReactiveCollection(collectionOptions, callback) {
|
|
|
89
89
|
}
|
|
90
90
|
})();
|
|
91
91
|
|
|
92
|
+
if (collection.isLoaded) {
|
|
93
|
+
collection.safeValue = collection.value.map(toSafeModelValue);
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
return collection;
|
|
93
97
|
}
|
|
94
98
|
|
|
@@ -120,6 +124,10 @@ export function createReactiveModel(modelOptions, callback) {
|
|
|
120
124
|
}
|
|
121
125
|
})();
|
|
122
126
|
|
|
127
|
+
if (model.isLoaded) {
|
|
128
|
+
model.safeValue = toSafeModelValue(model.value);
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
return model;
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -142,6 +150,10 @@ export function createCollection(collectionOptions) {
|
|
|
142
150
|
}
|
|
143
151
|
})();
|
|
144
152
|
|
|
153
|
+
if (collection.isLoaded) {
|
|
154
|
+
collection.safeValue = collection.value.map(toSafeModelValue);
|
|
155
|
+
}
|
|
156
|
+
|
|
145
157
|
return collection;
|
|
146
158
|
}
|
|
147
159
|
|
|
@@ -162,6 +174,10 @@ export function createModel(modelOptions) {
|
|
|
162
174
|
}
|
|
163
175
|
})();
|
|
164
176
|
|
|
177
|
+
if (model.isLoaded) {
|
|
178
|
+
model.safeValue = toSafeModelValue(model.value);
|
|
179
|
+
}
|
|
180
|
+
|
|
165
181
|
return model;
|
|
166
182
|
}
|
|
167
183
|
|
|
@@ -232,8 +248,8 @@ export function renderTemplate(templateString, data, socket) {
|
|
|
232
248
|
});
|
|
233
249
|
}
|
|
234
250
|
|
|
235
|
-
export function updateConsumerElements(
|
|
236
|
-
if (
|
|
251
|
+
export function updateConsumerElements(consumers, value, template) {
|
|
252
|
+
if (consumers) {
|
|
237
253
|
let consumerParts = consumers.split(',')
|
|
238
254
|
.filter(part => part)
|
|
239
255
|
.map(part => {
|
|
@@ -242,8 +258,15 @@ export function updateConsumerElements(parentElement, consumers, value) {
|
|
|
242
258
|
})
|
|
243
259
|
.filter(([ selector, attributeName ]) => selector);
|
|
244
260
|
|
|
261
|
+
if (template) {
|
|
262
|
+
value = renderTemplate(
|
|
263
|
+
template,
|
|
264
|
+
{ value }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
245
268
|
for (let [ selector, attributeName ] of consumerParts) {
|
|
246
|
-
let matchingElements =
|
|
269
|
+
let matchingElements = document.querySelectorAll(selector);
|
|
247
270
|
if (attributeName) {
|
|
248
271
|
for (let element of matchingElements) {
|
|
249
272
|
if (typeof value === 'boolean') {
|