saasufy-components 2.0.0 → 2.1.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);
@@ -33,7 +33,9 @@ class CollectionAdder extends SocketConsumer {
33
33
  'collection-type',
34
34
  'collection-fields',
35
35
  'model-values',
36
- 'submit-button-label'
36
+ 'submit-button-label',
37
+ 'hide-submit-button',
38
+ 'success-message'
37
39
  ];
38
40
  }
39
41
 
@@ -80,22 +82,92 @@ class CollectionAdder extends SocketConsumer {
80
82
  };
81
83
  }
82
84
 
85
+ async submit() {
86
+ if (!this.isReady) {
87
+ throw new Error('Collection adder form is not ready to be submitted');
88
+ }
89
+ let successMessage = this.getAttribute('success-message');
90
+ let messageContainer = this.querySelector('.collection-adder-message-container');
91
+
92
+ messageContainer.classList.remove('success');
93
+ messageContainer.classList.remove('error');
94
+ messageContainer.textContent = '';
95
+
96
+ let newModelData = {
97
+ ...this.modelFieldValues,
98
+ ...Object.fromEntries(
99
+ [...this.querySelectorAll('.collection-adder-input')]
100
+ .filter(input => input.value !== '')
101
+ .map((input) => {
102
+ let fieldType = this.fieldTypes[input.name];
103
+ let Type = this.getTypeCastFunction(fieldType);
104
+ let value;
105
+ if (fieldType === 'checkbox') {
106
+ value = input.checked;
107
+ } else {
108
+ value = input.value;
109
+ }
110
+ return [ input.name, Type(value) ];
111
+ })
112
+ )
113
+ };
114
+
115
+ try {
116
+ await this.collection.create(newModelData);
117
+ messageContainer.classList.add('success');
118
+ messageContainer.classList.remove('error');
119
+ this.dispatchEvent(
120
+ new CustomEvent('success', {
121
+ detail: newModelData
122
+ })
123
+ );
124
+ let form = this.querySelector('.collection-adder-form');
125
+ form.reset();
126
+ if (successMessage) {
127
+ messageContainer.textContent = successMessage;
128
+ } else {
129
+ messageContainer.textContent = '';
130
+ }
131
+ } catch (error) {
132
+ messageContainer.classList.add('error');
133
+ messageContainer.classList.remove('success');
134
+ messageContainer.textContent = error.message;
135
+ this.dispatchEvent(
136
+ new CustomEvent('error', {
137
+ detail: error
138
+ })
139
+ );
140
+ }
141
+ }
142
+
143
+ reset() {
144
+ let form = this.querySelector('.collection-adder-form');
145
+ if (form) {
146
+ form.reset();
147
+ }
148
+ }
149
+
83
150
  render() {
84
151
  let submitButtonLabel = this.getAttribute('submit-button-label');
85
152
  if (submitButtonLabel == null) {
86
153
  submitButtonLabel = 'Submit';
87
154
  }
155
+ let hideSubmitButton = this.hasAttribute('hide-submit-button');
88
156
  let collectionType = this.getAttribute('collection-type');
89
157
  let {
90
158
  fieldNames,
91
159
  fieldTypes
92
160
  } = this.getFieldDetails(this.getAttribute('collection-fields'));
93
161
 
162
+ this.fieldTypes = fieldTypes;
163
+
94
164
  let {
95
165
  fieldNames: modelFieldNames,
96
166
  fieldValues: modelFieldValues
97
167
  } = this.getFieldDetails(this.getAttribute('model-values'));
98
168
 
169
+ this.modelFieldValues = modelFieldValues;
170
+
99
171
  if (this.collection) this.collection.destroy();
100
172
 
101
173
  this.collection = new AGCollection({
@@ -167,7 +239,7 @@ class CollectionAdder extends SocketConsumer {
167
239
  <div class="collection-adder-message-container"></div>
168
240
  <div class="collection-adder-form-content">
169
241
  ${items.join('')}
170
- <input type="submit" value="${submitButtonLabel}" />
242
+ ${hideSubmitButton ? '' : `<input type="submit" value="${submitButtonLabel}" />`}
171
243
  </div>
172
244
  </form>
173
245
  `;
@@ -182,54 +254,10 @@ class CollectionAdder extends SocketConsumer {
182
254
  });
183
255
  }
184
256
 
185
- let messageContainer = this.querySelector('.collection-adder-message-container');
186
-
187
257
  let form = this.querySelector('.collection-adder-form');
188
258
  form.addEventListener('submit', async (event) => {
189
259
  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
- }
260
+ await this.submit();
233
261
  });
234
262
  }
235
263
  }
@@ -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.1.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {