saasufy-components 9.0.4 → 9.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
@@ -404,7 +404,7 @@ In addition to the slots shown above, the `collection-viewer` also supports the
404
404
  **Attributes**
405
405
 
406
406
  - `collection-type` (required): Specifies the type of collection to display. This should match a `Model` available in your Saasufy service.
407
- - `collection-fields` (required): A comma-separated list of fields from the `Model` that you want to display or use. This lets you pick specific pieces of data from your collection to work with.
407
+ - `collection-fields` (required): A comma-separated list of fields from the `Model` that you want to display or use. This lets you pick specific pieces of data from your collection to work with. Note that updates made to these fields can cause the `collection-viewer` to re-render itself so you should consider whether or not a field needs to be specified at this level in the component hierarchy or should be delegated to a child component (otherwise it could cause nested `model-input` components to lose focus).
408
408
  - `collection-view` (required): Determines the view of the collection. This should match one of the `Views` defined in your Saasufy service under that specific `Model`.
409
409
  - `collection-view-params` (required): Parameters for the view, specified as comma-separated key-value pairs (e.g., key1=value1,key2=value2). These parameters can customize the behavior of the collection view. The keys must match `paramFields` specified in your Saasufy service under the relevant `View`.
410
410
  - `collection-page-size`: Sets how many items from the collection are displayed at once. This is useful for pagination, allowing users to navigate through large sets of data in chunks.
@@ -437,6 +437,7 @@ This component is a simplified version of the `collection-adder-form` component
437
437
  collection-fields="name,brand,qty"
438
438
  model-values="categoryName=electronics,isNew:boolean=true"
439
439
  submit-button-label="Create"
440
+ show-field-errors
440
441
  trim-spaces
441
442
  ></collection-adder>
442
443
  ```
@@ -453,6 +454,7 @@ This component is a simplified version of the `collection-adder-form` component
453
454
  - `success-message`: A message to show the user if the resource has been successfully added to the collection after submitting the form.
454
455
  - `autocapitalize`: Can be set to `off` to disable auto-capitalization of the first input character on mobile devices.
455
456
  - `autocorrect`: Can be set to `off` to disable auto-correction of input on mobile devices.
457
+ - `show-field-errors`: If this attribute exists on the element, then granular error messages will be shown above each corresponding input element instead of a single form-level error message.
456
458
  - `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.
457
459
  - `consumers`: This allows you to connect this `collection-adder` to other elements on your page. It should be used in conjunction with the `provider-template` attribute. The `consumers` attribute takes a list of selectors with optional attributes to target in the format `element-selector:attribute-name`. For example `.my-input:value` will find all elements with a `my-input` class and update their `value` attributes with the output of the `collection-adder` component on submit. You can specify multiple selectors separated by commas such as `.my-input,my-div`; in this case, because attribute names are not specified, values will be injected into the `value` attribute (for input elements) or into the `innerHTML` property (for other kinds of elements). The default attribute/property depends on the element type.
458
460
  - `success-consumers`: Same as the `consumers` attribute but the target elements are only updated if insertion was a success.
@@ -492,6 +494,7 @@ This component is similar to the `collection-adder` component except that it req
492
494
  - `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.
493
495
  - `computable-model-values`: If this attribute exists on the element, then the `model-values` attribute will be re-computed before the form is submitted; this may be useful if the `model-values` attribute contains expressions which need to be updated before submitting.
494
496
  - `success-message`: A message to show the user if the resource has been successfully added to the collection after submitting the form.
497
+ - `show-field-errors`: If this attribute exists on the element, then granular error messages will be shown above each corresponding input element instead of a single form-level error message.
495
498
  - `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.
496
499
  - `auto-reset-hidden-inputs`: If this attribute exists, then `hidden` input elements which are inside the `collection-adder-form` will be cleared along with other input fields. By default, `hidden` input elements are not cleared when the form is reset.
497
500
  - `disable-submit-on-enter-key`: If this attribute exists, then the form will not submit when the enter key is pressed.
@@ -58,6 +58,20 @@ class CollectionAdderForm extends SocketConsumer {
58
58
  messageContainer.textContent = '';
59
59
  }
