saasufy-components 2.12.1 → 2.14.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
@@ -1,6 +1,41 @@
1
1
  # Saasufy Components
2
2
  Components for Saasufy
3
3
 
4
+ ### Application state
5
+ - [app-router](#app-router)
6
+ - [socket-provider](#socket-provider)
7
+
8
+ ### Collections of resources
9
+ - [collection-browser](#collection-browser)
10
+ - [collection-adder](#collection-adder)
11
+ - [collection-deleter](#collection-deleter)
12
+ - [collection-reducer](#collection-reducer)
13
+
14
+ ### Single resources
15
+ - [model-input](#model-input)
16
+ - [model-text](#model-text)
17
+ - [model-viewer](#model-viewer)
18
+
19
+ ### User input
20
+ - [input-provider](#input-provider)
21
+
22
+ ### Authentication
23
+ - [log-in-form](#log-in-form)
24
+ - [log-out](#log-out)
25
+ - [oauth-link](#oauth-link)
26
+ - [oauth-handler](#oauth-handler)
27
+
28
+ ### Display groups
29
+ - [render-group](#render-group)
30
+ - [conditional-group](#conditional-group)
31
+ - [if-group](#if-group)
32
+ - [switch-group](#switch-group)
33
+ - [collection-adder-group](#collection-adder-group)
34
+
35
+ ### Modal windows
36
+ - [confirm-modal](#confirm-modal)
37
+ - [overlay-modal](#overlay-modal)
38
+
4
39
  ## Overview
5
40
 
6
41
  Many Saasufy components use a combination of `<template slot="item">` and `<div slot="viewport">`. Both of these are known as `slotted` elements.
@@ -29,14 +64,17 @@ To use components, you just need to include them into your `.html` file inside y
29
64
  <script src="https://saasufy.com/node_modules/saasufy-components/model-text.js" type="module" defer></script>
30
65
  <script src="https://saasufy.com/node_modules/saasufy-components/model-viewer.js" type="module" defer></script>
31
66
  <script src="https://saasufy.com/node_modules/saasufy-components/input-provider.js" type="module" defer></script>
32
- <script src="https://saasufy.com/node_modules/saasufy-components/confirm-modal.js" type="module" defer></script>
33
- <script src="https://saasufy.com/node_modules/saasufy-components/render-group.js" type="module" defer></script>
34
- <script src="https://saasufy.com/node_modules/saasufy-components/conditional-group.js" type="module" defer></script>
35
- <script src="https://saasufy.com/node_modules/saasufy-components/collection-adder-group.js" type="module" defer></script>
36
67
  <script src="https://saasufy.com/node_modules/saasufy-components/log-in-form.js" type="module" defer></script>
37
68
  <script src="https://saasufy.com/node_modules/saasufy-components/log-out.js" type="module" defer></script>
38
69
  <script src="https://saasufy.com/node_modules/saasufy-components/oauth-link.js" type="module" defer></script>
39
70
  <script src="https://saasufy.com/node_modules/saasufy-components/oauth-handler.js" type="module" defer></script>
71
+ <script src="https://saasufy.com/node_modules/saasufy-components/render-group.js" type="module" defer></script>
72
+ <script src="https://saasufy.com/node_modules/saasufy-components/conditional-group.js" type="module" defer></script>
73
+ <script src="https://saasufy.com/node_modules/saasufy-components/if-group.js" type="module" defer></script>
74
+ <script src="https://saasufy.com/node_modules/saasufy-components/switch-group.js" type="module" defer></script>
75
+ <script src="https://saasufy.com/node_modules/saasufy-components/collection-adder-group.js" type="module" defer></script>
76
+ <script src="https://saasufy.com/node_modules/saasufy-components/confirm-modal.js" type="module" defer></script>
77
+ <script src="https://saasufy.com/node_modules/saasufy-components/overlay-modal.js" type="module" defer></script>
40
78
  ```
41
79
 
42
80
  You should only add the script tags for the components which your page uses to avoid wasting bandwidth and unnecessarily delaying page load for your users.
@@ -144,40 +182,152 @@ Can also perform basic CRUD operations such as deleting or creating records by l
144
182
  </div>
145
183
  </template>
146
184
 
147
- <div slot="viewport" class="chat-viewport"></div>
185
+ <template slot="loader">
186
+ <div class="loading-spinner-container">
187
+ <div class="spinning">&#8635;</div>
188
+ </div>
189
+ </template>
148
190
 
149
- <div slot="loader" class="loading-spinner-container">
150
- <div class="spinning">&#8635;</div>
151
- </div>
191
+ <div slot="viewport" class="chat-viewport"></div>
152
192
  </collection-browser>
153
193
  ```
154
194
 
155
195
  **Attributes**
156
196
 
157
- - `collection-type` (required): Specifies the type of collection to be used, which defines the dataset you want to work with. This should match a `Model` available in your Saasufy service.
197
+ - `collection-type` (required): Specifies the type of collection to display. This should match a `Model` available in your Saasufy service.
158
198
  - `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.
159
199
  - `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`.
160
200
  - `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`.
161
201
  - `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.
162
202
  - `collection-page-offset`: Indicates the current page offset in the collection's data. It’s like telling the browser which page of data you want to display initially.
163
203
  - `type-alias`: Allows you to provide an alternative name for your `Model` to use when injecting values inside the template. This is useful for situations where you may have multiple `collection-browser` elements and/or `model-viewer` elements of the same type nested inside each other and want to avoid `Model` name clashes in the nested template definitions. For example, if the `type-alias` in the snippet above was set to `SubChat`, then `{{Chat.message}}` would become `{{SubChat.message}}`.
164
- - `hide-error-logs`: A flag that, when present, suppresses error logs from being printed to the console. This can be used to keep the user interface clean of technical messages.
204
+ - `hide-error-logs`: A flag which, when present, suppresses error logs from being printed to the console.
165
205
  - `max-show-loader`: If this attribute is present, your slotted `loader` element will be shown as often as possible; this includes situations where the collection is merely refreshing itself. It is disabled by default.
166
206
 
167
207
  ### collection-adder
168
208
 
169
209
  A form component for inserting data into collections.
170
210
 
211
+ **Example usage**
212
+
213
+ ```html
214
+ <collection-adder
215
+ collection-type="Product"
216
+ collection-fields="name,brand,qty"
217
+ model-values="categoryName=electronics"
218
+ submit-button-label="Create"
219
+ trim-spaces
220
+ ></collection-adder>
221
+ ```
222
+
223
+ **Attributes**
224
+
225
+ - `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.
226
+ - `collection-fields`: A comma-separated list of fields from the `Model` to display as input elements inside the form for the user to fill in. Each field name in this list can optionally be followed by an input element type after a `:` character. For example `collection-fields="qty:number, size:select(small,medium,large)` will create one input element with `type="number"` and one with `type=select` with options `small`, `medium` or `large`. To make a select field optional, you can prefix the list of options with a comma; e.g. `size:select(,small,medium,large)`.
227
+ - `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.
228
+ - `submit-button-label`: Text to display on the submit button. If not specified, defaults to `Submit`.
229
+ - `hide-submit-button`: Adding this attribute will hide the submit button from the form.
230
+ - `success-message`: A message to show the user if the resource has been successfully added to the collection after submitting the form.
231
+ - `autocapitalize`: Can be set to `off` to disable auto-capitalization of the first input character on mobile devices.
232
+ - `autocorrect`: Can be set to `off` to disable auto-correction of input on mobile devices.
233
+ - `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.
234
+
171
235
  ### collection-deleter
172
236
 
173
237
  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).
174
238
  It supports either immediate deletion or deletion upon confirmation; in the latter case, the parent `collection-browser` must have a `confirm-modal` component slotted into its `modal` slot.
175
239
 
240
+ **Example usage**
241
+
242
+ ```html
243
+ <!-- Must be placed somewhere inside a collection-browser component, typically inside the slotted item template. -->
244
+ <collection-deleter model-id="{{Product.id}}" confirm-message="Are you sure you want to delete the {{Product.name}} product?" onclick="confirmDeleteItem()">&#x2715;</collection-deleter>
245
+ ```
246
+
247
+ **Attributes**
248
+
249
+ - `model-id` (required): Specifies the ID of the resource to delete from the parent collection when this component is activated. This can be achieved by invoking either the `deleteItem()` or `confirmDeleteItem()` function from inside an event handler. The example above shows how to achieve deletion via the `onclick` event. You can either invoke `deleteItem()` to delete the resource immediately or you can invoke `confirmDeleteItem()` to require additional confirmation prior to deletion.
250
+ - `confirm-message`: The confirmation message to show the user when this component's `confirmDeleteItem()` function is invoked,
251
+
252
+ If `confirmDeleteItem()` is used, then the parent `collection-browser` must have a `confirm-modal` element slotted into it as shown here:
253
+
254
+ ```html
255
+ <collection-browser
256
+ collection-type="Product"
257
+ collection-fields="name,qty"
258
+ collection-view="alphabeticalView"
259
+ collection-view-params=""
260
+ collection-page-size="50"
261
+ >
262
+ <template slot="item">
263
+ <div>
264
+ <div class="chat-message">{{Product.name}}</div>
265
+ <collection-deleter model-id="{{Product.id}}" confirm-message="Are you sure you want to delete the {{Product.name}} product?" onclick="confirmDeleteItem()">&#x2715;</collection-deleter>
266
+ </div>
267
+ </template>
268
+
269
+ <div slot="viewport" class="chat-viewport"></div>
270
+
271
+ <!-- The confirm-modal element must be specified here with slot="modal" to prompt the user for confirmation -->
272
+ <confirm-modal slot="modal" title="Delete confirmation" message="" confirm-button-label="Delete"></confirm-modal>
273
+ </collection-browser>
274
+ ```
275
+
176
276
  ### collection-reducer
177
277
 
178
278
  Similar to the `collection-browser` component but designed to render collections in combined format. For example, to combine values from multiple records into a single item.
179
279
  A common use case is to extract and join values to pass to other child components via their attributes.
180
280
 
281
+ **Example usage**
282
+
283
+ ```html
284
+ <!--
285
+ Here, the collection-reducer fetches all available categories and joins their name fields
286
+ to use as options for a model-input select element.
287
+ This allows the user to choose among available category names to update the
288
+ current product's categoryName field.
289
+ You should assume that {{productId}} comes from a parent component (e.g. app-router).
290
+ -->
291
+ <collection-reducer
292
+ collection-type="Category"
293
+ collection-fields="name"
294
+ collection-view="alphabeticalView"
295
+ collection-page-size="100"
296
+ collection-view-params=""
297
+ >
298
+ <template slot="item">
299
+ <div class="form">
300
+ <label class="label-container">
301
+ <div>Product category</div>
302
+ <model-input
303
+ type="select"
304
+ options="{{joinFields(Category, 'name')}}"
305
+ model-type="Product"
306
+ default-value="accountId"
307
+ model-id="{{productId}}"
308
+ model-field="categoryName"
309
+ ></model-input>
310
+ </label>
311
+ </div>
312
+ </template>
313
+ <div slot="viewport"></div>
314
+ </collection-reducer>
315
+ ```
316
+
317
+ Unlike with `collection-browser` or `model-viewer`, the template variable references not a single model instance but an array of model instances.
318
+ In the example above, the name of the first element can be accessed with `{{Category[0].name}}`.
319
+
320
+ **Attributes**
321
+
322
+ - `collection-type` (required): Specifies the type of collection to use. This should match a `Model` available in your Saasufy service.
323
+ - `collection-fields` (required): A comma-separated list of fields from the `Model` that you want to use. This lets you pick specific pieces of data from your collection to work with.
324
+ - `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`.
325
+ - `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`.
326
+ - `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.
327
+ - `collection-page-offset`: Indicates the current page offset in the collection's data. It’s like telling the browser which page of data you want to display initially.
328
+ - `type-alias`: Allows you to provide an alternative name for your `Model` to use when injecting values inside the template. This is useful for situations where you may have multiple `collection-browser` elements and/or `model-viewer` elements of the same type nested inside each other and want to avoid `Model` name clashes in the nested template definitions. For example, if the `type-alias` in the snippet above was set to `SubChat`, then `{{Chat.message}}` would become `{{SubChat.message}}`.
329
+ - `hide-error-logs`: A flag which, when present, suppresses error logs from being printed to the console.
330
+
181
331
  ### model-input
182
332
 
183
333
  Used for displaying and editing a single field of a model instance in real time.
@@ -211,18 +361,24 @@ A component which can be placed inside a `socket-provider` to deauthenticate the
211
361
 
212
362
  Example usage: `<log-out onclick="logOut()"><a href="javascript:void(0)">Log out</a></log-out>`
213
363
 
214
- ### confirm-modal
364
+ ### oauth-link
215
365
 
216
- A modal component to prompt the user for confirmation before performing sensitive operations.
366
+ A link to initiate an OAuth flow to authenticate a user as part of log in. Can support a range of different OAuth providers.
217
367
 
218
- ### overlay-modal
368
+ ### oauth-handler
219
369
 
220
- A general purpose modal component.
370
+ This component handles the final stage of OAuth and then redirects the user to the relevant page/URL if successful.
371
+ When using an OAuth provider, the callback URL which you register with the provider must lead the user back to a page which contains this `oauth-handler` component.
221
372
 
222
373
  ### render-group
223
374
 
224
375
  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.
225
376
 
377
+ ### conditional-group
378
+
379
+ A component which exposes a `show-content` property which can be set to true or false to show or hide its content.
380
+ 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. Unlike `if-group`, this component does not require a template, however, the trade-off is that the slotted content will be pre-processed by the browser even when the expression is false. This may not be suitable if you want to (for example) render child images conditionally because the browser will preload images based on their `src` attributes, even if they are not shown.
381
+
226
382
  ### if-group
227
383
 
228
384
  A component which exposes a `show-content` property which can be set to true or false to show or hide its content.
@@ -233,11 +389,14 @@ It's intended to be placed inside a `model-viewer`, `collection-browser` or `col
233
389
  A component which exposes a `show-cases` property which can be set to key-value pairs in the format `key1=true,key2=false`. It can be used to conditionally display multiple slotted elements based on multiple conditions. It helps to keep HTML clean when the conditions are complex.
234
390
  This element is intended to be placed inside a `model-viewer`, `collection-browser` or `collection-reducer` component such that the true/false values of the `show-cases` attribute can be computed using template `{{expression}}` placeholders. This component requires one or more templates and a viewport to be slotted in. The content of the templates will not be processed unless the `show-cases` condition is met. All templates must have a `slot="content"` attribute and a name attribute in the format `name="key1"` where the value `key1` corresponds to the key specified inside the `show-cases` attribute of the `switch-group`.
235
391
 
236
- ### conditional-group
237
-
238
- A component which exposes a `show-content` property which can be set to true or false to show or hide its content.
239
- 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. Unlike `if-group`, this component does not require a template, however, the trade-off is that the slotted content will be pre-processed by the browser even when the expression is false. This may not be suitable if you want to (for example) render child images conditionally because the browser will preload images based on their `src` attributes, even if they are not shown.
240
-
241
392
  ### collection-adder-group
242
393
 
243
394
  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.
395
+
396
+ ### confirm-modal
397
+
398
+ A modal component to prompt the user for confirmation before performing sensitive operations.
399
+
400
+ ### overlay-modal
401
+
402
+ A general purpose modal component.
@@ -9,6 +9,7 @@ class CollectionBrowser extends SocketConsumer {
9
9
  super();
10
10
  this.isReady = false;
11
11
  this.isStale = true;
12
+ this.activeLoader = null;
12
13
  this.attachShadow({ mode: 'open' });
13
14
 
14
15
  this.handleShowModalEvent = (event) => {
@@ -46,7 +47,7 @@ class CollectionBrowser extends SocketConsumer {
46
47
  this.goToPage((event.detail || {}).offset);
47
48
  };
48
49
 
49
- this.handleSlotChangeEvent = () => {
50
+ this.handleSlotChangeEvent = (event) => {
50
51
  this.renderList();
51
52
  };
52
53
  }
@@ -54,6 +55,7 @@ class CollectionBrowser extends SocketConsumer {
54
55
  connectedCallback() {
55
56
  this.isReady = true;
56
57
  this.socket = this.getSocket();
58
+ this.activeLoader = null;
57
59
  this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
58
60
  this.addEventListener('showModal', this.handleShowModalEvent);
59
61
  this.addEventListener('crudCreate', this.handleCRUDCreateEvent);
@@ -162,23 +164,26 @@ class CollectionBrowser extends SocketConsumer {
162
164
 
163
165
  renderList() {
164
166
  let viewportSlot = this.shadowRoot.querySelector('slot[name="viewport"]');
167
+
168
+ let viewportNode = viewportSlot.assignedNodes()[0];
169
+ if (!viewportNode) return;
170
+
165
171
  let loaderSlot = this.shadowRoot.querySelector('slot[name="loader"]');
166
- let hasLoaders = !!loaderSlot.assignedNodes().length;
172
+ let loaderNode = loaderSlot.assignedNodes()[0];
167
173
 
168
174
  if (!this.collection || !this.collection.isLoaded) {
169
- if (hasLoaders && (this.isStale || this.hasAttribute('max-show-loader'))) {
170
- viewportSlot.classList.add('hidden');
171
- loaderSlot.classList.remove('hidden');
175
+ if (
176
+ loaderNode &&
177
+ this.activeLoader !== loaderNode &&
178
+ (this.isStale || this.hasAttribute('max-show-loader'))
179
+ ) {
180
+ this.activeLoader = loaderNode;
181
+ viewportNode.innerHTML = loaderNode.innerHTML;
172
182
  }
173
183
  return;
174
184
  }
175
185
  this.isStale = false;
176
-
177
- loaderSlot.classList.add('hidden');
178
- viewportSlot.classList.remove('hidden');
179
-
180
- let viewportNode = viewportSlot.assignedNodes()[0];
181
- if (!viewportNode) return;
186
+ this.activeLoader = null;
182
187
 
183
188
  let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
184
189
  let noItemTemplate = this.shadowRoot.querySelector('slot[name="no-item"]').assignedNodes()[0];
@@ -241,11 +246,6 @@ class CollectionBrowser extends SocketConsumer {
241
246
  });
242
247
 
243
248
  this.shadowRoot.innerHTML = `
244
- <style>
245
- .hidden {
246
- display: none;
247
- }
248
- </style>
249
249
  <slot name="loader"></slot>
250
250
  <slot name="item"></slot>
251
251
  <slot name="no-item"></slot>
@@ -7,6 +7,9 @@ const DEFAULT_RELOAD_DELAY = 0;
7
7
  class CollectionReducer extends SocketConsumer {
8
8
  constructor() {
9
9
  super();
10
+ this.isReady = false;
11
+ this.isStale = true;
12
+ this.activeLoader = null;
10
13
  this.attachShadow({ mode: 'open' });
11
14
 
12
15
  this.handleGoToPreviousPageEvent = () => {
@@ -24,12 +27,12 @@ class CollectionReducer extends SocketConsumer {
24
27
  this.handleSlotChangeEvent = () => {
25
28
  this.renderReduction();
26
29
  };
27
- this.isReady = false;
28
30
  }
29
31
 
30
32
  connectedCallback() {
31
33
  this.isReady = true;
32
34
  this.socket = this.getSocket();
35
+ this.activeLoader = null;
33
36
  this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
34
37
  this.addEventListener('goToPreviousPage', this.handleGoToPreviousPageEvent);
35
38
  this.addEventListener('goToNextPage', this.handleGoToNextPageEvent);
@@ -47,12 +50,14 @@ class CollectionReducer extends SocketConsumer {
47
50
 
48
51
  goToPreviousPage() {
49
52
  if (!this.collection) return;
53
+ this.isStale = true;
50
54
  this.collection.fetchPreviousPage();
51
55
  this.setAttribute('collection-page-offset', this.collection.meta.pageOffset);
52
56
  }
53
57
 
54
58
  goToNextPage() {
55
59
  if (!this.collection) return;
60
+ this.isStale = true;
56
61
  this.collection.fetchNextPage();
57
62
  this.setAttribute('collection-page-offset', this.collection.meta.pageOffset);
58
63
  }
@@ -69,6 +74,7 @@ class CollectionReducer extends SocketConsumer {
69
74
  'collection-view-params',
70
75
  'collection-page-size',
71
76
  'collection-page-offset',
77
+ 'max-show-loader',
72
78
  'type-alias',
73
79
  'hide-error-logs'
74
80
  ];
@@ -79,6 +85,7 @@ class CollectionReducer extends SocketConsumer {
79
85
  if (this.collection && name === 'collection-page-offset') {
80
86
  let newOffset = Number(newValue);
81
87
  if (newOffset !== this.collection.meta.pageOffset) {
88
+ this.isStale = true;
82
89
  this.collection.fetchPage(newOffset);
83
90
  }
84
91
  this.updatePageNumberElements();
@@ -128,22 +135,26 @@ class CollectionReducer extends SocketConsumer {
128
135
 
129
136
  renderReduction() {
130
137
  let viewportSlot = this.shadowRoot.querySelector('slot[name="viewport"]');
138
+
139
+ let viewportNode = viewportSlot.assignedNodes()[0];
140
+ if (!viewportNode) return;
141
+
131
142
  let loaderSlot = this.shadowRoot.querySelector('slot[name="loader"]');
132
- let hasLoaders = !!loaderSlot.assignedNodes().length;
143
+ let loaderNode = loaderSlot.assignedNodes()[0];
133
144
 
134
145
  if (!this.collection || !this.collection.isLoaded) {
135
- if (hasLoaders) {
136
- viewportSlot.classList.add('hidden');
137
- loaderSlot.classList.remove('hidden');
146
+ if (
147
+ loaderNode &&
148
+ this.activeLoader !== loaderNode &&
149
+ (this.isStale || this.hasAttribute('max-show-loader'))
150
+ ) {
151
+ this.activeLoader = loaderNode;
152
+ viewportNode.innerHTML = loaderNode.innerHTML;
138
153
  }
139
154
  return;
140
155
  }
141
-
142
- loaderSlot.classList.add('hidden');
143
- viewportSlot.classList.remove('hidden');
144
-
145
- let viewportNode = viewportSlot.assignedNodes()[0];
146
- if (!viewportNode) return;
156
+ this.isStale = false;
157
+ this.activeLoader = null;
147
158
 
148
159
  let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
149
160
  let noItemTemplate = this.shadowRoot.querySelector('slot[name="no-item"]').assignedNodes()[0];
@@ -174,10 +185,11 @@ class CollectionReducer extends SocketConsumer {
174
185
  );
175
186
  let viewParams = Object.fromEntries(
176
187
  collectionViewParams.split(',')
177
- .map(pair => pair.split('='))
178
- .filter(([field]) => field)
188
+ .map(pair => pair.split('='))
189
+ .filter(([field]) => field)
179
190
  );
180
191
  if (this.collection) this.collection.destroy();
192
+ this.isStale = true;
181
193
 
182
194
  this.collection = new AGCollection({
183
195
  socket: this.socket,
@@ -191,11 +203,6 @@ class CollectionReducer extends SocketConsumer {
191
203
  });
192
204
 
193
205
  this.shadowRoot.innerHTML = `
194
- <style>
195
- .hidden {
196
- display: none;
197
- }
198
- </style>
199
206
  <slot name="loader"></slot>
200
207
  <slot name="item"></slot>
201
208
  <slot name="no-item"></slot>
package/model-input.js CHANGED
@@ -61,6 +61,14 @@ class ModelInput extends SocketConsumer {
61
61
 
62
62
  set value(newValue) {
63
63
  if (this.inputElement) {
64
+ if (this.inputElement.type === 'checkbox') {
65
+ let oldValue = this.inputElement.checked;
66
+ this.inputElement.checked = newValue;
67
+ if (oldValue !== newValue) {
68
+ this.triggerChange();
69
+ }
70
+ return;
71
+ }
64
72
  let oldValue = this.inputElement.value;
65
73
  this.inputElement.value = newValue;
66
74
  if (oldValue !== newValue) {
@@ -70,7 +78,13 @@ class ModelInput extends SocketConsumer {
70
78
  }
71
79
 
72
80
  get value() {
73
- return (this.inputElement && this.inputElement.value) || '';
81
+ if (!this.inputElement) {
82
+ return '';
83
+ }
84
+ if (this.inputElement.type === 'checkbox') {
85
+ return this.inputElement.checked;
86
+ }
87
+ return this.inputElement.value;
74
88
  }
75
89
 
76
90
  render() {
package/model-viewer.js CHANGED
@@ -11,11 +11,13 @@ class ModelViewer extends SocketConsumer {
11
11
  this.renderItem();
12
12
  };
13
13
  this.isReady = false;
14
+ this.activeLoader = null;
14
15
  }
15
16
 
16
17
  connectedCallback() {
17
18
  this.isReady = true;
18
19
  this.socket = this.getSocket();
20
+ this.activeLoader = null;
19
21
  this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
20
22
  this.render();
21
23
  }
@@ -53,22 +55,22 @@ class ModelViewer extends SocketConsumer {
53
55
 
54
56
  renderItem() {
55
57
  let viewportSlot = this.shadowRoot.querySelector('slot[name="viewport"]');
58
+
59
+ let viewportNode = viewportSlot.assignedNodes()[0];
60
+ if (!viewportNode) return;
61
+
56
62
  let loaderSlot = this.shadowRoot.querySelector('slot[name="loader"]');
57
- let hasLoaders = !!loaderSlot.assignedNodes().length;
63
+ let loaderNode = loaderSlot.assignedNodes()[0];
58
64
 
59
65
  if (!this.model || !this.model.isLoaded) {
60
- if (hasLoaders) {
61
- viewportSlot.classList.add('hidden');
62
- loaderSlot.classList.remove('hidden');
66
+ if (loaderNode && this.activeLoader !== loaderNode) {
67
+ this.activeLoader = loaderNode;
68
+ viewportNode.innerHTML = loaderNode.innerHTML;
63
69
  }
64
70
  return;
65
71
  }
66
72
 
67
- loaderSlot.classList.add('hidden');
68
- viewportSlot.classList.remove('hidden');
69
-
70
- let viewportNode = viewportSlot.assignedNodes()[0];
71
- if (!viewportNode) return;
73
+ this.activeLoader = null;
72
74
 
73
75
  let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
74
76
  let modelValue = this.model.value;
@@ -130,11 +132,6 @@ class ModelViewer extends SocketConsumer {
130
132
  }
131
133
 
132
134
  this.shadowRoot.innerHTML = `
133
- <style>
134
- .hidden {
135
- display: none;
136
- }
137
- </style>
138
135
  <slot name="loader"></slot>
139
136
  <slot name="item"></slot>
140
137
  <slot name="no-item"></slot>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "2.12.1",
3
+ "version": "2.14.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {