saasufy-components 2.16.0 → 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 CHANGED
@@ -348,11 +348,8 @@ 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.
@@ -365,6 +362,7 @@ Used for displaying and editing a single field of a model instance in real time.
365
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.
366
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.
367
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.
368
366
 
369
367
  ### model-text
370
368
 
@@ -386,8 +384,6 @@ Used to displaying a field of a model instance in real time.
386
384
  - `model-type` (required): Specifies the type of model to bind to. This should match a `Model` available in your Saasufy service.
387
385
  - `model-id` (required): The id of the model instance/record to bind to.
388
386
  - `model-field` (required): The field of the model instance/record to bind to for reading.
389
- - `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.
390
- - `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.
391
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.
392
388
 
393
389
  ### model-viewer
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
- 'model-instance-property',
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 bindToModel = this.hasAttribute('bind-to-model');
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
- let currentNode = this.parentNode;
111
- let model;
112
- let isModelLocal = false;
113
- if (bindToCollection) {
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
- collection = currentNode[collectionInstanceProperty];
120
- if (collection && collection.type !== modelType) {
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 (collection) {
130
- model = collection.agModels[modelId];
131
- if (!model.agFields[modelField]) {
132
- model.addField(modelField);
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
- if (!model) {
137
- if (bindToModel) {
138
- modelInstanceProperty = 'model';
139
- }
140
- currentNode = this.parentNode;
141
- if (modelInstanceProperty) {
142
- while (currentNode) {
143
- model = currentNode[modelInstanceProperty];
144
- if (model && modelType && (model.type !== modelType || !(model.fields || []).includes(modelField))) {
145
- model = null;
146
- }
147
- if (model && !model.agFields) {
148
- model = null;
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
- isModelLocal = true;
179
- }
141
+ })();
180
142
  }
181
143
  if (model.isLoaded) {
182
144
  this.setAttribute('is-loaded', '');
@@ -240,9 +202,7 @@ class ModelInput extends SocketConsumer {
240
202
  this.destroy = () => {
241
203
  destroyInputSync();
242
204
  this.isLoadedConsumer && this.isLoadedConsumer.kill();
243
- if (isModelLocal) {
244
- model.destroy();
245
- }
205
+ model.destroy();
246
206
  };
247
207
  }
248
208
 
@@ -304,8 +264,9 @@ class ModelInput extends SocketConsumer {
304
264
  let debounce = debouncer();
305
265
 
306
266
  let onInputChange = (event) => {
307
- if (event.target.value === String(model.value[fieldName])) return;
308
267
  debounce(async () => {
268
+ if (event.target.value === String(model.value[fieldName] || '')) return;
269
+
309
270
  let providerTemplate = this.getAttribute('provider-template');
310
271
  try {
311
272
  if (inputElement.type === 'checkbox') {
@@ -342,6 +303,8 @@ class ModelInput extends SocketConsumer {
342
303
  if (inputElement.type !== 'checkbox' && inputElement.type !== 'select') {
343
304
  onInputKeyUp = async (event) => {
344
305
  debounce(async () => {
306
+ if (event.target.value === String(model.value[fieldName] || '')) return;
307
+
345
308
  let providerTemplate = this.getAttribute('provider-template');
346
309
  try {
347
310
  if (event.target.value === '') {
package/model-text.js CHANGED
@@ -20,10 +20,7 @@ class ModelText extends SocketConsumer {
20
20
 
21
21
  static get observedAttributes() {
22
22
  return [
23
- 'model-instance-property',
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 bindToModel = this.hasAttribute('bind-to-model');
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
- let currentNode = this.parentNode;
51
- let model;
52
- let isModelLocal = false;
53
- if (bindToCollection) {
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
- collection = currentNode[collectionInstanceProperty];
60
- if (collection && collection.type !== modelType) {
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 (collection) {
70
- model = collection.agModels[modelId];
71
- if (!model.agFields[modelField]) {
72
- model.addField(modelField);
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
- if (!model) {
77
- if (bindToModel) {
78
- modelInstanceProperty = 'model';
79
- }
80
- currentNode = this.parentNode;
81
- if (modelInstanceProperty) {
82
- while (currentNode) {
83
- model = currentNode[modelInstanceProperty];
84
- if (model && modelType && (model.type !== modelType || !(model.fields || []).includes(modelField))) {
85
- model = null;
86
- }
87
- if (model && !model.agFields) {
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
- isModelLocal = true;
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
- if (isModelLocal) {
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 && this.isModelLocal) {
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 modelInstanceProperty = this.getAttribute('model-instance-property');
91
+ let socketInstanceProperty = this.getAttribute('socket-instance-property');
97
92
  let hideErrorLogs = this.hasAttribute('hide-error-logs');
98
93
 
99
- if (this.model && this.isModelLocal) this.model.destroy();
94
+ if (this.model) this.model.destroy();
100
95
 
101
- let currentNode = this.parentNode;
102
- let model;
103
- if (modelInstanceProperty) {
96
+ let socket;
97
+ if (socketInstanceProperty) {
98
+ let currentNode = this.parentNode;
104
99
  while (currentNode) {
105
- model = currentNode[modelInstanceProperty];
106
- if (model && modelType && (model.type !== modelType || !(model.fields || []).includes(modelField))) {
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 (!model) {
104
+ if (!socket) {
116
105
  throw new Error(
117
106
  `The ${
118
107
  this.nodeName.toLowerCase()
119
- } element failed to obtain a model via the specified model-instance-property - Ensure that the element is nested inside a parent element which exposes a model instance of the same type which has the relevant fields`
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
- this.model = new AGModel({
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": "2.16.0",
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": "^4.1.0",
21
- "ag-model": "^2.1.0",
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