60
60
 
61
+ // Clear any existing field errors
62
+ let inputSlot = this.shadowRoot.querySelector('.input-slot');
63
+ if (inputSlot) {
64
+ let slottedElements = inputSlot.assignedElements();
65
+ slottedElements.forEach(element => {
66
+ let existingFieldErrors = element.querySelectorAll('.collection-adder-field-error-container');
67
+ existingFieldErrors.forEach(el => el.textContent = '');
68
+
69
+ // Clear error classes from all inputs
70
+ let allInputs = element.querySelectorAll('input, textarea, select');
71
+ allInputs.forEach(input => input.classList.remove('error'));
72
+ });
73
+ }
74
+
61
75
  let trimSpaces = this.hasAttribute('trim-spaces');
62
76
 
63
77
  let inputElements = this.getAllInputElements();
@@ -158,8 +172,45 @@ class CollectionAdderForm extends SocketConsumer {
158
172
  if (messageContainer) {
159
173
  messageContainer.classList.add('error');
160
174
  messageContainer.classList.remove('success');
175
+ }
176
+
177
+ if (this.hasAttribute('show-field-errors') && error.fieldErrors) {
178
+ let inputElements = this.getAllInputElements();
179
+
180
+ for (let fieldName in error.fieldErrors) {
181
+ let fieldError = error.fieldErrors[fieldName];
182
+
183
+ // Find the input element by name
184
+ let input = inputElements.find(el => el.name === fieldName);
185
+
186
+ if (input) {
187
+ // Look for a parent field container
188
+ let fieldContainer = input.closest('.collection-adder-field-container');
189
+
190
+ if (fieldContainer) {
191
+ let errorDiv = fieldContainer.querySelector('.collection-adder-field-error-container');
192
+ let inputType = input.type || (input.nodeName === 'TEXTAREA' ? 'textarea' : 'select');
193
+
194
+ // For radio buttons and checkboxes, just add error class to input(s)
195
+ if (inputType === 'radio') {
196
+ let radioInputs = fieldContainer.querySelectorAll(`input[name="${fieldName}"][type="radio"]`);
197
+ radioInputs.forEach(radio => radio.classList.add('error'));
198
+ } else if (inputType === 'checkbox') {
199
+ input.classList.add('error');
200
+ } else {
201
+ // For other input types, populate the error message div
202
+ if (errorDiv) {
203
+ let simpleFieldError = fieldError.replace(/^Invalid [^:]+:/i, '');
204
+ errorDiv.textContent = simpleFieldError;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ } else if (messageContainer) {
161
211
  messageContainer.textContent = formatError(error);
162
212
  }
213
+
163
214
  this.dispatchEvent(
164
215
  new CustomEvent('error', {
165
216
  detail: error
@@ -59,6 +59,14 @@ class CollectionAdder extends SocketConsumer {
59
59
  messageContainer.classList.remove('error');
60
60
  messageContainer.textContent = '';
61
61
 
62
+ // Clear any existing field errors
63
+ let existingFieldErrors = this.querySelectorAll('.collection-adder-field-error-container');
64
+ existingFieldErrors.forEach(el => el.textContent = '');
65
+
66
+ // Clear error classes from all inputs
67
+ let allInputs = this.querySelectorAll('.collection-adder-input, .collection-adder-radio');
68
+ allInputs.forEach(input => input.classList.remove('error'));
69
+
62
70
  let radioInputs = [ ...this.querySelectorAll('.collection-adder-radio') ];
63
71
  let radioData = {};
64
72
  for (let radio of radioInputs) {
@@ -150,7 +158,36 @@ class CollectionAdder extends SocketConsumer {
150
158
  } catch (error) {
151
159
  messageContainer.classList.add('error');
152
160
  messageContainer.classList.remove('success');
153
- messageContainer.textContent = formatError(error);
161
+
162
+ if (this.hasAttribute('show-field-errors') && error.fieldErrors) {
163
+ for (let fieldName in error.fieldErrors) {
164
+ let fieldError = error.fieldErrors[fieldName];
165
+ let fieldContainer = this.querySelector(`.collection-adder-field-container[data-field="${fieldName}"]`);
166
+
167
+ if (fieldContainer) {
168
+ let fieldType = this.fieldTypes[fieldName];
169
+ let errorDiv = fieldContainer.querySelector('.collection-adder-field-error-container');
170
+
171
+ // For radio buttons and checkboxes, just add error class to input(s)
172
+ if (fieldType === 'radio') {
173
+ let radioInputs = fieldContainer.querySelectorAll('.collection-adder-radio');
174
+ radioInputs.forEach(radio => radio.classList.add('error'));
175
+ } else if (fieldType === 'checkbox') {
176
+ let input = fieldContainer.querySelector('.collection-adder-input');
177
+ input.classList.add('error');
178
+ } else {
179
+ // For other input types, populate the error message div
180
+ if (errorDiv) {
181
+ let simpleFieldError = fieldError.replace(/^Invalid [^:]+:/i, '');
182
+ errorDiv.textContent = simpleFieldError;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ } else {
188
+ messageContainer.textContent = formatError(error);
189
+ }
190
+
154
191
  this.dispatchEvent(
155
192
  new CustomEvent('error', {
156
193
  detail: error
@@ -241,101 +278,126 @@ class CollectionAdder extends SocketConsumer {
241
278
  let optionLabel = optionLabels[field] == null ? 'Options' : optionLabels[field];
242
279
  if (inputType === 'checkbox') {
243
280
  items.push(
244
- `<label>
245
- ${
246
- inputLabel
247
- }
248
- <input class="collection-adder-input" type="${
249
- inputType
250
- }" name="${
251
- field
252
- }" />
253
- </label>`
281
+ `<div class="collection-adder-field-container" data-field="${field}">
282
+ <div class="collection-adder-field-error-container error"></div>
283
+ <label>
284
+ ${
285
+ inputLabel
286
+ }
287
+ <input class="collection-adder-input" type="${
288
+ inputType
289
+ }" name="${
290
+ field
291
+ }" />
292
+ </label>
293
+ </div>`
254
294
  );
255
295
  } else if (inputType === 'select') {
256
296
  items.push(
257
- `<label>
258
- ${
259
- inputLabel
260
- }
261
- <select name="${field}" class="collection-adder-input collection-adder-select">
262
- <option value="" selected disabled hidden>${optionLabel}</option>
263
- ${inputTypeParams.map(param => `<option value="${param}">${param}</option>`).join('')}
264
- </select>
265
- </label>`
297
+ `<div class="collection-adder-field-container" data-field="${field}">
298
+ <div class="collection-adder-field-error-container error"></div>
299
+ <label>
300
+ ${
301
+ inputLabel
302
+ }
303
+ <select name="${field}" class="collection-adder-input collection-adder-select">
304
+ <option value="" selected disabled hidden>${optionLabel}</option>
305
+ ${inputTypeParams.map(param => `<option value="${param}">${param}</option>`).join('')}
306
+ </select>
307
+ </label>
308
+ </div>`
266
309
  );
267
310
  } else if (inputType === 'text-select') {
268
311
  items.push(
269
- `<div class="collection-adder-select-text-row">
270
- <input class="collection-adder-input" type="text" name="${
271
- field
272
- }" placeholder="${
273
- inputLabel
274
- }"${extraAttributesString} />
275
- <select class="collection-adder-select">
276
- <option value="" selected disabled hidden>${optionLabel}</option>
277
- ${inputTypeParams.map(param => `<option value="${param}">${param}</option>`).join('')}
278
- </select>
312
+ `<div class="collection-adder-field-container" data-field="${field}">
313
+ <div class="collection-adder-field-error-container error"></div>
314
+ <div class="collection-adder-select-text-row">
315
+ <input class="collection-adder-input" type="text" name="${
316
+ field
317
+ }" placeholder="${
318
+ inputLabel
319
+ }"${extraAttributesString} />
320
+ <select class="collection-adder-select">
321
+ <option value="" selected disabled hidden>${optionLabel}</option>
322
+ ${inputTypeParams.map(param => `<option value="${param}">${param}</option>`).join('')}
323
+ </select>
324
+ </div>
279
325
  </div>`
280
326
  );
281
327
  } else if (inputType === 'radio') {
282
328
  items.push(
283
- `
284
- ${
285
- inputLabel
286
- }
287
- ${
288
- inputTypeParams.map(
289
- param => (
290
- `<label>
291
- <input class="collection-adder-radio" type="${
292
- inputType
293
- }" name="${
294
- field
295
- }" value=${
296
- param
297
- } />
298
- ${param}
299
- </label>`
300
- )
301
- ).join('')
302
- }`
329
+ `<div class="collection-adder-field-container" data-field="${field}">
330
+ <div class="collection-adder-field-error-container error"></div>
331
+ <div>
332
+ ${
333
+ inputLabel
334
+ }
335
+ ${
336
+ inputTypeParams.map(
337
+ param => (
338
+ `<label>
339
+ <input class="collection-adder-radio" type="${
340
+ inputType
341
+ }" name="${
342
+ field
343
+ }" value=${
344
+ param
345
+ } />
346
+ ${param}
347
+ </label>`
348
+ )
349
+ ).join('')
350
+ }
351
+ </div>
352
+ </div>`
303
353
  );
304
354
  } else if (inputType === 'textarea') {
305
355
  items.push(
306
- `<textarea class="collection-adder-input" name="${
307
- field
308
- }" placeholder="${
309
- inputLabel
310
- }"${extraAttributesString}></textarea>`
356
+ `<div class="collection-adder-field-container" data-field="${field}">
357
+ <div class="collection-adder-field-error-container error"></div>
358
+ <textarea class="collection-adder-input" name="${
359
+ field
360
+ }" placeholder="${
361
+ inputLabel
362
+ }"${extraAttributesString}></textarea>
363
+ </div>`
311
364
  );
312
365
  } else if (inputType === 'file') {
313
366
  items.push(
314
- `<input class="collection-adder-input" type="${
315
- inputType
316
- }" name="${
317
- field
318
- }"${extraAttributesString} />`
367
+ `<div class="collection-adder-field-container" data-field="${field}">
368
+ <div class="collection-adder-field-error-container error"></div>
369
+ <input class="collection-adder-input" type="${
370
+ inputType
371
+ }" name="${
372
+ field
373
+ }"${extraAttributesString} />
374
+ </div>`
319
375
  );
320
376
  } else if (inputType === 'number') {
321
377
  items.push(
322
- `<input class="collection-adder-input" type="${
323
- inputType
324
- }" name="${
325
- field
326
- }" placeholder="${
327
- inputLabel
328
- }" step="any"${extraAttributesString} />`
378
+ `<div class="collection-adder-field-container" data-field="${field}">
379
+ <div class="collection-adder-field-error-container error"></div>
380
+ <input class="collection-adder-input" type="${
381
+ inputType
382
+ }" name="${
383
+ field
384
+ }" placeholder="${
385
+ inputLabel
386
+ }" step="any"${extraAttributesString} />
387
+ </div>`
329
388
  );
330
389
  } else {
331
390
  items.push(
332
- `<input class="collection-adder-input" type="${
333
- inputType
334
- }" name="${
335
- field
336
- }" placeholder="${
337
- inputLabel
338
- }"${extraAttributesString} />`
391
+ `<div class="collection-adder-field-container" data-field="${field}">
392
+ <div class="collection-adder-field-error-container error"></div>
393
+ <input class="collection-adder-input" type="${
394
+ inputType
395
+ }" name="${
396
+ field
397
+ }" placeholder="${
398
+ inputLabel
399
+ }"${extraAttributesString} />
400
+ </div>`
339
401
  );
340
402
  }
341
403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "9.0.4",
3
+ "version": "9.1.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/styles.css CHANGED
@@ -159,6 +159,11 @@ collection-adder .collection-adder-form-content > label {
159
159
  align-self: flex-start;
160
160
  }
161
161
 
162
+ collection-adder .collection-adder-field-container {
163
+ display: flex;
164
+ flex-direction: column;
165
+ }
166
+
162
167
  .label-container {
163
168
  display: flex;
164
169
  justify-content: flex-start;