bank20baht-ui 0.0.7 → 0.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 +39 -0
- package/dist/components/baht-form.d.ts +16 -0
- package/dist/core/engine/conditions.d.ts +5 -0
- package/dist/core/i18n.d.ts +6 -0
- package/dist/core/types.d.ts +21 -2
- package/dist/core/validators.d.ts +5 -3
- package/dist/custom-elements.json +49 -0
- package/dist/engine.js +236 -214
- package/dist/index.js +331 -289
- package/dist/web-types.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,45 @@ Value is an array of row objects (`getData().items → [{ name, qty }, …]`). R
|
|
|
67
67
|
the UI (`minRows`/`maxRows` bound the count), each row validates its sub-fields — errors come
|
|
68
68
|
back as `items[0].name`, on both the client and `bank20baht-validator`. One level deep only.
|
|
69
69
|
|
|
70
|
+
### Dependent / async dropdowns
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// schema — functions never live in JSON, only registry names:
|
|
74
|
+
{ order: 2, type: 'dropdown', key: 'amphoe', optionsFrom: 'amphoeFor', optionsDeps: ['province'] }
|
|
75
|
+
|
|
76
|
+
// registry:
|
|
77
|
+
el.registry = {
|
|
78
|
+
options: { amphoeFor: (data) => fetch(`/api/amphoe?p=${data.province}`).then(r => r.json()) },
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Options resolve from the registry with the form's current data (sync or Promise). When a
|
|
83
|
+
dep changes, options refetch and the dependent value clears — cascading down chains
|
|
84
|
+
(จังหวัด → อำเภอ → ตำบล).
|
|
85
|
+
|
|
86
|
+
### Cross-field validation
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
{ order: 2, type: 'date', key: 'end',
|
|
90
|
+
validators: [{ type: 'compare', op: 'gte', field: 'start' }] }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`compare` checks against a sibling field (confirm email = `op: 'eq'`), numeric/date-aware,
|
|
94
|
+
skips while either side is empty. Works inside repeater rows and on the server.
|
|
95
|
+
|
|
96
|
+
### Drafts / autosave (host pattern)
|
|
97
|
+
|
|
98
|
+
The library is controlled — autosave is three lines in your app:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
el.addEventListener('baht-change', (e) => localStorage.setItem(key, JSON.stringify(e.detail.data)));
|
|
102
|
+
// (workflow: listen to baht-data-change instead)
|
|
103
|
+
el.value = JSON.parse(localStorage.getItem(key) ?? '{}'); // restore on mount
|
|
104
|
+
// clear the key after the server accepts the submission
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
See `apps/web`'s render page for a working restore/discard/clear-on-submit flow.
|
|
108
|
+
|
|
70
109
|
### i18n — Thai (or any) built-in strings
|
|
71
110
|
|
|
72
111
|
All built-in strings — validation messages, Next/Submit/Back, the approval step —
|
|
@@ -32,7 +32,23 @@ export declare class BahtForm extends BahtElement {
|
|
|
32
32
|
* which drops focus and makes continuous typing impossible.
|
|
33
33
|
*/
|
|
34
34
|
private controls;
|
|
35
|
+
/** Resolved dynamic options + the dep snapshot / fetch epoch they belong to. */
|
|
36
|
+
private dynamicOptions;
|
|
37
|
+
private optionsSnapshots;
|
|
38
|
+
private optionsEpoch;
|
|
35
39
|
protected willUpdate(changed: Map<PropertyKey, unknown>): void;
|
|
40
|
+
/**
|
|
41
|
+
* Refetch `optionsFrom` options whose dep snapshot changed. Sync fetchers
|
|
42
|
+
* land immediately; async ones re-render on resolve (stale responses are
|
|
43
|
+
* dropped by epoch).
|
|
44
|
+
*/
|
|
45
|
+
private syncDynamicOptions;
|
|
46
|
+
/**
|
|
47
|
+
* A user edit invalidates every field whose optionsDeps include the edited
|
|
48
|
+
* key — their values reset to defaults, cascading down chains
|
|
49
|
+
* (province -> district -> subdistrict).
|
|
50
|
+
*/
|
|
51
|
+
private clearDependents;
|
|
36
52
|
private get sortedFields();
|
|
37
53
|
private stateFor;
|
|
38
54
|
private valueFor;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { Condition, NestedData } from '../types.js';
|
|
2
2
|
/** Read a "formName.fieldKey" reference out of nested data. */
|
|
3
3
|
export declare function readRef(data: NestedData, ref: string): unknown;
|
|
4
|
+
/**
|
|
5
|
+
* Ordering used by rule conditions AND the `compare` validator: numeric when
|
|
6
|
+
* both sides coerce, date-aware, else lexicographic. Null = not comparable.
|
|
7
|
+
*/
|
|
8
|
+
export declare function compareValues(a: unknown, b: unknown): number | null;
|
|
4
9
|
export declare function evalCondition(cond: Condition | undefined, data: NestedData): boolean;
|
package/dist/core/i18n.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export interface MessageCatalog {
|
|
|
6
6
|
max: string;
|
|
7
7
|
pattern: string;
|
|
8
8
|
email: string;
|
|
9
|
+
compareEq: string;
|
|
10
|
+
compareNe: string;
|
|
11
|
+
compareGt: string;
|
|
12
|
+
compareGte: string;
|
|
13
|
+
compareLt: string;
|
|
14
|
+
compareLte: string;
|
|
9
15
|
addRow: string;
|
|
10
16
|
removeRow: string;
|
|
11
17
|
minRows: string;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -5,11 +5,20 @@ export interface FieldOption {
|
|
|
5
5
|
key: string;
|
|
6
6
|
label: string;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
8
|
+
/** Ordering/equality ops available to the `compare` validator. */
|
|
9
|
+
export type CompareOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';
|
|
10
|
+
/**
|
|
11
|
+
* Declarative validator (spec 01 §4). `custom` resolves `fn` from the
|
|
12
|
+
* registry; `compare` checks this value against a sibling field's value
|
|
13
|
+
* (same form — or same row inside a repeater) and skips while the sibling
|
|
14
|
+
* is empty or unknown.
|
|
15
|
+
*/
|
|
9
16
|
export interface ValidatorSpec {
|
|
10
|
-
type: 'required' | 'minLength' | 'maxLength' | 'min' | 'max' | 'pattern' | 'email' | 'custom';
|
|
17
|
+
type: 'required' | 'minLength' | 'maxLength' | 'min' | 'max' | 'pattern' | 'email' | 'compare' | 'custom';
|
|
11
18
|
value?: unknown;
|
|
12
19
|
fn?: string;
|
|
20
|
+
op?: CompareOp;
|
|
21
|
+
field?: string;
|
|
13
22
|
message?: string;
|
|
14
23
|
}
|
|
15
24
|
/** One item of `field_data[]` (spec 01 §1). */
|
|
@@ -26,6 +35,14 @@ export interface FieldDescriptor {
|
|
|
26
35
|
disabled?: boolean;
|
|
27
36
|
required?: boolean;
|
|
28
37
|
options?: FieldOption[];
|
|
38
|
+
/** Registry name resolving this field's options from current form data. */
|
|
39
|
+
optionsFrom?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Same-form field keys this field's options depend on. When one changes
|
|
42
|
+
* (user edit), the options refetch and this field's value clears —
|
|
43
|
+
* cascading through chained dependents (province -> district -> …).
|
|
44
|
+
*/
|
|
45
|
+
optionsDeps?: string[];
|
|
29
46
|
maxLength?: number;
|
|
30
47
|
minLength?: number;
|
|
31
48
|
min?: number | string;
|
|
@@ -154,6 +171,8 @@ export interface Registry {
|
|
|
154
171
|
formatters?: Record<string, (value: unknown) => unknown>;
|
|
155
172
|
/** Return null when valid, or an error message string. */
|
|
156
173
|
validators?: Record<string, (value: unknown) => string | null>;
|
|
174
|
+
/** Option fetchers for `optionsFrom` — receive the form's current flat data. */
|
|
175
|
+
options?: Record<string, (data: Record<string, unknown>) => FieldOption[] | Promise<FieldOption[]>>;
|
|
157
176
|
}
|
|
158
177
|
export interface DerivedFieldState {
|
|
159
178
|
visible: boolean;
|
|
@@ -2,7 +2,9 @@ import { FieldDescriptor, Registry, ValidationError } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Validate one field's value against its declarative validators (spec 01 §4).
|
|
4
4
|
* Empty + not required = valid (other validators only run on non-empty values).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* `data` is the surrounding flat form data — only `compare` validators read
|
|
6
|
+
* it; omitting it skips them. Repeaters (spec 01 §10) additionally check row
|
|
7
|
+
* bounds and recurse into each row's sub-fields, prefixing keys as
|
|
8
|
+
* `key[i].subKey` (each row is its own compare context).
|
|
7
9
|
*/
|
|
8
|
-
export declare function validateField(field: FieldDescriptor, value: unknown, registry?: Registry): ValidationError[];
|
|
10
|
+
export declare function validateField(field: FieldDescriptor, value: unknown, registry?: Registry, data?: Record<string, unknown>): ValidationError[];
|
|
@@ -86,6 +86,55 @@
|
|
|
86
86
|
"default": "new Map<string, HTMLElement & Record<string, unknown>>()",
|
|
87
87
|
"description": "One control element per field key, reused across renders. Recreating them\nevery render (the naive approach) replaces the DOM node on each keystroke,\nwhich drops focus and makes continuous typing impossible."
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
"kind": "field",
|
|
91
|
+
"name": "dynamicOptions",
|
|
92
|
+
"privacy": "private",
|
|
93
|
+
"default": "new Map<string, FieldOption[]>()",
|
|
94
|
+
"description": "Resolved dynamic options + the dep snapshot / fetch epoch they belong to."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"kind": "field",
|
|
98
|
+
"name": "optionsSnapshots",
|
|
99
|
+
"privacy": "private",
|
|
100
|
+
"default": "new Map<string, string>()"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"kind": "field",
|
|
104
|
+
"name": "optionsEpoch",
|
|
105
|
+
"privacy": "private",
|
|
106
|
+
"default": "new Map<string, number>()"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"kind": "method",
|
|
110
|
+
"name": "syncDynamicOptions",
|
|
111
|
+
"privacy": "private",
|
|
112
|
+
"return": {
|
|
113
|
+
"type": {
|
|
114
|
+
"text": "void"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"description": "Refetch `optionsFrom` options whose dep snapshot changed. Sync fetchers\nland immediately; async ones re-render on resolve (stale responses are\ndropped by epoch)."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"kind": "method",
|
|
121
|
+
"name": "clearDependents",
|
|
122
|
+
"privacy": "private",
|
|
123
|
+
"return": {
|
|
124
|
+
"type": {
|
|
125
|
+
"text": "void"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"parameters": [
|
|
129
|
+
{
|
|
130
|
+
"name": "changedKey",
|
|
131
|
+
"type": {
|
|
132
|
+
"text": "string"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"description": "A user edit invalidates every field whose optionsDeps include the edited\nkey — their values reset to defaults, cascading down chains\n(province -> district -> subdistrict)."
|
|
137
|
+
},
|
|
89
138
|
{
|
|
90
139
|
"kind": "field",
|
|
91
140
|
"name": "sortedFields",
|