saasufy-components 2.0.0 → 2.2.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
@@ -76,3 +76,12 @@ A general purpose modal component.
76
76
  ### render-group
77
77
 
78
78
  A component which can be used to guarantee that certain children components are always rendered at the same time (once they are loaded) to provide a smooth user experience.
79
+
80
+ ### conditional-group
81
+
82
+ A modal component which exposes a `show-content` property which can be set to true or false to show or hide its content.
83
+ It's intended to be placed inside a `model-viewer`, `collection-browser` or `collection-reducer` component such that the true/false value of the `show-content` attribute can be computed using a template `{{expression}}` placeholder.
84
+
85
+ ### collection-adder-group
86
+
87
+ A component which can be used to group together multiple `collection-adder` components. It can be used to insert multiple records into a collection via a single button click.
@@ -0,0 +1,42 @@
1
+ class CollectionAdderGroup extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ }
6
+
7
+ connectedCallback() {
8
+ this.shadowRoot.innerHTML = `
9
+ <style>
10
+ .hidden {
11
+ display: none;
12
+ }
13
+ </style>
14
+ <slot name="success" class="hidden"></slot>
15
+ <slot name="error-container" class="hidden"></slot>
16
+ <slot name="collection-adder"></slot>
17
+ <slot name="submit-button"></slot>
18
+ `;
19
+ this.shadowRoot.querySelector('slot[name="submit-button"]').addEventListener('click', async (event) => {
20
+ let collectionAdders = this.shadowRoot.querySelector('slot[name="collection-adder"]').assignedNodes();
21
+ try {
22
+ await Promise.all(
23
+ collectionAdders.map(async (adder) => adder.submit())
24
+ );
25
+ } catch (error) {
26
+ let errorContainerSlot = this.shadowRoot.querySelector('slot[name="error-container"]');
27
+ errorContainerSlot.classList.remove('hidden');
28
+ this.shadowRoot.querySelector('slot[name="success"]').classList.add('hidden');
29
+ let errorContainers = errorContainerSlot.assignedNodes();
30
+ for (let container of errorContainers) {
31
+ container.textContent = error.message;
32
+ }
33
+ return;
34
+ }
35
+ let successSlot = this.shadowRoot.querySelector('slot[name="success"]');
36
+ successSlot.classList.remove('hidden');
37
+ this.shadowRoot.querySelector('slot[name="error-container"]').classList.add('hidden');
38
+ });
39
+ }
40
+ }
41
+
42
+ window.customElements.define('collection-adder-group', CollectionAdderGroup);
@@ -32,8 +32,11 @@ class CollectionAdder extends SocketConsumer {
32
32
  return [
33
33
  'collection-type',
34
34
  'collection-fields',
35
+ 'field-labels',
35
36
  'model-values',
36
- 'submit-button-label'
37
+ 'submit-button-label',
38
+ 'hide-submit-button',
39
+ 'success-message'
37
40
  ];
38
41
  }
39
42
 
@@ -80,22 +83,96 @@ class CollectionAdder extends SocketConsumer {
80
83
  };
81
84
  }
82
85
 
