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 +41 -1
- package/collection-adder-form.js +213 -0
- package/collection-adder.js +9 -5
- package/model-input.js +7 -2
- package/package.json +2 -2
- package/utils.js +4 -0
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);
|
package/collection-adder.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { SocketConsumer } from './socket.js';
|
|
2
|
-
import {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
+
"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
|
|
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
|
}
|