saasufy-components 3.8.0 → 3.10.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 +12 -2
- package/app-router.js +18 -1
- package/input-provider.js +15 -6
- package/model-input.js +10 -7
- package/package.json +1 -1
- package/utils.js +5 -2
package/README.md
CHANGED
|
@@ -146,9 +146,12 @@ So for example, if your `index.html` file is served up from the URL `http://mywe
|
|
|
146
146
|
|
|
147
147
|
- `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.
|
|
148
148
|
- `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.
|
|
149
|
+
- `target-page`: A convenience attribute which can be used to programmatically change the `location.hash` in the address bar.
|
|
149
150
|
|
|
150
151
|
**Attributes of slotted page templates**
|
|
151
152
|
|
|
153
|
+
- `route-path`: The path of the page. Supports custom URL parameters in the format `/org/:orgId/user/:userId`.
|
|
154
|
+
- `partial-route`: An optional attribute which, if specified, will allow the `route-path` to be matched partially from the start. This can be used to ignore the ending of a path which is not relevant to the page in order to avoid unnecessary re-renders.
|
|
152
155
|
- `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.
|
|
153
156
|
- `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.
|
|
154
157
|
- `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.
|
|
@@ -416,6 +419,7 @@ Used for displaying and editing a single field of a model instance in real time.
|
|
|
416
419
|
- `height`: Allows you to set the height of the inner `input` element programmatically.
|
|
417
420
|
- `default-value`: A default value to show the user if the underlying model field's value is null or undefined. Note that setting a default value will not affect the underlying model's value.
|
|
418
421
|
- `value`: Used to set the model's value.
|
|
422
|
+
- `computable-value`: If this attribute is present, it will be possible to execute expressions using the `{{expression}}` syntax inside the `value` attribute.
|
|
419
423
|
- `hide-error-logs`: By default, this component will log errors to the console. If set, this attribute will suppress such errors from showing up on the console.
|
|
420
424
|
- `autocapitalize`: Can be `on` or `off` - It will set the auto-capitalize attribute on the inner input element. This is useful for mobile devices to enable or disable auto-capitalization of the first character which is typed into the input element.
|
|
421
425
|
- `autocorrect`: Can be `on` or `off` - It will set the auto-correct attribute on the inner input element. This is useful for mobile devices to enable or disable auto-correct.
|
|
@@ -523,6 +527,7 @@ This component can be configured to provide its data to multiple components (con
|
|
|
523
527
|
- `options`: A comma-separated list of options to provide - This only works if the input type is `select`.
|
|
524
528
|
- `height`: Allows you to set the height of the inner `input` element programmatically.
|
|
525
529
|
- `value`: Used to set the input's value.
|
|
530
|
+
- `computable-value`: If this attribute is present, it will be possible to execute expressions using the `{{expression}}` syntax inside the `value` attribute.
|
|
526
531
|
- `autocapitalize`: Can be `on` or `off` - It will set the auto-capitalize attribute on the inner input element. This is useful for mobile devices to enable or disable auto-capitalization of the first character which is typed into the input element.
|
|
527
532
|
- `autocorrect`: Can be `on` or `off` - It will set the auto-correct attribute on the inner input element. This is useful for mobile devices to enable or disable auto-correct.
|
|
528
533
|
|
|
@@ -543,21 +548,26 @@ This component can be configured to provide its data to multiple components (con
|
|
|
543
548
|
The following example shows how to bind multiple `input-provider` components to an `input-combiner`.
|
|
544
549
|
In this example, the `input-combiner` component forwards the combined, formatted values to the `collection-view-params` attribute of a `company-browser` component.
|
|
545
550
|
|
|
551
|
+
Note that the `value` inside the `provider-template` attribute of the `input-combiner` is an object with keys that represent the `name` attribute of the different source `input-provider` components.
|
|
552
|
+
The `combineFilters` function produces a combined query string (joined with `%AND%`) by iterating over the `value` object which holds different parts of the query as provided by different `input-provider` components.
|
|
553
|
+
|
|
546
554
|
```html
|
|
547
555
|
<input-provider
|
|
548
556
|
class="desc-input"
|
|
557
|
+
name="desc"
|
|
549
558
|
type="text"
|
|
550
559
|
consumers=".search-combiner"
|
|
551
|
-
provider-template="
|
|
560
|
+
provider-template="description contains (?i){{value}}"
|
|
552
561
|
value=""
|
|
553
562
|
placeholder="Description filter"
|
|
554
563
|
></input-provider>
|
|
555
564
|
|
|
556
565
|
<input-provider
|
|
557
566
|
class="city-input"
|
|
567
|
+
name="city"
|
|
558
568
|
type="text"
|
|
559
569
|
consumers=".search-combiner"
|
|
560
|
-
provider-template="city
|
|
570
|
+
provider-template="city contains (?i){{value}}"
|
|
561
571
|
value=""
|
|
562
572
|
placeholder="City filter"
|
|
563
573
|
></input-provider>
|
package/app-router.js
CHANGED
|
@@ -14,6 +14,7 @@ export class AppRouter extends SocketConsumer {
|
|
|
14
14
|
this.pageRouteInfos = [];
|
|
15
15
|
this.debounce = debouncer();
|
|
16
16
|
this.debounceDelay = DEFAULT_DEBOUNCE_DELAY;
|
|
17
|
+
this.lastPageState = null;
|
|
17
18
|
|
|
18
19
|
this.hashStartRegex = /^#/;
|
|
19
20
|
|
|
@@ -30,12 +31,15 @@ export class AppRouter extends SocketConsumer {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
this.pageRouteInfos = Object.keys(this.pages).map((route) => {
|
|
34
|
+
let pageTemplate = this.pages[route];
|
|
35
|
+
let allowPartialMatch = pageTemplate.hasAttribute('partial-route');
|
|
33
36
|
return {
|
|
34
|
-
regExp: new RegExp(`^${route.replace(/\/:[^\/]+/g, '/([^/]*)').replace(/\//g, '\\/')}
|
|
37
|
+
regExp: new RegExp(`^${route.replace(/\/:[^\/]+/g, '/([^/]*)').replace(/\//g, '\\/')}${allowPartialMatch ? '' : '$'}`, 'g'),
|
|
35
38
|
route,
|
|
36
39
|
params: [ ...route.matchAll(/:[^\/]*/g) ].map(paramMatch => paramMatch[0].replace(':', ''))
|
|
37
40
|
};
|
|
38
41
|
});
|
|
42
|
+
|
|
39
43
|
if (!this.socket || this.socket.state === this.socket.OPEN) {
|
|
40
44
|
this.debounce(this.renderCurrentPage, this.debounceDelay);
|
|
41
45
|
}
|
|
@@ -52,6 +56,7 @@ export class AppRouter extends SocketConsumer {
|
|
|
52
56
|
|
|
53
57
|
static get observedAttributes() {
|
|
54
58
|
return [
|
|
59
|
+
'target-page',
|
|
55
60
|
'default-page',
|
|
56
61
|
'debounce-delay',
|
|
57
62
|
'max-redirects'
|
|
@@ -65,6 +70,8 @@ export class AppRouter extends SocketConsumer {
|
|
|
65
70
|
} else {
|
|
66
71
|
this.debounceDelay = DEFAULT_DEBOUNCE_DELAY;
|
|
67
72
|
}
|
|
73
|
+
} else if (name === 'target-page') {
|
|
74
|
+
location.hash = newValue;
|
|
68
75
|
} else if (this.isReady) {
|
|
69
76
|
this.renderCurrentPage();
|
|
70
77
|
}
|
|
@@ -225,6 +232,16 @@ export class AppRouter extends SocketConsumer {
|
|
|
225
232
|
routeArgs = {};
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
|
|
236
|
+
let pageState = JSON.stringify({
|
|
237
|
+
route,
|
|
238
|
+
routeArgs
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (pageState === this.lastPageState) return;
|
|
242
|
+
|
|
243
|
+
this.lastPageState = pageState;
|
|
244
|
+
|
|
228
245
|
routerViewport.innerHTML = renderTemplate(
|
|
229
246
|
pageTemplate.innerHTML,
|
|
230
247
|
routeArgs,
|
package/input-provider.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
debouncer,
|
|
3
|
+
renderTemplate,
|
|
4
|
+
updateConsumerElements
|
|
5
|
+
} from './utils.js';
|
|
2
6
|
|
|
3
7
|
const DEFAULT_DEBOUNCE_DELAY = 800;
|
|
4
8
|
|
|
@@ -46,6 +50,9 @@ class InputProvider extends HTMLElement {
|
|
|
46
50
|
|
|
47
51
|
set value(newValue) {
|
|
48
52
|
if (this.inputElement) {
|
|
53
|
+
if (newValue && this.hasAttribute('computable-value')) {
|
|
54
|
+
newValue = renderTemplate(newValue);
|
|
55
|
+
}
|
|
49
56
|
let oldValue = this.inputElement.value;
|
|
50
57
|
this.inputElement.value = newValue;
|
|
51
58
|
if (oldValue !== newValue) {
|
|
@@ -135,21 +142,22 @@ class InputProvider extends HTMLElement {
|
|
|
135
142
|
|
|
136
143
|
let onInputChange = (event) => {
|
|
137
144
|
debounce(async () => {
|
|
145
|
+
let elementName = this.getAttribute('name');
|
|
138
146
|
let providerTemplate = this.getAttribute('provider-template');
|
|
139
147
|
if (event.target.value === this.lastValue) return;
|
|
140
148
|
if (inputElement.type === 'checkbox') {
|
|
141
149
|
let checked = !!inputElement.checked;
|
|
142
150
|
this.lastValue = checked;
|
|
143
|
-
updateConsumerElements(consumers, checked, providerTemplate);
|
|
151
|
+
updateConsumerElements(consumers, checked, providerTemplate, elementName);
|
|
144
152
|
return;
|
|
145
153
|
}
|
|
146
154
|
this.lastValue = event.target.value;
|
|
147
155
|
if (event.target.value === '') {
|
|
148
|
-
updateConsumerElements(consumers, '', providerTemplate);
|
|
156
|
+
updateConsumerElements(consumers, '', providerTemplate, elementName);
|
|
149
157
|
} else {
|
|
150
158
|
let targetValue = inputElement.type === 'number' ?
|
|
151
159
|
Number(event.target.value) : event.target.value;
|
|
152
|
-
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
160
|
+
updateConsumerElements(consumers, targetValue, providerTemplate, elementName);
|
|
153
161
|
}
|
|
154
162
|
}, debounceDelay);
|
|
155
163
|
};
|
|
@@ -159,15 +167,16 @@ class InputProvider extends HTMLElement {
|
|
|
159
167
|
if (inputElement.type !== 'checkbox') {
|
|
160
168
|
onInputKeyUp = async (event) => {
|
|
161
169
|
debounce(async () => {
|
|
170
|
+
let elementName = this.getAttribute('name');
|
|
162
171
|
let providerTemplate = this.getAttribute('provider-template');
|
|
163
172
|
if (event.target.value === this.lastValue) return;
|
|
164
173
|
this.lastValue = event.target.value;
|
|
165
174
|
if (event.target.value === '') {
|
|
166
|
-
updateConsumerElements(consumers, '', providerTemplate);
|
|
175
|
+
updateConsumerElements(consumers, '', providerTemplate, elementName);
|
|
167
176
|
} else {
|
|
168
177
|
let targetValue = inputElement.type === 'number' ?
|
|
169
178
|
Number(event.target.value) : event.target.value;
|
|
170
|
-
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
179
|
+
updateConsumerElements(consumers, targetValue, providerTemplate, elementName);
|
|
171
180
|
}
|
|
172
181
|
}, debounceDelay);
|
|
173
182
|
};
|
package/model-input.js
CHANGED
|
@@ -60,6 +60,9 @@ class ModelInput extends SocketConsumer {
|
|
|
60
60
|
|
|
61
61
|
set value(newValue) {
|
|
62
62
|
if (this.inputElement) {
|
|
63
|
+
if (newValue && this.hasAttribute('computable-value')) {
|
|
64
|
+
newValue = renderTemplate(newValue);
|
|
65
|
+
}
|
|
63
66
|
if (this.inputElement.type === 'checkbox') {
|
|
64
67
|
let oldValue = this.inputElement.checked;
|
|
65
68
|
this.inputElement.checked = newValue;
|
|
@@ -223,14 +226,14 @@ class ModelInput extends SocketConsumer {
|
|
|
223
226
|
if (inputElement.type === 'checkbox') {
|
|
224
227
|
let checked = !!fieldValue;
|
|
225
228
|
inputElement.checked = checked;
|
|
226
|
-
updateConsumerElements(consumers, checked, providerTemplate);
|
|
229
|
+
updateConsumerElements(consumers, checked, providerTemplate, this.getAttribute('name'));
|
|
227
230
|
} else {
|
|
228
231
|
let defaultValue = this.getAttribute('default-value') || '';
|
|
229
232
|
let elementValue = fieldValue == null ? defaultValue : fieldValue;
|
|
230
233
|
if (inputElement.type !== 'file') {
|
|
231
234
|
inputElement.value = elementValue;
|
|
232
235
|
}
|
|
233
|
-
updateConsumerElements(consumers, elementValue, providerTemplate);
|
|
236
|
+
updateConsumerElements(consumers, elementValue, providerTemplate, this.getAttribute('name'));
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
239
|
|
|
@@ -299,7 +302,7 @@ class ModelInput extends SocketConsumer {
|
|
|
299
302
|
try {
|
|
300
303
|
if (inputElement.type === 'checkbox') {
|
|
301
304
|
let checked = !!inputElement.checked;
|
|
302
|
-
updateConsumerElements(consumers, checked, providerTemplate);
|
|
305
|
+
updateConsumerElements(consumers, checked, providerTemplate, this.getAttribute('name'));
|
|
303
306
|
await model.update(fieldName, checked);
|
|
304
307
|
inputElement.classList.remove(errorStyleClass);
|
|
305
308
|
inputElement.classList.add(successStyleClass);
|
|
@@ -323,12 +326,12 @@ class ModelInput extends SocketConsumer {
|
|
|
323
326
|
inputValue = event.target.value;
|
|
324
327
|
}
|
|
325
328
|
if (inputValue === '') {
|
|
326
|
-
updateConsumerElements(consumers, '', providerTemplate);
|
|
329
|
+
updateConsumerElements(consumers, '', providerTemplate, this.getAttribute('name'));
|
|
327
330
|
await model.delete(fieldName);
|
|
328
331
|
} else {
|
|
329
332
|
let targetValue = inputElement.type === 'number' ?
|
|
330
333
|
Number(inputValue) : inputValue;
|
|
331
|
-
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
334
|
+
updateConsumerElements(consumers, targetValue, providerTemplate, this.getAttribute('name'));
|
|
332
335
|
await model.update(fieldName, targetValue);
|
|
333
336
|
}
|
|
334
337
|
inputElement.classList.remove(errorStyleClass);
|
|
@@ -353,11 +356,11 @@ class ModelInput extends SocketConsumer {
|
|
|
353
356
|
try {
|
|
354
357
|
if (event.target.value === '') {
|
|
355
358
|
await model.delete(fieldName);
|
|
356
|
-
updateConsumerElements(consumers, '', providerTemplate);
|
|
359
|
+
updateConsumerElements(consumers, '', providerTemplate, this.getAttribute('name'));
|
|
357
360
|
} else {
|
|
358
361
|
let targetValue = inputElement.type === 'number' ?
|
|
359
362
|
Number(event.target.value) : event.target.value;
|
|
360
|
-
updateConsumerElements(consumers, targetValue, providerTemplate);
|
|
363
|
+
updateConsumerElements(consumers, targetValue, providerTemplate, this.getAttribute('name'));
|
|
361
364
|
await model.update(fieldName, targetValue);
|
|
362
365
|
}
|
|
363
366
|
inputElement.classList.remove(errorStyleClass);
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -251,7 +251,7 @@ export function renderTemplate(templateString, data, socket) {
|
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
export function updateConsumerElements(consumers, value, template) {
|
|
254
|
+
export function updateConsumerElements(consumers, value, template, sourceElementName) {
|
|
255
255
|
if (consumers) {
|
|
256
256
|
let consumerParts = consumers.split(',')
|
|
257
257
|
.filter(part => part)
|
|
@@ -297,7 +297,10 @@ export function updateConsumerElements(consumers, value, template) {
|
|
|
297
297
|
} else if (element.nodeName === 'MODEL-INPUT') {
|
|
298
298
|
element.value = value;
|
|
299
299
|
} else if (element.nodeName === 'INPUT-COMBINER') {
|
|
300
|
-
element.setAttribute(
|
|
300
|
+
element.setAttribute(
|
|
301
|
+
'value',
|
|
302
|
+
sourceElementName ? `${sourceElementName}:${value}` : value
|
|
303
|
+
);
|
|
301
304
|
} else {
|
|
302
305
|
element.innerHTML = value;
|
|
303
306
|
}
|