saasufy-components 2.6.2 → 2.7.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,23 +1,169 @@
1
1
  # Saasufy Components
2
2
  Components for Saasufy
3
3
 
4
+ ## Overview
5
+
6
+ Many Saasufy components use a combination of `<template slot="item">` and `<div slot="viewport">`. Both of these are known as `slotted` elements.
7
+
8
+ A `<template slot="item">` is an inert/hidden element which the component uses to format its output. The `<div slot="viewport">` is an element which will contain the rendered output of the component.
9
+
10
+ Components which follow this pattern take templates as input and render them inside the specified `<div slot="viewport">` element. Note that the viewport can be any element and not necessarily a `div`.
11
+
12
+ The most important thing to remember is that the element marked with the `slot="item"` attribute serves as a template for your component and the element marked with `slot="viewport"` serves as a container for its output. Although the `item` slot is commonly used, components may require different slot names for input templates so it's important to read the documentation carefully.
13
+
14
+ A component's output consists of instances of your template after they've been populated with data from the component.
15
+ You can apply custom CSS/styling to elements that are rendered inside a viewport - It may be useful to add a custom `class` attribute to your viewport element to allow targeting using CSS selectors.
16
+
4
17
  ## How to use
5
18
 
6
- Coming soon.
19
+ To use components, you just need to include them into your `.html` file inside your `<head>` tag like this:
20
+
21
+ ```html
22
+ <script src="https://saasufy.com/node_modules/saasufy-components/socket.js" type="module" defer></script>
23
+ <script src="https://saasufy.com/node_modules/saasufy-components/app-router.js" type="module" defer></script>
24
+ <script src="https://saasufy.com/node_modules/saasufy-components/collection-browser.js" type="module" defer></script>
25
+ <script src="https://saasufy.com/node_modules/saasufy-components/collection-adder.js" type="module" defer></script>
26
+ <script src="https://saasufy.com/node_modules/saasufy-components/collection-deleter.js" type="module" defer></script>
27
+ <script src="https://saasufy.com/node_modules/saasufy-components/collection-reducer.js" type="module" defer></script>
28
+ <script src="https://saasufy.com/node_modules/saasufy-components/model-input.js" type="module" defer></script>
29
+ <script src="https://saasufy.com/node_modules/saasufy-components/model-text.js" type="module" defer></script>
30
+ <script src="https://saasufy.com/node_modules/saasufy-components/model-viewer.js" type="module" defer></script>
31
+ <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
+ <script src="https://saasufy.com/node_modules/saasufy-components/log-in-form.js" type="module" defer></script>
37
+ <script src="https://saasufy.com/node_modules/saasufy-components/log-out.js" type="module" defer></script>
38
+ <script src="https://saasufy.com/node_modules/saasufy-components/oauth-link.js" type="module" defer></script>
39
+ <script src="https://saasufy.com/node_modules/saasufy-components/oauth-handler.js" type="module" defer></script>
40
+ ```
41
+
42
+ 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.
43
+ The `.js` file name of the component matches the names of the HTML tags which the script provides.
44
+ The only exception is the `socket-provider` component which is located inside the `socket.js` file - This is because that file exposes multiple components which are used behind the scenes by other components.
45
+ You should always include the `socket.js` script as shown above whenever you use components which depend on data from Saasufy.
46
+
47
+ Linking the scripts directly from `saasufy.com` as shown above is the simplest way to get started as it doesn't require loading anything or hosting the scripts yourself.
48
+ An alternative approach is to download Saasufy components using `npm install saasufy-components` and then link to them from your own hosting provider.
7
49
 
8
50
  ## Components
9
51
 
52
+ ### app-router
53
+
54
+ A component which allows you build apps which have multiple pages. Inside it, you should specify a template for each page in your app along with the route/path to bind each page to.
55
+ This component also supports simple redirects as well as redirects based on the user's current authentication state. Redirects can be soft or hard; a soft redirect does not change the current URL/path, a hard redirect does.
56
+
57
+ **Example usage**
58
+
59
+ ```html
60
+ <app-router>
61
+ <template slot="page" route-path="/:productName">
62
+ <div class="product-title">Product: {{capitalize(productName)}}</div>
63
+ </template>
64
+ <div slot="viewport"></div>
65
+ </app-router>
66
+ ```
67
+
68
+ This example shows how an infinite number of pages can be represented with a single route template.
69
+ In this case, the `/:productName` inside the `route-path` attribute of the slotted `<template slot="page">` element serves as a placeholder variable which can match any text.
70
+ You can then use such route variables inside the page template itself as shown with `<div class="product-title">Product: {{capitalize(productName)}}</div>` - In this case, the `productName` variable which comes from the route path is first capitalized using the `capitalize` utility function before it is rendered into the template.
71
+ Note that the `app-router` can contain any number of pages/routes so the `slot="page"` attribute can be re-used multiple times, e.g:
72
+
73
+ ```html
74
+ <app-router>
75
+ <template slot="page" route-path="/home">
76
+ <div>This is the home page</div>
77
+ </template>
78
+ <template slot="page" route-path="/about-us">
79
+ <div>This is the about-us page</div>
80
+ </template>
81
+ <template slot="page" route-path="/products">
82
+ <div>This is the products page</div>
83
+ </template>
84
+ <div slot="viewport"></div>
85
+ </app-router>
86
+ ```
87
+
88
+ The app router path is based on the `location.hash` so the path section in your browser's address bar needs to start with the `#` character.
89
+ So for example, if your `index.html` file is served up from the URL `http://mywebsite.com`, then, to activate the `/products` route as in the example above, you would need to type the URL `http://mywebsite.com#/products`.
90
+
91
+ **Attributes of app-router**
92
+
93
+ - `default-page`: The path/route of the default page to send the user to in case no routes are matched. Commonly used to point to a `/page-not-found` page. For the home page, it's typically recommended to create a page template with `route-path=""` instead.
94
+ - `debounce-delay`: The number of milliseconds to wait before changing the route. This can help to avoid multiple renders if the route changes rapidly (e.g. when doing hard redirects). Defaults to 100ms.
95
+
96
+ **Attributes of slotted page templates**
97
+
98
+ - `redirect`: An optional path/route which this page should redirect to. Note that the content of this page will not be shown so it should always be empty.
99
+ - `auth-redirect`: An optional path/route which this page should redirect to if the user is authenticated. If the user is not authenticated, then the content of the template will be displayed as normal.
100
+ - `no-auth-redirect`: An optional path/route which this page should redirect to if the user is not authenticated. If the user is authenticated, then the content of the template will be displayed as normal.
101
+ - `hard-redirect`: An optional, additional attribute which can be specified alongside a `redirect`, `auth-redirect` or `no-auth-redirect` attribute to ensure that any redirect will change the URL in the address bar. Redirects are `soft` redirects by default; this means that they redirect the user to the target page but keep the current URL path unchanged.
102
+
10
103
  ### socket-provider
11
104
 
12
105
  A top level component which connects to your Saasufy service and inside which you can place other components which depends on Saasufy data.
13
106
  A Saasufy component which integrates with data from Saasufy is known as a `socket-consumer` and must always be placed inside a `socket-provider` element (although it does not have to be a direct child).
14
107
 
108
+ **Example usage**
109
+
110
+ ```html
111
+ <socket-provider url="wss://saasufy.com/sid7890/socketcluster/">
112
+ <!-- Elements which rely on Saasufy data go here. -->
113
+ </socket-provider>
114
+ ```
115
+
116
+ **Attributes**
117
+
118
+ - `url` (required): Specifies the URL for the Saasufy service to use as the data store for all components which are placed inside it.
119
+ - `auth-token-name`: Allows you to specify a custom key for your token - This will be used as the key in localStorage. You generally do not need to set this attribute. It is intended for situations were an app has multiple `socket-provider` elements connecting different services with different/separate authentication flows.
120
+
15
121
  ### collection-browser
16
122
 
17
123
  Used for rendering collections as lists, tables or other sequences based on a specific view using a template.
18
124
  Supports pagination by allowing you to specify custom buttons or links to navigate between pages.
19
125
  Can also perform basic CRUD operations such as deleting or creating records by listening for events from child components.
20
126
 
127
+ **Example usage**
128
+
129
+ ```html
130
+ <collection-browser
131
+ collection-type="Chat"
132
+ collection-fields="username,message,createdAt"
133
+ collection-view="recentView"
134
+ collection-view-params=""
135
+ collection-page-size="50"
136
+ >
137
+ <template slot="item">
138
+ <div>
139
+ <div class="chat-username">
140
+ <b>{{Chat.username}}</b>
141
+ </div>
142
+ <div class="chat-message">{{Chat.message}}</div>
143
+ <div class="chat-created-at">{{date(Chat.createdAt)}}</div>
144
+ </div>
145
+ </template>
146
+
147
+ <div slot="viewport" class="chat-viewport"></div>
148
+
149
+ <div slot="loader" class="loading-spinner-container">
150
+ <div class="spinning">&#8635;</div>
151
+ </div>
152
+ </collection-browser>
153
+ ```
154
+
155
+ **Attributes**
156
+
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.
158
+ - `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
+ - `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
+ - `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
+ - `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
+ - `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
+ - `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.
165
+ - `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
+
21
167
  ### collection-adder
22
168
 
23
169
  A form component for inserting data into collections.
@@ -10,7 +10,10 @@ class CollectionAdder extends SocketConsumer {
10
10
  text: String,
11
11
  textarea: String,
12
12
  checkbox: Boolean,
13
- number: Number
13
+ number: Number,
14
+ radio: String,
15
+ select: String,
16
+ 'text-select': String
14
17
  };
15
18
  this.fieldPartsRegExp = /("[^"]*"|'[^']*'|\([^)]*\)|[^,()"']+)+/g;
16
19
  this.inputTypeWithParamsRegExp = /^([^()]*)(\(([^)]*)\))?/;
@@ -33,6 +36,7 @@ class CollectionAdder extends SocketConsumer {
33
36
  'collection-type',
34
37
  'collection-fields',
35
38
  'field-labels',
39
+ 'option-labels',
36
40
  'model-values',
37
41
  'submit-button-label',
38
42
  'hide-submit-button',
@@ -94,8 +98,17 @@ class CollectionAdder extends SocketConsumer {
94
98
  messageContainer.classList.remove('error');
95
99
  messageContainer.textContent = '';
96
100
 
101
+ let radioInputs = [...this.querySelectorAll('.collection-adder-radio')];
102
+ let radioData = {};
103
+ for (let radio of radioInputs) {
104
+ if (radio.checked) {
105
+ radioData[radio.name] = radio.value;
106
+ }
107
+ }
108
+
97
109
  let newModelData = {
98
110
  ...this.modelFieldValues,
111
+ ...radioData,
99
112
  ...Object.fromEntries(
100
113
  [...this.querySelectorAll('.collection-adder-input')]
101
114
  .filter(input => input.value !== '')
@@ -171,6 +184,10 @@ class CollectionAdder extends SocketConsumer {
171
184
  fieldValues: modelFieldLabels
172
185
  } = this.getFieldDetails(this.getAttribute('field-labels'));
173
186
 
187
+ let {
188
+ fieldValues: optionLabels
189
+ } = this.getFieldDetails(this.getAttribute('option-labels'));
190
+
174
191
  this.modelFieldValues = modelFieldValues;
175
192
 
176
193
  if (this.collection) this.collection.destroy();
@@ -192,6 +209,7 @@ class CollectionAdder extends SocketConsumer {
192
209
  inputTypeParams = inputTypeParts[3] ? inputTypeParts[3].split(',') : [];
193
210
  }
194
211
  let inputLabel = modelFieldLabels[field] == null ? field : modelFieldLabels[field];
212
+ let optionLabel = optionLabels[field] == null ? 'Options' : optionLabels[field];
195
213
  if (inputType === 'checkbox') {
196
214
  items.push(
197
215
  `<label>
@@ -205,6 +223,18 @@ class CollectionAdder extends SocketConsumer {
205
223
  }" />
206
224
  </label>`
207
225
  );
226
+ } else if (inputType === 'select') {
227
+ items.push(
228
+ `<label>
229
+ ${
230
+ inputLabel
231
+ }
232
+ <select name="${field}" class="collection-adder-input collection-adder-select">
233
+ <option value="" selected disabled hidden>${optionLabel}</option>
234
+ ${inputTypeParams.map(param => `<option value="${param}">${param}</option>`).join('')}
235
+ </select>
236
+ </label>`
237
+ );
208
238
  } else if (inputType === 'text-select') {
209
239
  items.push(
210
240
  `<div class="collection-adder-select-text-row">
@@ -214,11 +244,34 @@ class CollectionAdder extends SocketConsumer {
214
244
  inputLabel
215
245
  }" />
216
246
  <select class="collection-adder-select">