86
+ async submit() {
87
+ if (!this.isReady) {
88
+ throw new Error('Collection adder form is not ready to be submitted');
89
+ }
90
+ let successMessage = this.getAttribute('success-message');
91
+ let messageContainer = this.querySelector('.collection-adder-message-container');
92
+
93
+ messageContainer.classList.remove('success');
94
+ messageContainer.classList.remove('error');
95
+ messageContainer.textContent = '';
96
+
97
+ let newModelData = {
98
+ ...this.modelFieldValues,
99
+ ...Object.fromEntries(
100
+ [...this.querySelectorAll('.collection-adder-input')]
101
+ .filter(input => input.value !== '')
102
+ .map((input) => {
103
+ let fieldType = this.fieldTypes[input.name];
104
+ let Type = this.getTypeCastFunction(fieldType);
105
+ let value;
106
+ if (fieldType === 'checkbox') {
107
+ value = input.checked;
108
+ } else {
109
+ value = input.value;
110
+ }
111
+ return [ input.name, Type(value) ];
112
+ })
113
+ )
114
+ };
115
+
116
+ try {
117
+ await this.collection.create(newModelData);
118
+ messageContainer.classList.add('success');
119
+ messageContainer.classList.remove('error');
120
+ this.dispatchEvent(
121
+ new CustomEvent('success', {
122
+ detail: newModelData
123
+ })
124
+ );
125
+ let form = this.querySelector('.collection-adder-form');
126
+ form.reset();
127
+ if (successMessage) {
128
+ messageContainer.textContent = successMessage;
129
+ } else {
130
+ messageContainer.textContent = '';
131
+ }
132
+ } catch (error) {
133
+ messageContainer.classList.add('error');
134
+ messageContainer.classList.remove('success');
135
+ messageContainer.textContent = error.message;
136
+ this.dispatchEvent(
137
+ new CustomEvent('error', {
138
+ detail: error
139
+ })
140
+ );
141
+ }
142
+ }
143
+
144
+ reset() {
145
+ let form = this.querySelector('.collection-adder-form');
146
+ if (form) {
147
+ form.reset();
148
+ }
149
+ }
150
+
83
151
  render() {
84
152
  let submitButtonLabel = this.getAttribute('submit-button-label');
85
153
  if (submitButtonLabel == null) {
86
154
  submitButtonLabel = 'Submit';
87
155
  }
156
+ let hideSubmitButton = this.hasAttribute('hide-submit-button');
88
157
  let collectionType = this.getAttribute('collection-type');
89
158
  let {
90
159
  fieldNames,
91
160
  fieldTypes
92
161
  } = this.getFieldDetails(this.getAttribute('collection-fields'));
93
162
 
163
+ this.fieldTypes = fieldTypes;
164
+
94
165
  let {
95
166
  fieldNames: modelFieldNames,
96
167
  fieldValues: modelFieldValues
97
168
  } = this.getFieldDetails(this.getAttribute('model-values'));
98
169
 
170
+ let {
171
+ fieldValues: modelFieldLabels
172
+ } = this.getFieldDetails(this.getAttribute('field-labels'));
173
+
174
+ this.modelFieldValues = modelFieldValues;
175
+
99
176
  if (this.collection) this.collection.destroy();
100
177
 
101
178
  this.collection = new AGCollection({
@@ -114,11 +191,12 @@ class CollectionAdder extends SocketConsumer {
114
191
  inputType = inputTypeParts[1];
115
192
  inputTypeParams = inputTypeParts[3] ? inputTypeParts[3].split(',') : [];
116
193
  }
194
+ let inputLabel = modelFieldLabels[field] == null ? field : modelFieldLabels[field];
117
195
  if (inputType === 'checkbox') {
118
196
  items.push(
119
197
  `<label>
120
198
  ${
121
- field
199
+ inputLabel
122
200
  }
123
201
  <input class="collection-adder-input" type="${
124
202
  inputType
@@ -133,7 +211,7 @@ class CollectionAdder extends SocketConsumer {
133
211
  <input class="collection-adder-input" type="text" name="${
134
212
  field
135
213
  }" placeholder="${
136
- field
214
+ inputLabel
137
215
  }" />
138
216
  <select class="collection-adder-select">
139
217
  <option value="" selected disabled hidden>Options</option>
@@ -146,7 +224,7 @@ class CollectionAdder extends SocketConsumer {
146
224
  `<textarea class="collection-adder-input" name="${
147
225
  field
148
226
  }" placeholder="${
149
- field
227
+ inputLabel
150
228
  }"></textarea>`
151
229
  );
152
230
  } else {
@@ -156,7 +234,7 @@ class CollectionAdder extends SocketConsumer {
156
234
  }" name="${
157
235
  field
158
236
  }" placeholder="${
159
- field
237
+ inputLabel
160
238
  }" />`
161
239
  );
162
240
  }
@@ -167,7 +245,7 @@ class CollectionAdder extends SocketConsumer {
167
245
  <div class="collection-adder-message-container"></div>
168
246
  <div class="collection-adder-form-content">
169
247
  ${items.join('')}
170
- <input type="submit" value="${submitButtonLabel}" />
248
+ ${hideSubmitButton ? '' : `<input type="submit" value="${submitButtonLabel}" />`}
171
249
  </div>
172
250
  </form>
173
251
  `;
@@ -182,54 +260,10 @@ class CollectionAdder extends SocketConsumer {
182
260
  });
183
261
  }
184
262
 
185
- let messageContainer = this.querySelector('.collection-adder-message-container');
186
-
187
263
  let form = this.querySelector('.collection-adder-form');
188
264
  form.addEventListener('submit', async (event) => {
189
265
  event.preventDefault();
190
- messageContainer.classList.remove('success');
191
- messageContainer.classList.remove('error');
192
- messageContainer.textContent = '';
193
-
194
- let newModelData = {
195
- ...modelFieldValues,
196
- ...Object.fromEntries(
197
- [...event.target.querySelectorAll('.collection-adder-input')]
198
- .filter(input => input.value !== '')
199
- .map((input) => {
200
- let fieldType = fieldTypes[input.name];
201
- let Type = this.getTypeCastFunction(fieldType);
202
- let value;
203
- if (fieldType === 'checkbox') {
204
- value = input.checked;
205
- } else {
206
- value = input.value;
207
- }
208
- return [ input.name, Type(value) ];
209
- })
210
- )
211
- };
212
-
213
- try {
214
- await this.collection.create(newModelData);
215
- messageContainer.classList.add('success');
216
- messageContainer.classList.remove('error');
217
- this.dispatchEvent(
218
- new CustomEvent('success', {
219
- detail: newModelData
220
- })
221
- );
222
- form.reset();
223
- } catch (error) {
224
- messageContainer.classList.add('error');
225
- messageContainer.classList.remove('success');
226
- messageContainer.textContent = error.message;
227
- this.dispatchEvent(
228
- new CustomEvent('error', {
229
- detail: error
230
- })
231
- );
232
- }
266
+ await this.submit();
233
267
  });
234
268
  }
235
269
  }
@@ -0,0 +1,30 @@
1
+ class ConditionalGroup extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ this.isReady = false;
6
+ }
7
+
8
+ static get observedAttributes() {
9
+ return [
10
+ 'show-content',
11
+ ];
12
+ }
13
+
14
+ attributeChangedCallback() {
15
+ if (!this.isReady) return;
16
+ this.render();
17
+ }
18
+
19
+ connectedCallback() {
20
+ this.isReady = true;
21
+ this.render();
22
+ }
23
+
24
+ render() {
25
+ let showContent = this.getAttribute('show-content');
26
+ this.shadowRoot.innerHTML = showContent != null && showContent !== 'false' ? '<slot></slot>' : '';
27
+ }
28
+ }
29
+
30
+ window.customElements.define('conditional-group', ConditionalGroup);
package/model-viewer.js CHANGED
@@ -21,7 +21,13 @@ class ModelViewer extends SocketConsumer {
21
21
  }
22
22
 
23
23
  disconnectedCallback() {
24
- if (this.model) this.model.destroy();
24
+ if (this.model && this.isModelLocal) {
25
+ this.model.destroy();
26
+ } else {
27
+ if (this.modelLoadConsumer) this.modelLoadConsumer.kill();
28
+ if (this.modelChangeConsumer) this.modelChangeConsumer.kill();
29
+ if (this.modelErrorConsumer) this.modelErrorConsumer.kill();
30
+ }
25
31
  this.shadowRoot.removeEventListener('slotchange', this.handleSlotChangeEvent);
26
32
  }
27
33
 
@@ -85,16 +91,40 @@ class ModelViewer extends SocketConsumer {
85
91
  let modelType = this.getAttribute('model-type');
86
92
  let modelId = this.getAttribute('model-id');
87
93
  let modelFields = this.getAttribute('model-fields') || '';
94
+ let modelInstanceProperty = this.getAttribute('model-instance-property');
88
95
  let hideErrorLogs = this.hasAttribute('hide-error-logs');
89
96
 
90
- if (this.model) this.model.destroy();
97
+ if (this.model && this.isModelLocal) this.model.destroy();
91
98
 
92
- this.model = new AGModel({
93
- socket: this.socket,
94
- type: modelType,
95
- id: modelId,
96
- fields: modelFields.split(',').map(field => field.trim()).filter(field => field)
97
- });
99
+ let currentNode = this.parentNode;
100
+ let model;
101
+ if (modelInstanceProperty) {
102
+ while (currentNode) {
103
+ model = currentNode[modelInstanceProperty];
104
+ if (model && modelType && (model.type !== modelType || !(model.fields || []).includes(modelField))) {
105
+ model = null;
106
+ }
107
+ if (model) break;
108
+ currentNode = currentNode.getRootNode().host || currentNode.parentNode;
109
+ }
110
+ if (!model) {
111
+ throw new Error(
112
+ `The ${
113
+ this.nodeName.toLowerCase()
114
+ } 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 field`
115
+ );
116
+ };
117
+ this.model = model;
118
+ this.isModelLocal = false;
119
+ } else {
120
+ this.model = new AGModel({
121
+ socket: this.socket,
122
+ type: modelType,
123
+ id: modelId,
124
+ fields: modelFields.split(',').map(field => field.trim()).filter(field => field)
125
+ });
126
+ this.isModelLocal = true;
127
+ }
98
128
 
99
129
  this.shadowRoot.innerHTML = `
100
130
  <style>
@@ -109,7 +139,9 @@ class ModelViewer extends SocketConsumer {
109
139
  `;
110
140
 
111
141
  (async () => {
112
- await this.model.listener('load').once();
142
+ this.modelLoadConsumer = this.model.listener('load').createConsumer();
143
+ let packet = await this.modelLoadConsumer.next();
144
+ if (packet.done) return;
113
145
  if (!this.modelValueExists(this.model.value)) {
114
146
  this.renderItem();
115
147
  }
@@ -117,15 +149,23 @@ class ModelViewer extends SocketConsumer {
117
149
  this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
118
150
  })();
119
151
 
152
+ if (this.model.isLoaded) {
153
+ this.renderItem();
154
+ this.setAttribute('is-loaded', '');
155
+ this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
156
+ }
157
+
120
158
  (async () => {
121
- for await (let event of this.model.listener('change')) {
159
+ this.modelChangeConsumer = this.model.listener('change').createConsumer();
160
+ for await (let event of this.modelChangeConsumer) {
122
161
  this.renderItem();
123
162
  }
124
163
  })();
125
164
 
126
165
  if (!hideErrorLogs) {
127
166
  (async () => {
128
- for await (let { error } of this.model.listener('error')) {
167
+ this.modelErrorConsumer = this.model.listener('error').createConsumer();
168
+ for await (let { error } of this.modelErrorConsumer) {
129
169
  console.error(
130
170
  `Model viewer encountered an error: ${error.message}`
131
171
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {