saasufy-components 3.39.0 → 4.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
@@ -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,209 @@
1
+ import { SocketConsumer } from './socket.js';
2
+ import { convertStringToFieldParams, getTypeCastFunction } from './utils.js';
3
+ import AGCollection from '/node_modules/ag-collection/ag-collection.js';
4
+
5
+ class CollectionAdderForm extends SocketConsumer {
6
+ constructor(options) {
7
+ super();
8
+ this.attachShadow({
9
+ mode: 'open'
10
+ });
11
+ this.options = options || {};
12
+ this.isReady = false;
13
+ }
14
+
15
+ connectedCallback() {
16
+ this.socket = this.getSocket();
17
+ this.isReady = true;
18
+ this.render();
19
+ }
20
+
21
+ disconnectedCallback() {
22
+ if (this.collection) this.collection.destroy();
23
+ }
24
+
25
+ static get observedAttributes() {
26
+ return [
27
+ 'collection-type',
28
+ 'model-values',
29
+ 'success-message',
30
+ 'trim-spaces'
31
+ ];
32
+ }
33
+
34
+ attributeChangedCallback(name, oldValue, newValue) {
35
+ if (!this.isReady) return;
36
+ this.render();
37
+ }
38
+
39
+ async submit() {
40
+ if (!this.isReady) {
41
+ throw new Error('Collection adder form is not ready to be submitted');
42
+ }
43
+ let successMessage = this.getAttribute('success-message');
44
+ let messageContainer = this.shadowRoot.querySelector('slot[name="message"]').assignedElements()[0];
45
+
46
+ if (messageContainer) {
47
+ messageContainer.classList.remove('success');
48
+ messageContainer.classList.remove('error');
49
+ messageContainer.textContent = '';
50
+ }
51
+
52
+ let trimSpaces = this.hasAttribute('trim-spaces');
53
+
54
+ let inputElements = this.getAllInputElements();
55
+
56
+ try {
57
+ let newModelData = {
58
+ ...this.modelFieldValues,
59
+ ...Object.fromEntries(
60
+ await Promise.all(
61
+ [ ...inputElements ].filter(
62
+ input => input.type === 'radio' ? input.checked : input.value !== ''
63
+ )
64
+ .map(
65
+ async (input) => {
66
+ let inputType;
67
+ if (input.nodeName === 'TEXTAREA' || input.nodeName === 'SELECT') {
68
+ inputType = input.nodeName.toLowerCase();
69
+ } else {
70
+ inputType = input.type;
71
+ }
72
+ let Type = getTypeCastFunction(inputType);
73
+ let value;
74
+ if (inputType === 'file' && input.files && input.files.length) {
75
+ let reader = new FileReader();
76
+ let readerLoadPromise = new Promise((resolve, reject) => {
77
+ reader.addEventListener('load', () => {
78
+ resolve(reader.result);
79
+ });
80
+ reader.addEventListener('error', () => {
81
+ reject(new Error('Failed to read file'));
82
+ });
83
+ });
84
+ reader.readAsDataURL(input.files[0]);
85
+ value = await readerLoadPromise;
86
+ } else if (inputType === 'checkbox') {
87
+ value = input.checked;
88
+ } else {
89
+ value = input.value;
90
+ }
91
+ let sanitizedValue = Type(value);
92
+ if (trimSpaces && typeof sanitizedValue === 'string') {
93
+ sanitizedValue = sanitizedValue.trim();
94
+ }
95
+ return [ input.name, sanitizedValue ];
96
+ }
97
+ )
98
+ )
99
+ )
100
+ };
101
+
102
+ await this.collection.create(newModelData);
103
+ this.dispatchEvent(
104
+ new CustomEvent('success', {
105
+ detail: newModelData
106
+ })
107
+ );
108
+ this.reset();
109
+
110
+ messageContainer = this.shadowRoot.querySelector('slot[name="message"]').assignedElements()[0];
111
+ if (messageContainer) {
112
+ messageContainer.classList.add('success');
113
+ messageContainer.classList.remove('error');
114
+ if (successMessage) {
115
+ messageContainer.textContent = successMessage;
116
+ } else {
117
+ messageContainer.textContent = '';
118
+ }
119
+ }
120
+ } catch (error) {
121
+ messageContainer = this.shadowRoot.querySelector('slot[name="message"]').assignedElements()[0];
122
+ if (messageContainer) {
123
+ messageContainer.classList.add('error');
124
+ messageContainer.classList.remove('success');
125
+ messageContainer.textContent = error.message;
126
+ }
127
+ this.dispatchEvent(
128
+ new CustomEvent('error', {
129
+ detail: error
130
+ })
131
+ );
132
+ }
133
+ }
134
+
135
+ getAllInputElements() {
136
+ return this.shadowRoot.querySelector('.input-slot')
137
+ .assignedElements()
138
+ .filter(element => element)
139
+ .flatMap(element => [ element, ...element.querySelectorAll('input,textarea,select') ])
140
+ .filter(
141
+ (element) => {
142
+ return (element.nodeName === 'INPUT' && element.type !== 'submit' && element.type !== 'button') ||
143
+ element.nodeName === 'TEXTAREA' || element.nodeName === 'SELECT';
144
+ }
145
+ );
146
+ }
147
+
148
+ reset() {
149
+ let inputElements = this.getAllInputElements();
150
+ for (let element of inputElements) {
151
+ if (element.nodeName === 'INPUT') {
152
+ if (element.type === 'checkbox' || element.type === 'radio') {
153
+ element.checked = false;
154
+ } else {
155
+ element.value = '';
156
+ }
157
+ } else if (element.nodeName === 'TEXTAREA') {
158
+ element.value = '';
159
+ } else if (element.nodeName === 'SELECT') {
160
+ element.selectedIndex = 0;
161
+ }
162
+ }
163
+ }
164
+
165
+ render() {
166
+ let collectionType = this.getAttribute('collection-type');
167
+
168
+ let {
169
+ fieldTypes: modelFieldTypes,
170
+ fieldValues: modelFieldValues
171
+ } = convertStringToFieldParams(this.getAttribute('model-values'));
172
+
173
+ this.modelFieldValues = Object.fromEntries(
174
+ Object.entries(modelFieldValues).map(([key, value]) => {
175
+ if (modelFieldTypes[key] === 'boolean') {
176
+ return [ key, value !== 'false' && value !== '' ];
177
+ }
178
+ if (modelFieldTypes[key] === 'number') {
179
+ return [ key, Number(value) ];
180
+ }
181
+ return [ key, value ];
182
+ })
183
+ );
184
+
185
+ if (this.collection) this.collection.destroy();
186
+
187
+ this.collection = new AGCollection({
188
+ socket: this.socket,
189
+ type: collectionType,
190
+ fields: [],
191
+ writeOnly: true
192
+ });
193
+
194
+ this.shadowRoot.innerHTML = `
195
+ <slot name="message" class="collection-adder-message-container"></slot>
196
+ <slot class="input-slot"></slot>
197
+ `;
198
+
199
+ let inputSlot = this.shadowRoot.querySelector('.input-slot');
200
+ inputSlot.addEventListener('click', async (event) => {
201
+ let { target } = event;
202
+ if (target && target.nodeName === 'INPUT' && target.type === 'submit') {
203
+ await this.submit();
204
+ }
205
+ });
206
+ }
207
+ }
208
+
209
+ window.customElements.define('collection-adder-form', CollectionAdderForm);
@@ -53,7 +53,7 @@ class CollectionAdder extends SocketConsumer {
53
53
  messageContainer.classList.remove('error');
54
54
  messageContainer.textContent = '';
55
55
 
56
- let radioInputs = [...this.querySelectorAll('.collection-adder-radio')];
56
+ let radioInputs = [ ...this.querySelectorAll('.collection-adder-radio') ];
57
57
  let radioData = {};
58
58
  for (let radio of radioInputs) {
59
59
  if (radio.checked) {
@@ -69,7 +69,7 @@ class CollectionAdder extends SocketConsumer {
69
69
  ...radioData,
70
70
  ...Object.fromEntries(
71
71
  await Promise.all(
72
- [...this.querySelectorAll('.collection-adder-input')].filter(
72
+ [ ...this.querySelectorAll('.collection-adder-input') ].filter(
73
73
  input => input.value !== ''
74
74
  )
75
75
  .map(
@@ -192,7 +192,7 @@ class CollectionAdder extends SocketConsumer {
192
192
  this.collection = new AGCollection({
193
193
  socket: this.socket,
194
194
  type: collectionType,
195
- fields: [...new Set([ ...fieldNames, ...modelFieldNames ])],
195
+ fields: [ ...new Set([ ...fieldNames, ...modelFieldNames ]) ],
196
196
  writeOnly: true
197
197
  });
198
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "3.39.0",
3
+ "version": "4.0.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {