saasufy-components 1.3.1 → 1.5.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 +5 -1
- package/collection-browser.js +15 -14
- package/input-provider.js +207 -0
- package/model-input.js +5 -4
- package/model-text.js +2 -1
- package/model-viewer.js +114 -0
- package/package.json +1 -1
- package/socket.js +32 -5
- package/utils.js +1 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ A form component for inserting data into collections.
|
|
|
13
13
|
|
|
14
14
|
### collection-browser
|
|
15
15
|
|
|
16
|
-
Used for rendering collections as lists, tables or other sequences based on a specific view.
|
|
16
|
+
Used for rendering collections as lists, tables or other sequences based on a specific view using a template.
|
|
17
17
|
Supports pagination by allowing you to specify custom buttons or links to navigate between pages.
|
|
18
18
|
Can also perform basic CRUD operations such as deleting or creating records.
|
|
19
19
|
|
|
@@ -21,6 +21,10 @@ Can also perform basic CRUD operations such as deleting or creating records.
|
|
|
21
21
|
|
|
22
22
|
Used for displaying and editing a field of a model instance in real time.
|
|
23
23
|
|
|
24
|
+
### model-viewer
|
|
25
|
+
|
|
26
|
+
Used for rendering models using a template.
|
|
27
|
+
|
|
24
28
|
### confirm-modal
|
|
25
29
|
|
|
26
30
|
A modal component to prompt the user for confirmation before performing sensitive operations.
|
package/collection-browser.js
CHANGED
|
@@ -90,7 +90,8 @@ class CollectionBrowser extends SocketConsumer {
|
|
|
90
90
|
'collection-view',
|
|
91
91
|
'collection-view-params',
|
|
92
92
|
'collection-page-size',
|
|
93
|
-
'collection-page-offset'
|
|
93
|
+
'collection-page-offset',
|
|
94
|
+
'hide-error-logs'
|
|
94
95
|
];
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -148,23 +149,23 @@ class CollectionBrowser extends SocketConsumer {
|
|
|
148
149
|
|
|
149
150
|
renderList() {
|
|
150
151
|
if (!this.collection || !this.collection.isLoaded) return;
|
|
151
|
-
let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
|
|
152
|
-
if (!itemTemplate) return;
|
|
153
152
|
let viewportNode = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
|
|
154
153
|
if (!viewportNode) return;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
for (let modelItem of this.collection.value) {
|
|
158
|
-
let itemString = renderTemplate(
|
|
159
|
-
itemTemplate.innerHTML,
|
|
160
|
-
{ [this.collection.type]: modelItem }
|
|
161
|
-
);
|
|
162
|
-
items.push(itemString);
|
|
163
|
-
}
|
|
154
|
+
|
|
155
|
+
let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
|
|
164
156
|
let noItemTemplate = this.shadowRoot.querySelector('slot[name="no-item"]').assignedNodes()[0];
|
|
165
|
-
|
|
157
|
+
|
|
158
|
+
if (noItemTemplate && !this.collection.value.length) {
|
|
166
159
|
viewportNode.innerHTML = noItemTemplate.innerHTML;
|
|
167
|
-
} else {
|
|
160
|
+
} else if (itemTemplate) {
|
|
161
|
+
let items = [];
|
|
162
|
+
for (let modelItem of this.collection.value) {
|
|
163
|
+
let itemString = renderTemplate(
|
|
164
|
+
itemTemplate.innerHTML,
|
|
165
|
+
{ [this.collection.type]: modelItem }
|
|
166
|
+
);
|
|
167
|
+
items.push(itemString);
|
|
168
|
+
}
|
|
168
169
|
viewportNode.innerHTML = items.join('');
|
|
169
170
|
}
|
|
170
171
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { debouncer } from './utils.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DEBOUNCE_DELAY = 800;
|
|
4
|
+
|
|
5
|
+
class InputProvider extends HTMLElement {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.isReady = false;
|
|
9
|
+
this.inputElement = null;
|
|
10
|
+
}
|
|
11
|
+
connectedCallback() {
|
|
12
|
+
this.isReady = true;
|
|
13
|
+
this.destroy = () => {};
|
|
14
|
+
this.render();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnectedCallback() {
|
|
18
|
+
this.destroy();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static get observedAttributes() {
|
|
22
|
+
return [
|
|
23
|
+
'input-id',
|
|
24
|
+
'list',
|
|
25
|
+
'type',
|
|
26
|
+
'placeholder',
|
|
27
|
+
'consumers',
|
|
28
|
+
'debounce-delay',
|
|
29
|
+
'options',
|
|
30
|
+
'value',
|
|
31
|
+
'height'
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
36
|
+
if (!this.isReady) return;
|
|
37
|
+
this.destroy();
|
|
38
|
+
this.render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
triggerChange() {
|
|
42
|
+
this.inputElement && this.inputElement.dispatchEvent(new Event('change'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
set value(newValue) {
|
|
46
|
+
if (this.inputElement) {
|
|
47
|
+
let oldValue = this.inputElement.value;
|
|
48
|
+
this.inputElement.value = newValue;
|
|
49
|
+
if (oldValue !== newValue) {
|
|
50
|
+
this.triggerChange();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get value() {
|
|
56
|
+
return (this.inputElement && this.inputElement.value) || '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render() {
|
|
60
|
+
this.innerHTML = '';
|
|
61
|
+
let inputId = this.getAttribute('input-id');
|
|
62
|
+
let inputList = this.getAttribute('list');
|
|
63
|
+
let type = this.getAttribute('type') || 'text';
|
|
64
|
+
let placeholder = this.getAttribute('placeholder');
|
|
65
|
+
let options = this.getAttribute('options');
|
|
66
|
+
let height = this.getAttribute('height');
|
|
67
|
+
let value = this.getAttribute('value') || '';
|
|
68
|
+
|
|
69
|
+
let elementType;
|
|
70
|
+
if (type === 'select') {
|
|
71
|
+
elementType = 'select';
|
|
72
|
+
} else if (type === 'textarea') {
|
|
73
|
+
elementType = 'textarea';
|
|
74
|
+
} else {
|
|
75
|
+
elementType = 'input';
|
|
76
|
+
}
|
|
77
|
+
this.inputElement = document.createElement(elementType);
|
|
78
|
+
if (type === 'select') {
|
|
79
|
+
if (options) {
|
|
80
|
+
let optionElements = options
|
|
81
|
+
.split(',')
|
|
82
|
+
.map((option) => {
|
|
83
|
+
return `<option value="${option}">${option}</option>`;
|
|
84
|
+
})
|
|
85
|
+
.join('');
|
|
86
|
+
this.inputElement.innerHTML = optionElements;
|
|
87
|
+
}
|
|
88
|
+
} else if (elementType === 'input') {
|
|
89
|
+
if (inputList) {
|
|
90
|
+
this.inputElement.setAttribute('list', inputList)
|
|
91
|
+
}
|
|
92
|
+
this.inputElement.type = type;
|
|
93
|
+
}
|
|
94
|
+
if (height) {
|
|
95
|
+
this.inputElement.style.height = height;
|
|
96
|
+
}
|
|
97
|
+
if (inputId) {
|
|
98
|
+
this.inputElement.setAttribute('id', inputId);
|
|
99
|
+
}
|
|
100
|
+
if (placeholder) {
|
|
101
|
+
this.inputElement.setAttribute('placeholder', placeholder);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.appendChild(this.inputElement);
|
|
105
|
+
|
|
106
|
+
let destroyHandlers = this.updateConsumerElementsOnEdit();
|
|
107
|
+
|
|
108
|
+
if (value) {
|
|
109
|
+
this.value = value;
|
|
110
|
+
}
|
|
111
|
+
this.destroy = () => {
|
|
112
|
+
destroyHandlers();
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
updateConsumerElements(consumers, value) {
|
|
117
|
+
let parentElement = this.parentElement;
|
|
118
|
+
if (parentElement) {
|
|
119
|
+
let consumerParts = consumers.split(',')
|
|
120
|
+
.filter(part => part)
|
|
121
|
+
.map(part => {
|
|
122
|
+
part = part.trim();
|
|
123
|
+
return part.split(':').map(subPart => subPart.trim());
|
|
124
|
+
})
|
|
125
|
+
.filter(([ selector, attributeName ]) => selector);
|
|
126
|
+
|
|
127
|
+
for (let [ selector, attributeName ] of consumerParts) {
|
|
128
|
+
let matchingElements = parentElement.querySelectorAll(selector);
|
|
129
|
+
if (attributeName) {
|
|
130
|
+
for (let element of matchingElements) {
|
|
131
|
+
element.setAttribute(attributeName, value);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
for (let element of matchingElements) {
|
|
135
|
+
if (element.nodeType === 'INPUT') {
|
|
136
|
+
if (element.type === 'checkbox') {
|
|
137
|
+
if (value) {
|
|
138
|
+
element.setAttribute('checked', '');
|
|
139
|
+
} else {
|
|
140
|
+
element.removeAttribute('checked');
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
element.value = value;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
element.innerHTML = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateConsumerElementsOnEdit() {
|
|
155
|
+
let consumers = this.getAttribute('consumers');
|
|
156
|
+
if (!consumers) return;
|
|
157
|
+
|
|
158
|
+
let inputElement = this.inputElement;
|
|
159
|
+
|
|
160
|
+
let debounceDelay = this.getAttribute('debounce-delay');
|
|
161
|
+
debounceDelay = debounceDelay ? Number(debounceDelay) : DEFAULT_DEBOUNCE_DELAY;
|
|
162
|
+
let debounce = debouncer();
|
|
163
|
+
|
|
164
|
+
let onInputChange = (event) => {
|
|
165
|
+
debounce(async () => {
|
|
166
|
+
if (event.target.value === this.lastValue) return;
|
|
167
|
+
this.lastValue = event.target.value;
|
|
168
|
+
if (inputElement.type === 'checkbox') {
|
|
169
|
+
this.updateConsumerElements(consumers, !!inputElement.checked);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (event.target.value === '') {
|
|
173
|
+
this.updateConsumerElements(consumers, '');
|
|
174
|
+
} else {
|
|
175
|
+
let targetValue = inputElement.type === 'number' ?
|
|
176
|
+
Number(event.target.value) : event.target.value;
|
|
177
|
+
this.updateConsumerElements(consumers, targetValue);
|
|
178
|
+
}
|
|
179
|
+
}, debounceDelay);
|
|
180
|
+
};
|
|
181
|
+
inputElement.addEventListener('change', onInputChange);
|
|
182
|
+
|
|
183
|
+
let onInputKeyUp;
|
|
184
|
+
if (inputElement.type !== 'checkbox') {
|
|
185
|
+
onInputKeyUp = async (event) => {
|
|
186
|
+
debounce(async () => {
|
|
187
|
+
if (event.target.value === this.lastValue) return;
|
|
188
|
+
this.lastValue = event.target.value;
|
|
189
|
+
if (event.target.value === '') {
|
|
190
|
+
this.updateConsumerElements(consumers, '');
|
|
191
|
+
} else {
|
|
192
|
+
let targetValue = inputElement.type === 'number' ?
|
|
193
|
+
Number(event.target.value) : event.target.value;
|
|
194
|
+
this.updateConsumerElements(consumers, targetValue);
|
|
195
|
+
}
|
|
196
|
+
}, debounceDelay);
|
|
197
|
+
};
|
|
198
|
+
inputElement.addEventListener('keyup', onInputKeyUp);
|
|
199
|
+
}
|
|
200
|
+
return () => {
|
|
201
|
+
inputElement.removeEventListener('change', onInputChange);
|
|
202
|
+
onInputKeyUp && inputElement.removeEventListener('keyup', onInputKeyUp);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
window.customElements.define('input-provider', InputProvider);
|
package/model-input.js
CHANGED
|
@@ -21,7 +21,7 @@ class ModelInput extends SocketConsumer {
|
|
|
21
21
|
static get observedAttributes() {
|
|
22
22
|
return [
|
|
23
23
|
'input-id',
|
|
24
|
-
'
|
|
24
|
+
'list',
|
|
25
25
|
'model-instance-property',
|
|
26
26
|
'type',
|
|
27
27
|
'placeholder',
|
|
@@ -31,7 +31,8 @@ class ModelInput extends SocketConsumer {
|
|
|
31
31
|
'model-field',
|
|
32
32
|
'debounce-delay',
|
|
33
33
|
'options',
|
|
34
|
-
'height'
|
|
34
|
+
'height',
|
|
35
|
+
'hide-error-logs'
|
|
35
36
|
];
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -64,8 +65,8 @@ class ModelInput extends SocketConsumer {
|
|
|
64
65
|
let modelInstanceProperty = this.getAttribute('model-instance-property');
|
|
65
66
|
let showErrorMessage = this.hasAttribute('show-error-message');
|
|
66
67
|
let inputId = this.getAttribute('input-id');
|
|
67
|
-
let inputList = this.getAttribute('
|
|
68
|
-
let type = this.getAttribute('type');
|
|
68
|
+
let inputList = this.getAttribute('list');
|
|
69
|
+
let type = this.getAttribute('type') || 'text';
|
|
69
70
|
let placeholder = this.getAttribute('placeholder');
|
|
70
71
|
let modelType = this.getAttribute('model-type');
|
|
71
72
|
let modelId = this.getAttribute('model-id');
|
package/model-text.js
CHANGED
package/model-viewer.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { SocketConsumer } from './socket.js';
|
|
2
|
+
import AGModel from '/node_modules/ag-model/ag-model.js';
|
|
3
|
+
import { renderTemplate } from './utils.js';
|
|
4
|
+
|
|
5
|
+
class ModelViewer extends SocketConsumer {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.attachShadow({ mode: 'open' });
|
|
9
|
+
|
|
10
|
+
this.handleSlotChangeEvent = () => {
|
|
11
|
+
this.renderItem();
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
this.isReady = true;
|
|
17
|
+
this.socket = this.getSocket();
|
|
18
|
+
this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
disconnectedCallback() {
|
|
23
|
+
if (this.model) this.model.destroy();
|
|
24
|
+
this.shadowRoot.removeEventListener('slotchange', this.handleSlotChangeEvent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static get observedAttributes() {
|
|
28
|
+
return [
|
|
29
|
+
'model-type',
|
|
30
|
+
'model-id',
|
|
31
|
+
'model-fields',
|
|
32
|
+
'hide-error-logs'
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
37
|
+
if (!this.isReady) return;
|
|
38
|
+
this.render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
modelValueExists(modelValue) {
|
|
42
|
+
let { id, ...otherProps } = modelValue;
|
|
43
|
+
return !!Object.values(otherProps).filter(value => value != null).length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
renderItem() {
|
|
47
|
+
if (!this.model || !this.model.isLoaded) return;
|
|
48
|
+
let viewportNode = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
|
|
49
|
+
if (!viewportNode) return;
|
|
50
|
+
|
|
51
|
+
let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
|
|
52
|
+
let modelValue = this.model.value;
|
|
53
|
+
|
|
54
|
+
let noItemTemplate = this.shadowRoot.querySelector('slot[name="no-item"]').assignedNodes()[0];
|
|
55
|
+
if (noItemTemplate && !this.modelValueExists(modelValue)) {
|
|
56
|
+
viewportNode.innerHTML = noItemTemplate.innerHTML;
|
|
57
|
+
} else if (itemTemplate) {
|
|
58
|
+
let itemString = renderTemplate(
|
|
59
|
+
itemTemplate.innerHTML,
|
|
60
|
+
{ [this.model.type]: modelValue }
|
|
61
|
+
);
|
|
62
|
+
viewportNode.innerHTML = itemString;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render() {
|
|
67
|
+
let modelType = this.getAttribute('model-type');
|
|
68
|
+
let modelId = this.getAttribute('model-id');
|
|
69
|
+
let modelFields = this.getAttribute('model-fields') || '';
|
|
70
|
+
let hideErrorLogs = this.hasAttribute('hide-error-logs');
|
|
71
|
+
|
|
72
|
+
if (this.model) this.model.destroy();
|
|
73
|
+
|
|
74
|
+
this.model = new AGModel({
|
|
75
|
+
socket: this.socket,
|
|
76
|
+
type: modelType,
|
|
77
|
+
id: modelId,
|
|
78
|
+
fields: modelFields.split(',').map(field => field.trim()).filter(field => field)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.shadowRoot.innerHTML = `
|
|
82
|
+
<slot name="item"></slot>
|
|
83
|
+
<slot name="no-item"></slot>
|
|
84
|
+
<slot name="viewport"></slot>
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
(async () => {
|
|
88
|
+
await this.model.listener('load').once();
|
|
89
|
+
if (!this.modelValueExists(this.model.value)) {
|
|
90
|
+
this.renderItem();
|
|
91
|
+
}
|
|
92
|
+
this.setAttribute('is-loaded', '');
|
|
93
|
+
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
(async () => {
|
|
97
|
+
for await (let event of this.model.listener('change')) {
|
|
98
|
+
this.renderItem();
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
if (!hideErrorLogs) {
|
|
103
|
+
(async () => {
|
|
104
|
+
for await (let { error } of this.model.listener('error')) {
|
|
105
|
+
console.error(
|
|
106
|
+
`Model viewer encountered an error: ${error.message}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
window.customElements.define('model-viewer', ModelViewer);
|
package/package.json
CHANGED
package/socket.js
CHANGED
|
@@ -41,7 +41,14 @@ export class SocketProvider extends HTMLElement {
|
|
|
41
41
|
|
|
42
42
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
43
43
|
if (!this.isReady) return;
|
|
44
|
-
this.
|
|
44
|
+
let sanitizedURL = this.getSanitizedURL();
|
|
45
|
+
if (
|
|
46
|
+
this.saasufySocket &&
|
|
47
|
+
this.saasufySocket.transport &&
|
|
48
|
+
this.saasufySocket.transport.uri() !== sanitizedURL
|
|
49
|
+
) {
|
|
50
|
+
this.saasufySocket.disconnect();
|
|
51
|
+
}
|
|
45
52
|
this.getSocket();
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -53,14 +60,24 @@ export class SocketProvider extends HTMLElement {
|
|
|
53
60
|
|
|
54
61
|
getSocket() {
|
|
55
62
|
if (this.saasufySocket) {
|
|
63
|
+
this.saasufySocket.options = {
|
|
64
|
+
...this.saasufySocket.options,
|
|
65
|
+
...this.getSocketOptions()
|
|
66
|
+
};
|
|
67
|
+
this.saasufySocket.connect();
|
|
56
68
|
return this.saasufySocket;
|
|
57
69
|
}
|
|
58
70
|
return this.createSocket();
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
let authTokenName = this.getAttribute('auth-token-name') || null;
|
|
73
|
+
getSanitizedURL() {
|
|
63
74
|
let url = this.getAttribute('url') || '';
|
|
75
|
+
return url.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getSocketOptions() {
|
|
79
|
+
let authTokenName = this.getAttribute('auth-token-name') || null;
|
|
80
|
+
let url = this.getSanitizedURL();
|
|
64
81
|
let urlOptions;
|
|
65
82
|
let matchedList = url.match(urlPartsRegExp);
|
|
66
83
|
if (matchedList) {
|
|
@@ -75,12 +92,22 @@ export class SocketProvider extends HTMLElement {
|
|
|
75
92
|
path
|
|
76
93
|
};
|
|
77
94
|
} else {
|
|
95
|
+
if (url) {
|
|
96
|
+
throw new Error(`The specified URL ${url} was invalid`);
|
|
97
|
+
}
|
|
78
98
|
urlOptions = {};
|
|
79
99
|
}
|
|
80
|
-
|
|
100
|
+
let socketOptions = {
|
|
81
101
|
authTokenName,
|
|
82
102
|
...urlOptions
|
|
83
|
-
}
|
|
103
|
+
};
|
|
104
|
+
socketOptions.autoConnect = !!url;
|
|
105
|
+
return socketOptions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
createSocket() {
|
|
109
|
+
let socketOptions = this.getSocketOptions();
|
|
110
|
+
this.saasufySocket = create(socketOptions);
|
|
84
111
|
|
|
85
112
|
return this.saasufySocket;
|
|
86
113
|
}
|
package/utils.js
CHANGED
|
@@ -164,6 +164,7 @@ let templateFormatters = {
|
|
|
164
164
|
let valueString = String(value);
|
|
165
165
|
return `${valueString.slice(0, 1).toUpperCase()}${valueString.slice(1)}`;
|
|
166
166
|
},
|
|
167
|
+
trim: (value) => String(value).trim(),
|
|
167
168
|
fallback: (...args) => {
|
|
168
169
|
return args.filter(arg => arg)[0];
|
|
169
170
|
}
|