217
- <option value="" selected disabled hidden>Options</option>
218
- ${inputTypeParams.map(param => `<option value="${param}">${param}</option>`)}
247
+ <option value="" selected disabled hidden>${optionLabel}</option>
248
+ ${inputTypeParams.map(param => `<option value="${param}">${param}</option>`).join('')}
219
249
  </select>
220
250
  </div>`
221
251
  );
252
+ } else if (inputType === 'radio') {
253
+ items.push(
254
+ `
255
+ ${
256
+ inputLabel
257
+ }
258
+ ${
259
+ inputTypeParams.map(
260
+ param => (
261
+ `<label>
262
+ <input class="collection-adder-radio" type="${
263
+ inputType
264
+ }" name="${
265
+ field
266
+ }" value=${
267
+ param
268
+ } />
269
+ ${param}
270
+ </label>`
271
+ )
272
+ ).join('')
273
+ }`
274
+ );
222
275
  } else if (inputType === 'textarea') {
223
276
  items.push(
224
277
  `<textarea class="collection-adder-input" name="${
package/model-input.js CHANGED
@@ -208,19 +208,23 @@ class ModelInput extends SocketConsumer {
208
208
  };
209
209
  }
210
210
 
211
+ updateInputElement(inputElement, fieldValue) {
212
+ let consumers = this.getAttribute('consumers');
213
+ if (inputElement.type === 'checkbox') {
214
+ let checked = !!fieldValue;
215
+ inputElement.checked = checked;
216
+ updateConsumerElements(this.parentElement, consumers, checked);
217
+ } else {
218
+ let defaultValue = this.getAttribute('default-value') || '';
219
+ inputElement.value = fieldValue == null ? defaultValue : fieldValue;
220
+ updateConsumerElements(this.parentElement, consumers, inputElement.value);
221
+ }
222
+ }
223
+
211
224
  syncInputElementWithModel(inputElement, model, fieldName, messageContainerElement) {
212
225
  if (model.isLoaded) {
213
- let consumers = this.getAttribute('consumers');
214
226
  let fieldValue = model.value[fieldName];
215
- if (inputElement.type === 'checkbox') {
216
- let checked = !!fieldValue;
217
- inputElement.checked = checked;
218
- updateConsumerElements(this.parentElement, consumers, checked);
219
- } else {
220
- let defaultValue = this.getAttribute('default-value') || '';
221
- inputElement.value = fieldValue == null ? defaultValue : fieldValue;
222
- updateConsumerElements(this.parentElement, consumers, inputElement.value);
223
- }
227
+ this.updateInputElement(inputElement, fieldValue);
224
228
  }
225
229
  let stopSaving = this.saveInputElementOnEdit(inputElement, model, fieldName, messageContainerElement);
226
230
  let changeConsumer = model.listener('change').createConsumer();
@@ -228,17 +232,8 @@ class ModelInput extends SocketConsumer {
228
232
  for await (let event of changeConsumer) {
229
233
  let hasFocus = document.activeElement === this.inputElement;
230
234
  if (event.resourceField !== fieldName || (hasFocus && !event.isRemote)) continue;
231
- let consumers = this.getAttribute('consumers');
232
235
  let fieldValue = model.value[fieldName];
233
- if (inputElement.type === 'checkbox') {
234
- let checked = !!fieldValue;
235
- inputElement.checked = checked;
236
- updateConsumerElements(this.parentElement, consumers, checked);
237
- } else {
238
- let defaultValue = this.getAttribute('default-value') || '';
239
- inputElement.value = fieldValue == null ? defaultValue : fieldValue;
240
- updateConsumerElements(this.parentElement, consumers, inputElement.value);
241
- }
236
+ this.updateInputElement(inputElement, fieldValue);
242
237
  }
243
238
  })();
244
239
  return () => {
@@ -304,7 +299,7 @@ class ModelInput extends SocketConsumer {
304
299
  inputElement.addEventListener('change', onInputChange);
305
300
 
306
301
  let onInputKeyUp;
307
- if (inputElement.type !== 'checkbox') {
302
+ if (inputElement.type !== 'checkbox' && inputElement.type !== 'select') {
308
303
  onInputKeyUp = async (event) => {
309
304
  debounce(async () => {
310
305
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasufy-components",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "description": "Components for Saasufy.",
5
5
  "main": "index.js",
6
6
  "scripts": {