saasufy-components 3.39.0 → 4.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
@@ -8,6 +8,7 @@ Components for Saasufy
8
8
  ### Collections of resources
9
9
  - [collection-browser](#collection-browser)
10
10
  - [collection-adder](#collection-adder)
11
+ - [collection-adder-form](#collection-adder-form)
11
12
  - [collection-deleter](#collection-deleter)
12
13
  - [collection-reducer](#collection-reducer)
13
14
 
@@ -314,7 +315,8 @@ Can also perform basic CRUD operations such as deleting or creating records by l
314
315
 
315
316
  ### collection-adder
316
317
 
317
- A form component for inserting data into collections.
318
+ A basic form component for inserting data into collections.
319
+ This component is a simplified version of the `collection-adder-form` component as it doesn't require input elements to be slotted into it.
318
320
 
319
321
  **Import**
320
322
 
@@ -347,6 +349,44 @@ A form component for inserting data into collections.
347
349
  - `autocorrect`: Can be set to `off` to disable auto-correction of input on mobile devices.
348
350
  - `trim-spaces`: If this attribute exists on the element, then leading and trailing spaces will be trimmed from each input element's value before submitting the form.
349
351
 
352
+ ### collection-adder-form
353
+
354
+ A flexible form component for inserting data into collections.
355
+ This component is similar to the `collection-adder` component except that it requires input elements (and others) to be slotted into it.
356
+
357
+ **Import**
358
+
359
+ ```html
360
+ <script src="https://saasufy.com/node_modules/saasufy-components/collection-adder-form.js" type="module" defer></script>
361
+ ```
362
+
363
+ **Example usage**
364
+
365
+ ```html
366
+ <collection-adder-form
367
+ collection-type="Product"
368
+ model-values="categoryName=electronics,isNew:boolean=true"
369
+ trim-spaces
370
+ >
371
+ <div slot="message"></div>
372
+ <input type="text" name="name" placeholder="Name">
373
+ <input type="text" name="brand" placeholder="Brand">
374
+ <input type="number" name="qty" placeholder="Quantity">
375
+ <input type="submit" value="Add new product">
376
+ </collection-adder-form>
377
+ ```
378
+
379
+ **Attributes of collection-adder-form**
380
+
381
+ - `collection-type` (required): Specifies the type of collection to add the resource to when the form is submitted. This should match a `Model` available in your Saasufy service.
382
+ - `model-values`: An optional list of key-value pairs in the format `field1=value1,field2=value2` to add to the newly created resource alongside the values collected from the user via the form. For non-string values, the type should be provided in the format `fieldName:type=value`; supported types are `string`, `number` and `boolean`. Unlike `collection-fields` which are rendered as input elements in the form, `model-values` are not shown to the user.
383
+ - `success-message`: A message to show the user if the resource has been successfully added to the collection after submitting the form.
384
+ - `trim-spaces`: If this attribute exists on the element, then leading and trailing spaces will be trimmed from each input element's value before submitting the form.
385
+
386
+ **Attributes of slotted input, select, textarea... elements**
387
+
388
+ - `name` (required): This must correspond to a field name on the model specified via the `collection-type` attribute of the `collection-adder-form`.
389
+
350
390
  ### collection-deleter
351
391
 
352
392
  A component which can be placed anywhere inside a `collection-browser` component to delete a specific item from a collection as a result of a user action (e.g. on click).
@@ -0,0 +1,213 @@
1
+ import { SocketConsumer } from './socket.js';
2
+ import {
3
+ convertStringToFieldParams,
4
+ getTypeCastFunction,
5
+ formatError
6
+ } from './utils.js';
7
+ import AGCollection from '/node_modules/ag-collection/ag-collection.js';
8
+
9
+ class CollectionAdderForm extends SocketConsumer {
10
+ constructor(options) {
11
+ super();
12
+ this.attachShadow({
13
+ mode: 'open'
14
+ });
15
+ this.options = options || {};
16
+ this.isReady = false;
17
+ }
18
+
19
+ connectedCallback() {
20
+ this.socket = this.getSocket();
21
+ this.isReady = true;
22
+ this.render();
23
+ }
24
+
25
+ disconnectedCallback() {
26
+ if (this.collection) this.collection.destroy();
27
+ }
28
+
29
+ static get observedAttributes() {
30
+ return [
31
+ 'collection-type',
32
+ 'model-values',
33
+ 'success-message',
34
+ 'trim-spaces'
35
+ ];
36
+ }
37
+
38
+ attributeChangedCallback(name, oldValue, newValue) {
39
+ if (!this.isReady) return;
40
+ this.render();
41
+ }
42
+
43
+ async submit() {
44
+ if (!this.isReady) {
45
+ throw new Error('Collection adder form is not ready to be submitted');
46
+ }
47
+ let successMessage = this.getAttribute('success-message');
48
+ let messageContainer = this.shadowRoot.querySelector('slot[name="message"]').assignedElements()[0];
49
+
50
+ if (messageContainer) {
51
+ messageContainer.classList.remove('success');
52
+ messageContainer.classList.remove('error');
53
+ messageContainer.textContent = '';
54
+ }
55
+
56
+ let trimSpaces = this.hasAttribute('trim-spaces');
57
+
58
+ let inputElements = this.getAllInputElements();
59
+
60
+ try {
61
+ let newModelData = {
62
+ ...this.modelFieldValues,
63
+ ...Object.fromEntries(
64
+ await Promise.all(
65
+ [ ...inputElements ].filter(
66
+ input => input.type === 'radio' ? input.checked : input.value !== ''
67
+ )
68
+ .map(
69
+ async (input) => {
70
+ let inputType;
71
+ if (input.nodeName === 'TEXTAREA' || input.nodeName === 'SELECT') {
72
+ inputType = input.nodeName.toLowerCase();
73
+ } else {
74
+ inputType = input.type;
75
+ }
76
+ let Type = getTypeCastFunction(inputType);
77
+ let value;
78
+ if (inputType === 'file' && input.files && input.files.length) {
79
+ let reader = new FileReader();
80
+ let readerLoadPromise = new Promise((resolve, reject) => {
81
+ reader.addEventListener('load', () => {
82
+ resolve(reader.result);
83
+ });
84
+ reader.addEventListener('error', () => {
85
+ reject(new Error('Failed to read file'));
86
+ });
87
+ });
88
+ reader.readAsDataURL(input.files[0]);
89
+ value = await readerLoadPromise;
90
+ } else if (inputType === 'checkbox') {
91
+ value = input.checked;
92
+ } else {
93
+ value = input.value;
94
+ }
95
+ let sanitizedValue = Type(value);
96
+ if (trimSpaces && typeof sanitizedValue === 'string') {
97
+ sanitizedValue = sanitizedValue.trim();
98
+ }
99
+ return [ input.name, sanitizedValue ];
100
+ }
101
+ )
102
+ )
103
+ )
104
+ };
105
+
106
+ await this.collection.create(newModelData);
107
+ this.dispatchEvent(
108
+ new CustomEvent('success', {
109
+ detail: newModelData
110
+ })
111
+ );
112
+ this.reset();
113
+
114
+ messageContainer = this.shadowRoot.querySelector('slot[name="message"]').assignedElements()[0];
115
+ if (messageContainer) {
116
+ messageContainer.classList.add('success');
117
+ messageContainer.classList.remove('error');
118
+ if (successMessage) {
119
+ messageContainer.textContent = successMessage;
120
+ } else {
121
+ messageContainer.textContent = '';
122
+ }
123
+ }
124
+ } catch (error) {
125
+ messageContainer = this.shadowRoot.querySelector('slot[name="message"]').assignedElements()[0];
126
+ if (messageContainer) {
127
+ messageContainer.classList.add('error');
128
+ messageContainer.classList.remove('success');
129
+ messageContainer.textContent = formatError(error);
130
+ }
131
+ this.dispatchEvent(
132
+ new CustomEvent('error', {
133
+ detail: error
134
+ })
135
+ );
136
+ }
137
+ }
138
+
139
+ getAllInputElements() {
140
+ return this.shadowRoot.querySelector('.input-slot')
141
+ .assignedElements()
142
+ .filter(element => element)
143
+ .flatMap(element => [ element, ...element.querySelectorAll('input,textarea,select') ])
144
+ .filter(
145
+ (element) => {
146
+ return (element.nodeName === 'INPUT' && element.type !== 'submit' && element.type !== 'button') ||
147
+ element.nodeName === 'TEXTAREA' || element.nodeName === 'SELECT';
148
+ }
149
+ );
150
+ }
151
+
152
+ reset() {
153
+ let inputElements = this.getAllInputElements();
154
+ for (let element of inputElements) {
155
+ if (element.nodeName === 'INPUT') {
156
+ if (element.type === 'checkbox' || element.type === 'radio') {
157
+ element.checked = false;
158
+ } else {
159
+ element.value = '';
160
+ }
161
+ } else if (element.nodeName === 'TEXTAREA') {
162
+ element.value = '';
163
+ } else if (element.nodeName === 'SELECT') {
164
+ element.selectedIndex = 0;
165
+ }
166
+ }
167
+ }
168
+
169
+ render() {
170
+ let collectionType = this.getAttribute('collection-type');
171
+
172
+ let {
173
+ fieldTypes: modelFieldTypes,
174
+ fieldValues: modelFieldValues
175
+ } = convertStringToFieldParams(this.getAttribute('model-values'));
176
+
177
+ this.modelFieldValues = Object.fromEntries(
178
+ Object.entries(modelFieldValues).map(([key, value]) => {
179
+ if (modelFieldTypes[key] === 'boolean') {
180
+ return [ key, value !== 'false' && value !== '' ];
181
+ }
182
+ if (modelFieldTypes[key] === 'number') {
183
+ return [ key, Number(value) ];
184
+ }
185
+ return [ key, value ];
186
+ })
187
+ );
188
+
189
+ if (this.collection) this.collection.destroy();
190
+
191
+ this.collection = new AGCollection({
192
+ socket: this.socket,
193
+ type: collectionType,
194
+ fields: [],
195
+ writeOnly: true
196
+ });
197
+
198
+ this.shadowRoot.innerHTML = `
199
+ <slot name="message" class="collection-adder-message-container"></slot>
200
+ <slot class="input-slot"></slot>
201
+ `;
202
+
203
+ let inputSlot = this.shadowRoot.querySelector('.input-slot');
204
+ inputSlot.addEventListener('click', async (event) => {
205
+ let { target } = event;
206
+ if (target && target.nodeName === 'INPUT' && target.type === 'submit') {
207
+ await this.submit();
208
+ }
209
+ });
210
+ }
211
+ }
212
+
213
+ window.customElements.define('collection-adder-form', CollectionAdderForm);
@@ -1,5 +1,9 @@
1
1
  import { SocketConsumer } from './socket.js';
2
- import { convertStringToFieldParams, getTypeCastFunction } from './utils.js';
2
+ import {
3
+ convertStringToFieldParams,
4
+ getTypeCastFunction,
5
+ formatError
6
+ } from './utils.js';
3
7
  import AGCollection from '/node_modules/ag-collection/ag-collection.js';
4
8
 
5
9
  class CollectionAdder extends SocketConsumer {
@@ -53,7 +57,7 @@ class CollectionAdder extends SocketConsumer {
53
57
  messageContainer.classList.remove('error');
54
58
  messageContainer.textContent = '';
55
59
 
56
- let radioInputs = [...this.querySelectorAll('.collection-adder-radio')];
60
+ let radioInputs = [ ...this.querySelectorAll('.collection-adder-radio') ];
57
61
  let radioData = {};
58
62
  for (let radio of radioInputs) {
59
63
  if (radio.checked) {
@@ -69,7 +73,7 @@ class CollectionAdder extends SocketConsumer {
69
73
  ...radioData,
70
74
  ...Object.fromEntries(
71
75
  await Promise.all(
72
- [...this.querySelectorAll('.collection-adder-input')].filter(
76
+ [ ...this.querySelectorAll('.collection-adder-input') ].filter(
73
77
  input => input.value !== ''
74
78
  )
75
79
  .map(
@@ -122,7 +126,7 @@ class CollectionAdder extends SocketConsumer {
122
126
  } catch (error) {
123
127
  messageContainer.classList.add('error');
124
128
  messageContainer.classList.remove('success');
125
- messageContainer.textContent = error.message;
129
+ messageContainer.textContent = formatError(error);
126
130
  this.dispatchEvent(
127
131
  new CustomEvent('error', {
128
132
  detail: error
@@ -192,7 +196,7 @@ class CollectionAdder extends SocketConsumer {
192
196
  this.collection = new AGCollection({
193
197
  socket: this.socket,
194
198
  type: collectionType,
195
- fields: [...new Set([ ...fieldNames, ...modelFieldNames ])],
199
+ fields: [ ...new Set([ ...fieldNames, ...modelFieldNames ]) ],
196
200
  writeOnly: true
197
201
  });
198
202
 
package/model-input.js CHANGED
@@ -1,5 +1,10 @@
1
1
  import { SocketConsumer } from './socket.js';
2
- import { debouncer, updateConsumerElements, convertStringToFieldParams } from './utils.js';
2
+ import {
3
+ debouncer,
4
+ updateConsumerElements,
5
+ convertStringToFieldParams,
6
+ formatError
7
+ } from './utils.js';
3
8
  import AGModel from '/node_modules/ag-model/ag-model.js';
4
9
 
5
10
  class ModelInput extends SocketConsumer {
@@ -379,7 +384,7 @@ class ModelInput extends SocketConsumer {
379
384
  } catch (error) {
380
385
  inputElement.classList.add(errorStyleClass);
381
386
  inputElement.classList.remove(successStyleClass);
382
- showErrorMessage(fieldName, error.message);
387
+ showErrorMessage(fieldName, formatError(error));
383
388
  }
384
389
  }, debounceDelay);
385
390
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "3.39.0",
3
+ "version": "4.1.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -20,7 +20,7 @@
20
20
  "ag-collection": "^5.2.0",
21
21
  "ag-model": "^3.1.0",
22
22
  "capitalisk-auth-client": "^1.5.1",
23
- "socketcluster-client": "^19.1.2",
23
+ "socketcluster-client": "^19.2.1",
24
24
  "uuid": "^8.3.2"
25
25
  },
26
26
  "bugs": {
package/utils.js CHANGED
@@ -422,6 +422,10 @@ export function convertStringToFieldParams(string) {
422
422
  };
423
423
  }
424
424
 
425
+ export function formatError(error) {
426
+ return error.code === 1009 ? 'Resource exceeded the maximum size' : error.message;
427
+ }
428
+
425
429
  export function wait(duration) {
426
430
  return new Promise((resolve) => setTimeout(resolve, duration));
427
431
  }