structured-fw 1.0.7 → 1.0.8
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 +45 -0
- package/build/system/EventEmitter.d.ts +3 -3
- package/build/system/EventEmitter.js +3 -3
- package/build/system/Types.d.ts +1 -1
- package/build/system/Util.d.ts +1 -1
- package/build/system/Util.js +18 -2
- package/build/system/client/ClientComponent.js +52 -19
- package/build/system/server/Request.js +3 -3
- package/build/system/server/dom/DOMNode.js +2 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -689,6 +689,51 @@ then in ComponentName.html:
|
|
|
689
689
|
<div data-if="showDiv()"></div>
|
|
690
690
|
```
|
|
691
691
|
|
|
692
|
+
### Models
|
|
693
|
+
Every component client side part has it's own data store accessed by this.store. That is the primary way of storing data for your components client side, because it will survive on redraw and you can subscribe to data changes in the store using `this.store.onChange`.
|
|
694
|
+
|
|
695
|
+
That being said, we need an easy way to use input fields to set values in the store, as that's often what we do when we make web apps.
|
|
696
|
+
|
|
697
|
+
You can, of course, bind an even listener to the input and set the store value, that's quite easy, but we can accomplis this using `data-model` attribute.
|
|
698
|
+
|
|
699
|
+
You can add data-model to any HTMLInput within your component, and it will automatically update the store on input value change.
|
|
700
|
+
|
|
701
|
+
For example:
|
|
702
|
+
|
|
703
|
+
Direct key:\
|
|
704
|
+
`<input type="text" data-model="name">`
|
|
705
|
+
|
|
706
|
+
Direct key access:\
|
|
707
|
+
`this.store.get<string>('name')`
|
|
708
|
+
`// returns string`
|
|
709
|
+
|
|
710
|
+
Nested keys:\
|
|
711
|
+
`<input type="text" data-model="user[name]">`
|
|
712
|
+
|
|
713
|
+
Nested key access:\
|
|
714
|
+
`this.store.get<LooseObject>('user')`
|
|
715
|
+
`// returns { name: string }`
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
You can nest the keys to any depth, or even make the value an array member if you end the key with `[]`, for example:
|
|
719
|
+
```
|
|
720
|
+
<input type="text" data-model="user[hobbies][]">
|
|
721
|
+
this.store.get<LooseObject>('user')
|
|
722
|
+
// returns { user: { hobbies: Array<string> } }
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
You can use two modifier attributes with `data-model`:
|
|
726
|
+
- `data-type`
|
|
727
|
+
- `data-nullable`
|
|
728
|
+
|
|
729
|
+
`data-type` - cast value to given type. Can be one of number | boolean | string, string has no effect as HTMLInput values are already a string by default.\
|
|
730
|
+
If number: if input is empty or value casts to `NaN` then `0` (unless `data-nullable` in which case `null`), othrwise the casted number (uses parseFloat so it works with decimal numbers)\
|
|
731
|
+
If boolean: `"1"` and `"true"` casted to `true`, otherwise `false`\
|
|
732
|
+
If string no type casting is attempted.
|
|
733
|
+
|
|
734
|
+
`data-nullable` - value of this attribute is unused, as long as the attribute is present on the input, empty values will be casted to `null`. Can be used in conjunction with `data-type`
|
|
735
|
+
|
|
736
|
+
|
|
692
737
|
### Layout
|
|
693
738
|
Prior to version 0.8.7:
|
|
694
739
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitterCallback } from "./Types.js";
|
|
2
2
|
export declare class EventEmitter<T extends Record<string, any> = Record<string, any>> {
|
|
3
|
-
protected listeners: Partial<Record<keyof T, Array<EventEmitterCallback<any>>>>;
|
|
4
|
-
on<K extends keyof T
|
|
5
|
-
emit(eventName: keyof T, payload?: any): void;
|
|
3
|
+
protected listeners: Partial<Record<Extract<keyof T, string>, Array<EventEmitterCallback<any>>>>;
|
|
4
|
+
on<K extends Extract<keyof T, string>>(eventName: K, callback: EventEmitterCallback<T[K]>): void;
|
|
5
|
+
emit(eventName: Extract<keyof T, string>, payload?: any): void;
|
|
6
6
|
off(eventName: keyof T, callback: EventEmitterCallback<any>): void;
|
|
7
7
|
}
|
|
@@ -9,9 +9,9 @@ export class EventEmitter {
|
|
|
9
9
|
this.listeners[eventName].push(callback);
|
|
10
10
|
}
|
|
11
11
|
emit(eventName, payload) {
|
|
12
|
-
if (Array.isArray(this.listeners[eventName])) {
|
|
13
|
-
this.listeners[eventName].forEach((callback) => {
|
|
14
|
-
callback(payload);
|
|
12
|
+
if (Array.isArray(this.listeners[eventName]) || Array.isArray(this.listeners['*'])) {
|
|
13
|
+
(this.listeners[eventName] || []).concat(this.listeners['*'] || []).forEach((callback) => {
|
|
14
|
+
callback(payload, eventName);
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
}
|
package/build/system/Types.d.ts
CHANGED
|
@@ -172,4 +172,4 @@ export type ClientComponentTransition = {
|
|
|
172
172
|
};
|
|
173
173
|
export type ClientComponentTransitionEvent = 'show' | 'hide';
|
|
174
174
|
export type ClientComponentTransitions = Record<ClientComponentTransitionEvent, ClientComponentTransition>;
|
|
175
|
-
export type EventEmitterCallback<T> = (payload: T) => void;
|
|
175
|
+
export type EventEmitterCallback<T> = (payload: T, eventName: string) => void;
|
package/build/system/Util.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function toCamelCase(dataKey: string, separator?: string): string
|
|
|
6
6
|
export declare function toSnakeCase(str: string, joinWith?: string): string;
|
|
7
7
|
export declare function capitalize(str: string): string;
|
|
8
8
|
export declare function isAsync(fn: Function): boolean;
|
|
9
|
-
export declare function randomString(len: number): string;
|
|
9
|
+
export declare function randomString(len: number, method?: 'alphanumeric' | 'numbers' | 'letters' | 'lettersUppercase' | 'lettersLowercase'): string;
|
|
10
10
|
export declare function unique<T>(arr: Array<T>): Array<T>;
|
|
11
11
|
export declare function stripTags(contentWithHTML: string, keepTags?: Array<string>): string;
|
|
12
12
|
export declare function attributeValueToString(key: string, value: any): string;
|
package/build/system/Util.js
CHANGED
|
@@ -168,7 +168,7 @@ export function capitalize(str) {
|
|
|
168
168
|
export function isAsync(fn) {
|
|
169
169
|
return fn.constructor.name === 'AsyncFunction';
|
|
170
170
|
}
|
|
171
|
-
export function randomString(len) {
|
|
171
|
+
export function randomString(len, method = 'alphanumeric') {
|
|
172
172
|
const charCodes = new Uint8Array(len);
|
|
173
173
|
const generators = [
|
|
174
174
|
function () {
|
|
@@ -181,8 +181,24 @@ export function randomString(len) {
|
|
|
181
181
|
return 48 + Math.floor(Math.random() * 10);
|
|
182
182
|
}
|
|
183
183
|
];
|
|
184
|
+
const generatorsUsed = [];
|
|
185
|
+
if (method === 'alphanumeric') {
|
|
186
|
+
generatorsUsed.push(...generators);
|
|
187
|
+
}
|
|
188
|
+
else if (method === 'numbers') {
|
|
189
|
+
generatorsUsed.push(generators[2]);
|
|
190
|
+
}
|
|
191
|
+
else if (method === 'letters') {
|
|
192
|
+
generatorsUsed.push(...generators.slice(0, 2));
|
|
193
|
+
}
|
|
194
|
+
else if (method === 'lettersLowercase') {
|
|
195
|
+
generatorsUsed.push(generators[1]);
|
|
196
|
+
}
|
|
197
|
+
else if (method === 'lettersUppercase') {
|
|
198
|
+
generatorsUsed.push(generators[0]);
|
|
199
|
+
}
|
|
184
200
|
for (let i = 0; i < len; i++) {
|
|
185
|
-
charCodes[i] =
|
|
201
|
+
charCodes[i] = generatorsUsed[Math.floor(Math.random() * generatorsUsed.length)]();
|
|
186
202
|
}
|
|
187
203
|
return String.fromCodePoint(...charCodes);
|
|
188
204
|
}
|
|
@@ -277,21 +277,6 @@ export class ClientComponent extends EventEmitter {
|
|
|
277
277
|
if (node === undefined) {
|
|
278
278
|
node = this.domNode;
|
|
279
279
|
}
|
|
280
|
-
const modelData = (node) => {
|
|
281
|
-
const field = node.getAttribute('data-model');
|
|
282
|
-
if (field) {
|
|
283
|
-
const isCheckbox = node.tagName === 'INPUT' && node.type === 'checkbox';
|
|
284
|
-
const valueRaw = isCheckbox ? node.checked : node.value;
|
|
285
|
-
const value = queryStringDecodedSetValue(field, valueRaw);
|
|
286
|
-
return value;
|
|
287
|
-
}
|
|
288
|
-
return {};
|
|
289
|
-
};
|
|
290
|
-
const update = (data) => {
|
|
291
|
-
objectEach(data, (key, val) => {
|
|
292
|
-
this.store.set(key, val);
|
|
293
|
-
});
|
|
294
|
-
};
|
|
295
280
|
if (node.hasAttribute('data-model') && (node.tagName === 'INPUT' || node.tagName === 'SELECT' || node.tagName === 'TEXTAREA')) {
|
|
296
281
|
modelNodes.push(node);
|
|
297
282
|
}
|
|
@@ -303,6 +288,53 @@ export class ClientComponent extends EventEmitter {
|
|
|
303
288
|
});
|
|
304
289
|
}
|
|
305
290
|
if (isSelf) {
|
|
291
|
+
const modelData = (node) => {
|
|
292
|
+
const field = node.getAttribute('data-model');
|
|
293
|
+
if (field) {
|
|
294
|
+
const isCheckbox = node.tagName === 'INPUT' && node.type === 'checkbox';
|
|
295
|
+
const valueRaw = isCheckbox ? node.checked : node.value;
|
|
296
|
+
let valueCasted = valueRaw;
|
|
297
|
+
if (!isCheckbox && typeof valueRaw === 'string') {
|
|
298
|
+
const dataType = isCheckbox ? 'boolean' : node.getAttribute('data-type') || 'string';
|
|
299
|
+
const nullable = node.hasAttribute('data-nullable');
|
|
300
|
+
if (nullable && valueRaw.trim().length === 0) {
|
|
301
|
+
valueCasted = null;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
if (dataType === 'number') {
|
|
305
|
+
if (valueRaw.trim().length === 0) {
|
|
306
|
+
valueCasted = 0;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const num = parseFloat(valueRaw);
|
|
310
|
+
if (isNaN(num)) {
|
|
311
|
+
valueCasted = nullable ? null : 0;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
valueCasted = num;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (dataType === 'boolean') {
|
|
319
|
+
if (valueRaw === '1' || valueRaw === 'true') {
|
|
320
|
+
valueCasted = true;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
valueCasted = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const value = queryStringDecodedSetValue(field, valueCasted);
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
return {};
|
|
332
|
+
};
|
|
333
|
+
const update = (data) => {
|
|
334
|
+
objectEach(data, (key, val) => {
|
|
335
|
+
this.store.set(key, val);
|
|
336
|
+
});
|
|
337
|
+
};
|
|
306
338
|
let data = {};
|
|
307
339
|
modelNodes.forEach((modelNode) => {
|
|
308
340
|
modelNode.addEventListener('input', () => {
|
|
@@ -321,10 +353,11 @@ export class ClientComponent extends EventEmitter {
|
|
|
321
353
|
});
|
|
322
354
|
const field = modelNode.getAttribute('data-model');
|
|
323
355
|
if (field) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
356
|
+
const updateModel = modelNode.type !== 'radio' || modelNode.checked;
|
|
357
|
+
if (updateModel) {
|
|
358
|
+
const valueObject = modelData(modelNode);
|
|
359
|
+
data = mergeDeep(data, valueObject);
|
|
360
|
+
}
|
|
328
361
|
}
|
|
329
362
|
});
|
|
330
363
|
update(data);
|
|
@@ -328,15 +328,15 @@ export class Request {
|
|
|
328
328
|
const files = readdirSync(routesPath);
|
|
329
329
|
for (let i = 0; i < files.length; i++) {
|
|
330
330
|
const file = files[i];
|
|
331
|
-
if (!(file.endsWith('.js') || file.endsWith('.ts')) || file.endsWith('.d.ts')) {
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
331
|
const filePath = path.resolve(routesPath + '/' + file);
|
|
335
332
|
const isDirectory = statSync(filePath).isDirectory();
|
|
336
333
|
if (isDirectory) {
|
|
337
334
|
await this.loadHandlers(filePath);
|
|
338
335
|
}
|
|
339
336
|
else {
|
|
337
|
+
if (!(file.endsWith('.js') || file.endsWith('.ts')) || file.endsWith('.d.ts')) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
340
|
const fn = (await import('file:///' + filePath)).default;
|
|
341
341
|
if (typeof fn === 'function') {
|
|
342
342
|
fn(this.app);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HTMLParser } from "./HTMLParser.js";
|
|
2
|
-
export const selfClosingTags = ['br', 'wbr', 'hr', 'input', 'img', 'link', 'meta', 'source', 'embed', 'path', 'area'];
|
|
3
|
-
export const recognizedHTMLTags = ['body', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'b', 'i', 'a', 'em', 'strong', 'br', 'wbr', 'hr', 'abbr', 'bdi', 'bdo', 'blockquote', 'cite', 'code', 'del', 'dfn', 'ins', 'kbd', 'mark', 'pre', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'sub', 'sup', 'time', 'u', 'var', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'img', 'area', 'map', 'object', 'param', 'table', 'tr', 'td', 'th', 'caption', 'colgroup', 'col', 'form', 'input', 'label', 'select', 'option', 'textarea', 'button', 'fieldset', 'datalist', 'iframe', 'audio', 'video', 'source', 'track', 'script', 'noscript', 'div', 'nav', 'aside', 'canvas', 'embed', 'template'];
|
|
2
|
+
export const selfClosingTags = ['br', 'wbr', 'hr', 'input', 'img', 'link', 'meta', 'source', 'embed', 'path', 'area', 'rect'];
|
|
3
|
+
export const recognizedHTMLTags = ['body', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'b', 'i', 'a', 'em', 'strong', 'br', 'wbr', 'hr', 'abbr', 'bdi', 'bdo', 'blockquote', 'cite', 'code', 'del', 'dfn', 'ins', 'kbd', 'mark', 'pre', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'sub', 'sup', 'time', 'u', 'var', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'img', 'area', 'map', 'object', 'param', 'table', 'tr', 'td', 'th', 'caption', 'colgroup', 'col', 'form', 'input', 'label', 'select', 'option', 'textarea', 'button', 'fieldset', 'datalist', 'iframe', 'audio', 'video', 'source', 'track', 'script', 'noscript', 'div', 'nav', 'aside', 'canvas', 'embed', 'template', 'rect'];
|
|
4
4
|
export class DOMNode {
|
|
5
5
|
constructor(root, parentNode, tagName) {
|
|
6
6
|
this.parentNode = null;
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "build/index",
|
|
22
|
-
"version": "1.0.
|
|
22
|
+
"version": "1.0.8",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"develop": "tsc --watch",
|
|
25
25
|
"startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"./Document": "./build/system/server/Document.js",
|
|
56
56
|
"./Component": "./build/system/server/Component.js",
|
|
57
57
|
"./Layout": "./build/system/server/Layout.js",
|
|
58
|
+
"./DOMNode": "./build/system/server/dom/DOMNode.js",
|
|
58
59
|
"./FormValidation": "./build/system/server/FormValidation.js",
|
|
59
60
|
"./ClientComponent": "./build/system/client/ClientComponent.js",
|
|
60
61
|
"./Net": "./build/system/client/Net.js"
|