ts-util-core 2.0.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 +411 -0
- package/dist/core/ajax.d.ts +30 -0
- package/dist/core/ajax.d.ts.map +1 -0
- package/dist/core/ajax.js +110 -0
- package/dist/core/ajax.js.map +1 -0
- package/dist/core/event-emitter.d.ts +29 -0
- package/dist/core/event-emitter.d.ts.map +1 -0
- package/dist/core/event-emitter.js +67 -0
- package/dist/core/event-emitter.js.map +1 -0
- package/dist/core/message.d.ts +28 -0
- package/dist/core/message.d.ts.map +1 -0
- package/dist/core/message.js +172 -0
- package/dist/core/message.js.map +1 -0
- package/dist/core/view.d.ts +49 -0
- package/dist/core/view.d.ts.map +1 -0
- package/dist/core/view.js +87 -0
- package/dist/core/view.js.map +1 -0
- package/dist/formatting/formatters.d.ts +9 -0
- package/dist/formatting/formatters.d.ts.map +1 -0
- package/dist/formatting/formatters.js +109 -0
- package/dist/formatting/formatters.js.map +1 -0
- package/dist/formatting/registry.d.ts +31 -0
- package/dist/formatting/registry.d.ts.map +1 -0
- package/dist/formatting/registry.js +49 -0
- package/dist/formatting/registry.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/dom.d.ts +38 -0
- package/dist/utils/dom.d.ts.map +1 -0
- package/dist/utils/dom.js +95 -0
- package/dist/utils/dom.js.map +1 -0
- package/dist/utils/sprintf.d.ts +16 -0
- package/dist/utils/sprintf.d.ts.map +1 -0
- package/dist/utils/sprintf.js +116 -0
- package/dist/utils/sprintf.js.map +1 -0
- package/dist/validation/constraints.d.ts +23 -0
- package/dist/validation/constraints.d.ts.map +1 -0
- package/dist/validation/constraints.js +131 -0
- package/dist/validation/constraints.js.map +1 -0
- package/dist/validation/validator.d.ts +45 -0
- package/dist/validation/validator.d.ts.map +1 -0
- package/dist/validation/validator.js +210 -0
- package/dist/validation/validator.js.map +1 -0
- package/package.json +26 -0
- package/readme.txt +4 -0
- package/src/core/ajax.ts +127 -0
- package/src/core/event-emitter.ts +84 -0
- package/src/core/message.ts +212 -0
- package/src/core/view.ts +101 -0
- package/src/formatting/formatters.ts +118 -0
- package/src/formatting/registry.ts +53 -0
- package/src/index.ts +142 -0
- package/src/types.ts +85 -0
- package/src/utils/dom.ts +105 -0
- package/src/utils/sprintf.ts +141 -0
- package/src/validation/constraints.ts +168 -0
- package/src/validation/validator.ts +276 -0
package/README.md
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/banner.svg" alt="TS-Util — Type-safe form validation, AJAX, messaging, and view management" width="100%" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>The form infrastructure toolkit for enterprise web apps.</strong><br/>
|
|
7
|
+
Drop jQuery. Keep the patterns. Ship with confidence.
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="#quick-start">Quick Start</a> • 
|
|
12
|
+
<a href="#live-demo">Live Demo</a> • 
|
|
13
|
+
<a href="#modules">Modules</a> • 
|
|
14
|
+
<a href="#api-reference">API Reference</a> • 
|
|
15
|
+
<a href="docs/good-design-pattern-implementation-after.md">Design Patterns</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why TS-Util?
|
|
21
|
+
|
|
22
|
+
| Pain point | How this library solves it |
|
|
23
|
+
|---|---|
|
|
24
|
+
| **"Our forms need validation, masking, AJAX, dialogs — that's 4 libraries."** | One import. Six modules. Zero dependencies. |
|
|
25
|
+
| **"Dynamic content loaded via AJAX has no validation."** | `VIEW.load()` auto-initializes constraints and formatters on every fragment. |
|
|
26
|
+
| **"Adding a custom input format means touching library internals."** | `Formatter.add({ key, format })` — register from the outside, never fork. |
|
|
27
|
+
| **"Runtime surprises: wrong callback shape, misspelled event name."** | Every event, callback, and option is type-checked at compile time. |
|
|
28
|
+
|
|
29
|
+
### The deeper reason: discipline at scale
|
|
30
|
+
|
|
31
|
+
Every frontend project eventually hits the same problem: ten engineers (or ten AI Agents) write ten different ways to make an AJAX call. Should you validate the form before sending? Show a loading overlay? How should errors be handled? Everyone has a different answer, and code review can only do so much.
|
|
32
|
+
|
|
33
|
+
**TS-Util encodes decisions into infrastructure.** When you call `AJAX.request()`, form validation, loading state management, error broadcasting, and data serialization all happen automatically. You can't skip any of them — and neither can your teammates or your AI coding assistants.
|
|
34
|
+
|
|
35
|
+
**For teams** — engineers learn one API, new members read one example to get started, and every request flows through the same pipeline. No debates, no divergence.
|
|
36
|
+
|
|
37
|
+
**For AI Agents** — an Agent emits `AJAX.request({ url, form })` instead of expanding the full fetch + validation + error handling logic every time. Context window is AI's most precious resource; saving tokens preserves quality. The abstraction layer also acts as a guardrail — an Agent cannot "forget" to validate a form because the architecture enforces it automatically.
|
|
38
|
+
|
|
39
|
+
> **Wrapping isn't about writing less code — it's about making ten people, or ten Agents, produce output that looks like it came from one.**
|
|
40
|
+
|
|
41
|
+
📖 Read the full article in [six languages](docs/why-wrap-ajax-and-view.md) (繁體中文 · English · 日本語 · 한국어 · Español · Deutsch)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Live Demo
|
|
46
|
+
|
|
47
|
+
> **[Open `demo.html`](demo.html)** — an interactive single-page guide with live output consoles for every module.
|
|
48
|
+
>
|
|
49
|
+
> ```bash
|
|
50
|
+
> npx serve . # then open http://localhost:3000/demo.html
|
|
51
|
+
> ```
|
|
52
|
+
|
|
53
|
+
The demo lets you click through Events, AJAX, Validation, Formatting, MSG dialogs, VIEW injection, and utility functions — with code snippets alongside real-time results.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install ts-util-core
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Import what you need
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { AJAX, VIEW, MSG, Validation, Formatter, Events } from 'ts-util-core';
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Or use the global namespace (legacy `<script>` tags)
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<script type="module" src="dist/index.js"></script>
|
|
75
|
+
<script>
|
|
76
|
+
const { AJAX, MSG } = window['#'];
|
|
77
|
+
</script>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### A real-world example in 12 lines
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { AJAX, MSG, Events } from 'ts-util-core';
|
|
84
|
+
|
|
85
|
+
// Listen for lifecycle events
|
|
86
|
+
Events.on('ajax:before', ({ url }) => showSpinner(url));
|
|
87
|
+
Events.on('ajax:after', ({ url }) => hideSpinner(url));
|
|
88
|
+
|
|
89
|
+
// Submit a form with auto-validation
|
|
90
|
+
await AJAX.request({
|
|
91
|
+
url: '/api/orders',
|
|
92
|
+
form: document.getElementById('order-form')!,
|
|
93
|
+
success: () => MSG.info('Order saved!', { autoclose: 3000 }),
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
That single `AJAX.request()` call will:
|
|
98
|
+
1. Validate all `constraint="required"` fields in the form
|
|
99
|
+
2. Emit `ajax:before` (your spinner appears)
|
|
100
|
+
3. Serialize the form to JSON and POST it
|
|
101
|
+
4. Emit `ajax:after` (spinner hides)
|
|
102
|
+
5. Call your `success` callback
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Architecture
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
┌─────────────────┐
|
|
110
|
+
│ EventEmitter │ ← Typed central bus
|
|
111
|
+
│ (Mediator) │
|
|
112
|
+
└──┬──┬──┬──┬─────┘
|
|
113
|
+
┌────────┘ │ │ └────────┐
|
|
114
|
+
▼ ▼ ▼ ▼
|
|
115
|
+
┌────────┐ ┌──────┐ ┌───────────┐ ┌───────────┐
|
|
116
|
+
│ AJAX │ │ VIEW │ │ Validation │ │ Formatter │
|
|
117
|
+
│Facade +│ │Observ│ │ Strategy + │ │ Registry │
|
|
118
|
+
│Template│ │ er │ │ Decorator │ │ Pattern │
|
|
119
|
+
└────────┘ └──────┘ └───────────┘ └───────────┘
|
|
120
|
+
│ │ │ │
|
|
121
|
+
└─────┬─────┘ ┌────┘ │
|
|
122
|
+
▼ ▼ ▼
|
|
123
|
+
┌───────┐ ┌──────────┐ ┌──────────────┐
|
|
124
|
+
│ MSG │ │ Utils │ │ HTML attrs │
|
|
125
|
+
│Dialogs│ │sprintf, │ │ constraint= │
|
|
126
|
+
└───────┘ │formToJSON│ │ format= │
|
|
127
|
+
└──────────┘ └──────────────┘
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
All modules communicate through the typed `EventEmitter` — no module imports another directly. This makes every piece independently testable and replaceable.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Modules
|
|
135
|
+
|
|
136
|
+
### Events — the central bus
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Subscribe with full type safety — event names and payloads are checked
|
|
140
|
+
Events.on('ajax:before', ({ url }) => console.log(url)); // url: string
|
|
141
|
+
Events.on('ajax:error', ({ url, error }) => log(error)); // error: Error
|
|
142
|
+
|
|
143
|
+
// Unsubscribe
|
|
144
|
+
const off = Events.on('ajax:after', handler);
|
|
145
|
+
off(); // done
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Available events:**
|
|
149
|
+
|
|
150
|
+
| Event | Payload | Fired when |
|
|
151
|
+
|-------|---------|------------|
|
|
152
|
+
| `ajax:before` | `{ url }` | Request starts (unless `noblock`) |
|
|
153
|
+
| `ajax:after` | `{ url }` | Request completes |
|
|
154
|
+
| `ajax:error` | `{ url, error }` | Request fails |
|
|
155
|
+
| `view:beforeLoad` | `{ context }` | New DOM fragment initializes |
|
|
156
|
+
| `validation:invalid` | `{ labelNames, elements }` | Required fields missing |
|
|
157
|
+
| `validation:textareaTooLong` | `{ labelNames, maxlengths, elements }` | Textarea exceeds limit |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### AJAX — fetch with lifecycle
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Simple POST
|
|
165
|
+
await AJAX.request({
|
|
166
|
+
url: '/api/save',
|
|
167
|
+
data: { name: 'Alice' },
|
|
168
|
+
success: (res) => console.log('Done'),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// POST with auto-validation + form serialization
|
|
172
|
+
await AJAX.request({
|
|
173
|
+
url: '/api/save',
|
|
174
|
+
form: document.getElementById('myForm')!,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Typed JSON response
|
|
178
|
+
const user = await AJAX.requestJSON<User>({
|
|
179
|
+
url: '/api/user/1',
|
|
180
|
+
success: (data) => { /* data is User, not unknown */ },
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Validation — declarative constraints
|
|
187
|
+
|
|
188
|
+
Declare in HTML, the library does the rest:
|
|
189
|
+
|
|
190
|
+
```html
|
|
191
|
+
<input constraint="required" labelName="Name" />
|
|
192
|
+
<input constraint="required number" labelName="Amount" />
|
|
193
|
+
<input constraint="required upperCase onlyEn" labelName="Code" />
|
|
194
|
+
<input constraint="date" labelName="Start Date" />
|
|
195
|
+
<input constraint="time" labelName="Meeting Time" />
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Built-in constraints:** `required` `number` `date` `time` `upperCase` `onlyEn`
|
|
199
|
+
|
|
200
|
+
**Add your own:**
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
Validation.addConstraint({
|
|
204
|
+
name: 'email',
|
|
205
|
+
attach(el) {
|
|
206
|
+
el.addEventListener('change', () => {
|
|
207
|
+
if (el.value && !el.value.includes('@')) el.value = '';
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
// Now use: <input constraint="required email" labelName="Email" />
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Customize error handling:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
Validation.setRequiredInvalidCallback((labelNames, elements) => {
|
|
218
|
+
// Replace the default alert with your own UI
|
|
219
|
+
showToast(`Missing: ${labelNames.join(', ')}`);
|
|
220
|
+
elements[0]?.focus();
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Formatting — input masks
|
|
227
|
+
|
|
228
|
+
Declare in HTML:
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<input format="idNumber" /> <!-- A123456789 -->
|
|
232
|
+
<input format="date" /> <!-- 2026-02-24 (auto-inserts dashes) -->
|
|
233
|
+
<input format="time" /> <!-- 14:30 (auto-inserts colon) -->
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Register custom formatters:**
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
Formatter.add({
|
|
240
|
+
key: 'phone',
|
|
241
|
+
format: (el) => {
|
|
242
|
+
el.placeholder = '09XX-XXX-XXX';
|
|
243
|
+
el.addEventListener('input', () => {
|
|
244
|
+
let v = el.value.replace(/\D/g, '');
|
|
245
|
+
if (v.length > 4) v = v.slice(0, 4) + '-' + v.slice(4);
|
|
246
|
+
if (v.length > 8) v = v.slice(0, 8) + '-' + v.slice(8);
|
|
247
|
+
el.value = v.slice(0, 12);
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### MSG — vanilla DOM dialogs
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Auto-closing notification
|
|
259
|
+
MSG.info('Saved!', { title: 'Success', autoclose: 3000 });
|
|
260
|
+
|
|
261
|
+
// Modal (must click OK)
|
|
262
|
+
MSG.modal('Session expired.', { title: 'Warning' });
|
|
263
|
+
|
|
264
|
+
// Confirmation
|
|
265
|
+
MSG.confirm('Delete', 'Are you sure?', () => {
|
|
266
|
+
deleteRecord();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Dismiss programmatically
|
|
270
|
+
MSG.dismissModal();
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
### VIEW — dynamic content with auto-init
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Load an HTML fragment — constraints + formatters auto-initialize
|
|
279
|
+
await VIEW.load(document.getElementById('container')!, {
|
|
280
|
+
url: '/api/partial-view',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Or inject manually and trigger hooks
|
|
284
|
+
container.innerHTML = htmlString;
|
|
285
|
+
VIEW.invokeBeforeLoad(container);
|
|
286
|
+
|
|
287
|
+
// Register your own hook
|
|
288
|
+
VIEW.addBeforeLoad((context) => {
|
|
289
|
+
context.querySelectorAll('.tooltip').forEach(initTooltip);
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### Utilities
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { sprintf, formToJSON, isDateValid } from 'ts-util-core';
|
|
299
|
+
|
|
300
|
+
sprintf('Hello %s, you are %d years old', 'Alice', 30);
|
|
301
|
+
// → "Hello Alice, you are 30 years old"
|
|
302
|
+
|
|
303
|
+
sprintf('Price: $%.2f', 9.5);
|
|
304
|
+
// → "Price: $9.50"
|
|
305
|
+
|
|
306
|
+
const data = formToJSON(formElement);
|
|
307
|
+
// → { username: "alice", role: "viewer" }
|
|
308
|
+
|
|
309
|
+
isDateValid('2026-02-24'); // → true
|
|
310
|
+
isDateValid('not-a-date'); // → false
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## API Reference
|
|
316
|
+
|
|
317
|
+
### Singletons (pre-wired, ready to use)
|
|
318
|
+
|
|
319
|
+
| Export | Type | Description |
|
|
320
|
+
|--------|------|-------------|
|
|
321
|
+
| `AJAX` | `Ajax` | HTTP client with form validation integration |
|
|
322
|
+
| `VIEW` | `View` | Dynamic HTML fragment loader |
|
|
323
|
+
| `MSG` | `Message` | DOM dialog system |
|
|
324
|
+
| `Validation` | `Validator` | Form validation engine |
|
|
325
|
+
| `Formatter` | `FormatterRegistry` | Input mask registry |
|
|
326
|
+
| `Events` | `EventEmitter<TSUtilEventMap>` | Typed event bus |
|
|
327
|
+
|
|
328
|
+
### Utility functions
|
|
329
|
+
|
|
330
|
+
| Export | Signature | Description |
|
|
331
|
+
|--------|-----------|-------------|
|
|
332
|
+
| `sprintf` | `(fmt: string, ...args) => string` | printf-style string formatting |
|
|
333
|
+
| `formToJSON` | `(container: HTMLElement, options?) => FormDataRecord` | Serialize form inputs to JSON |
|
|
334
|
+
| `isDateValid` | `(value: string) => boolean` | Validate date strings |
|
|
335
|
+
| `parseHTML` | `(html: string) => HTMLElement` | Parse HTML string to DOM |
|
|
336
|
+
| `scrollToElement` | `(el: HTMLElement) => void` | Smooth scroll to element |
|
|
337
|
+
| `defaults` | `<T>(base: T, ...overrides: Partial<T>[]) => T` | Merge defaults with overrides |
|
|
338
|
+
|
|
339
|
+
### Classes (for advanced use / testing)
|
|
340
|
+
|
|
341
|
+
| Export | Description |
|
|
342
|
+
|--------|-------------|
|
|
343
|
+
| `EventEmitter<T>` | Create isolated event buses for testing |
|
|
344
|
+
| `Ajax` | Instantiate with a custom emitter |
|
|
345
|
+
| `View` | Instantiate with a custom emitter + ajax |
|
|
346
|
+
| `Message` | Standalone dialog system |
|
|
347
|
+
| `Validator` | Standalone validator with custom emitter |
|
|
348
|
+
| `FormatterRegistry` | Standalone formatter registry |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Project Structure
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
src/
|
|
356
|
+
├── index.ts # Barrel export + singleton wiring
|
|
357
|
+
├── types.ts # Shared type definitions
|
|
358
|
+
├── core/
|
|
359
|
+
│ ├── event-emitter.ts # Typed EventEmitter (Mediator)
|
|
360
|
+
│ ├── ajax.ts # HTTP client (Facade + Template Method)
|
|
361
|
+
│ ├── view.ts # Fragment loader (Observer)
|
|
362
|
+
│ └── message.ts # Dialog system (Facade)
|
|
363
|
+
├── validation/
|
|
364
|
+
│ ├── validator.ts # Validation engine (Strategy)
|
|
365
|
+
│ └── constraints.ts # Built-in constraints (Decorator)
|
|
366
|
+
├── formatting/
|
|
367
|
+
│ ├── registry.ts # Formatter registry (Registry Pattern)
|
|
368
|
+
│ └── formatters.ts # Built-in formatters
|
|
369
|
+
└── utils/
|
|
370
|
+
├── sprintf.ts # printf-style formatting
|
|
371
|
+
└── dom.ts # DOM helpers
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**12 source files · ~1,600 lines · strict TypeScript · ES2022 target · zero dependencies**
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Build
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npm run build # one-shot compile
|
|
382
|
+
npm run dev # watch mode
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Output goes to `dist/` with `.js`, `.d.ts`, and source maps.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Design Patterns
|
|
390
|
+
|
|
391
|
+
This library is a teaching-friendly codebase. Every module implements a named GoF pattern:
|
|
392
|
+
|
|
393
|
+
| Pattern | Module | What it teaches |
|
|
394
|
+
|---------|--------|----------------|
|
|
395
|
+
| **Mediator** | `EventEmitter` | Decoupled inter-module communication |
|
|
396
|
+
| **Facade** | `AJAX`, `MSG` | Hide multi-step complexity behind one call |
|
|
397
|
+
| **Template Method** | `requestJSON()` | Reuse a base algorithm, customize one step |
|
|
398
|
+
| **Observer** | `VIEW.addBeforeLoad()` | Plugin registration without coupling |
|
|
399
|
+
| **Strategy** | `setRequiredInvalidCallback()` | Replace behavior without modifying source |
|
|
400
|
+
| **Registry** | `Formatter` | Extensible key-based lookup |
|
|
401
|
+
| **Decorator** | `constraint="..."` attributes | Composable behavior via HTML |
|
|
402
|
+
|
|
403
|
+
Deep-dive documentation:
|
|
404
|
+
- **[Before (jQuery)](docs/good-design-pattern-implementation-before.md)** — patterns in the original codebase
|
|
405
|
+
- **[After (TypeScript)](docs/good-design-pattern-implementation-after.md)** — how TypeScript makes them safer
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## License
|
|
410
|
+
|
|
411
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EventEmitter } from './event-emitter.js';
|
|
2
|
+
import type { AppEventMap, AjaxRequestParams, AjaxJsonRequestParams } from '../types.js';
|
|
3
|
+
export declare class Ajax {
|
|
4
|
+
private emitter;
|
|
5
|
+
private validateForm;
|
|
6
|
+
constructor(emitter: EventEmitter<AppEventMap>);
|
|
7
|
+
/** Register the form validation function (injected by the Validation module). */
|
|
8
|
+
setValidator(fn: (form: HTMLElement) => boolean): void;
|
|
9
|
+
/**
|
|
10
|
+
* Send an HTTP request (POST by default).
|
|
11
|
+
*
|
|
12
|
+
* Lifecycle:
|
|
13
|
+
* 1. Validate form (if `params.form` provided)
|
|
14
|
+
* 2. Emit `ajax:before`
|
|
15
|
+
* 3. Serialize form data + merge `params.data`
|
|
16
|
+
* 4. `fetch()`
|
|
17
|
+
* 5. Emit `ajax:after` (on success) or `ajax:error` (on failure)
|
|
18
|
+
* 6. Call `params.success` / `params.error` / `params.complete`
|
|
19
|
+
*
|
|
20
|
+
* @returns The raw `Response`, or `undefined` if validation failed.
|
|
21
|
+
*/
|
|
22
|
+
request(params: AjaxRequestParams): Promise<Response | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* Send a request and parse the response as JSON.
|
|
25
|
+
*
|
|
26
|
+
* Template Method — delegates to `request()`, adds JSON parsing.
|
|
27
|
+
*/
|
|
28
|
+
requestJSON<T = unknown>(params: AjaxJsonRequestParams<T>): Promise<T | undefined>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=ajax.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ajax.d.ts","sourceRoot":"","sources":["../../src/core/ajax.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGzF,qBAAa,IAAI;IACf,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,YAAY,CAAiD;gBAEzD,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC;IAI9C,iFAAiF;IACjF,YAAY,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,GAAG,IAAI;IAItD;;;;;;;;;;;;OAYG;IACG,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAkEvE;;;;OAIG;IACG,WAAW,CAAC,CAAC,GAAG,OAAO,EAC3B,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;CAW1B"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// AJAX module — fetch-based HTTP client with lifecycle hooks
|
|
3
|
+
//
|
|
4
|
+
// Design patterns used:
|
|
5
|
+
// - Facade Pattern : one `request()` call orchestrates validate →
|
|
6
|
+
// block → serialize → fetch → unblock
|
|
7
|
+
// - Template Method Pattern: `requestJSON` / `post` / `postJSON` delegate
|
|
8
|
+
// to the base `request` with tweaked options
|
|
9
|
+
// - Strategy Pattern : loading overlay behavior is injected via
|
|
10
|
+
// the EventEmitter (ajax:before / ajax:after)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
import { formToJSON } from '../utils/dom.js';
|
|
13
|
+
export class Ajax {
|
|
14
|
+
emitter;
|
|
15
|
+
validateForm = null;
|
|
16
|
+
constructor(emitter) {
|
|
17
|
+
this.emitter = emitter;
|
|
18
|
+
}
|
|
19
|
+
/** Register the form validation function (injected by the Validation module). */
|
|
20
|
+
setValidator(fn) {
|
|
21
|
+
this.validateForm = fn;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Send an HTTP request (POST by default).
|
|
25
|
+
*
|
|
26
|
+
* Lifecycle:
|
|
27
|
+
* 1. Validate form (if `params.form` provided)
|
|
28
|
+
* 2. Emit `ajax:before`
|
|
29
|
+
* 3. Serialize form data + merge `params.data`
|
|
30
|
+
* 4. `fetch()`
|
|
31
|
+
* 5. Emit `ajax:after` (on success) or `ajax:error` (on failure)
|
|
32
|
+
* 6. Call `params.success` / `params.error` / `params.complete`
|
|
33
|
+
*
|
|
34
|
+
* @returns The raw `Response`, or `undefined` if validation failed.
|
|
35
|
+
*/
|
|
36
|
+
async request(params) {
|
|
37
|
+
// 1. Validate
|
|
38
|
+
if (params.form && this.validateForm) {
|
|
39
|
+
if (!this.validateForm(params.form))
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
// 2. Before-hook
|
|
43
|
+
if (!params.noblock) {
|
|
44
|
+
this.emitter.emit('ajax:before', { url: params.url });
|
|
45
|
+
}
|
|
46
|
+
// 3. Build body
|
|
47
|
+
const body = {};
|
|
48
|
+
if (params.form) {
|
|
49
|
+
const formData = formToJSON(params.form, {
|
|
50
|
+
ignoreDisabled: params.ignoreDisabled,
|
|
51
|
+
});
|
|
52
|
+
Object.assign(body, formData);
|
|
53
|
+
}
|
|
54
|
+
if (params.data) {
|
|
55
|
+
Object.assign(body, params.data);
|
|
56
|
+
}
|
|
57
|
+
// 4. Fetch
|
|
58
|
+
const headers = {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'Ajax-Call': 'true',
|
|
61
|
+
...params.headers,
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(params.url, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers,
|
|
67
|
+
body: JSON.stringify(body),
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
// 5a. After-hook (success)
|
|
73
|
+
if (!params.noblock) {
|
|
74
|
+
this.emitter.emit('ajax:after', { url: params.url });
|
|
75
|
+
}
|
|
76
|
+
params.success?.(response);
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
81
|
+
// 5b. After-hook (error)
|
|
82
|
+
if (!params.noblock) {
|
|
83
|
+
this.emitter.emit('ajax:after', { url: params.url });
|
|
84
|
+
}
|
|
85
|
+
this.emitter.emit('ajax:error', { url: params.url, error });
|
|
86
|
+
params.error?.(error);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
params.complete?.();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Send a request and parse the response as JSON.
|
|
95
|
+
*
|
|
96
|
+
* Template Method — delegates to `request()`, adds JSON parsing.
|
|
97
|
+
*/
|
|
98
|
+
async requestJSON(params) {
|
|
99
|
+
const response = await this.request({
|
|
100
|
+
...params,
|
|
101
|
+
success: undefined, // we handle success after JSON parse
|
|
102
|
+
});
|
|
103
|
+
if (!response)
|
|
104
|
+
return undefined;
|
|
105
|
+
const data = (await response.json());
|
|
106
|
+
params.success?.(data);
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=ajax.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ajax.js","sourceRoot":"","sources":["../../src/core/ajax.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fully typed event emitter.
|
|
3
|
+
*
|
|
4
|
+
* `TEvents` is a map of `{ eventName: payloadType }`.
|
|
5
|
+
* All `on`, `off`, and `emit` calls are type-checked against this map.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const emitter = new EventEmitter<{ 'ajax:before': { url: string } }>();
|
|
10
|
+
* emitter.on('ajax:before', ({ url }) => console.log(url));
|
|
11
|
+
* emitter.emit('ajax:before', { url: '/api' });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare class EventEmitter<TEvents extends {
|
|
15
|
+
[K in keyof TEvents]: unknown;
|
|
16
|
+
}> {
|
|
17
|
+
private listeners;
|
|
18
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
19
|
+
on<K extends keyof TEvents>(event: K, listener: (payload: TEvents[K]) => void): () => void;
|
|
20
|
+
/** Unsubscribe a specific listener. */
|
|
21
|
+
off<K extends keyof TEvents>(event: K, listener: (payload: TEvents[K]) => void): void;
|
|
22
|
+
/** Subscribe to an event — listener is automatically removed after one call. */
|
|
23
|
+
once<K extends keyof TEvents>(event: K, listener: (payload: TEvents[K]) => void): () => void;
|
|
24
|
+
/** Emit an event, calling all registered listeners with the payload. */
|
|
25
|
+
emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void;
|
|
26
|
+
/** Remove all listeners for a specific event, or all events if none specified. */
|
|
27
|
+
clear(event?: keyof TEvents): void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=event-emitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-emitter.d.ts","sourceRoot":"","sources":["../../src/core/event-emitter.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY,CAAC,OAAO,SAAS;KAAG,CAAC,IAAI,MAAM,OAAO,GAAG,OAAO;CAAE;IACzE,OAAO,CAAC,SAAS,CAGb;IAEJ,8DAA8D;IAC9D,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,EACxB,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,GACtC,MAAM,IAAI;IAab,uCAAuC;IACvC,GAAG,CAAC,CAAC,SAAS,MAAM,OAAO,EACzB,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,GACtC,IAAI;IAIP,gFAAgF;IAChF,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAC1B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,GACtC,MAAM,IAAI;IASb,wEAAwE;IACxE,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAQlE,kFAAkF;IAClF,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,OAAO,GAAG,IAAI;CAOnC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Typed EventEmitter — the unified hook / callback system
|
|
3
|
+
//
|
|
4
|
+
// Design patterns used:
|
|
5
|
+
// - Observer Pattern : multiple listeners subscribe to named events
|
|
6
|
+
// - Strategy Pattern : consumers replace default behavior via listeners
|
|
7
|
+
// - Generics : the event map is fully typed at compile time
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/**
|
|
10
|
+
* A fully typed event emitter.
|
|
11
|
+
*
|
|
12
|
+
* `TEvents` is a map of `{ eventName: payloadType }`.
|
|
13
|
+
* All `on`, `off`, and `emit` calls are type-checked against this map.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const emitter = new EventEmitter<{ 'ajax:before': { url: string } }>();
|
|
18
|
+
* emitter.on('ajax:before', ({ url }) => console.log(url));
|
|
19
|
+
* emitter.emit('ajax:before', { url: '/api' });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class EventEmitter {
|
|
23
|
+
listeners = new Map();
|
|
24
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
25
|
+
on(event, listener) {
|
|
26
|
+
if (!this.listeners.has(event)) {
|
|
27
|
+
this.listeners.set(event, new Set());
|
|
28
|
+
}
|
|
29
|
+
const set = this.listeners.get(event);
|
|
30
|
+
set.add(listener);
|
|
31
|
+
// Return unsubscribe function for convenience
|
|
32
|
+
return () => {
|
|
33
|
+
set.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Unsubscribe a specific listener. */
|
|
37
|
+
off(event, listener) {
|
|
38
|
+
this.listeners.get(event)?.delete(listener);
|
|
39
|
+
}
|
|
40
|
+
/** Subscribe to an event — listener is automatically removed after one call. */
|
|
41
|
+
once(event, listener) {
|
|
42
|
+
const wrapper = ((payload) => {
|
|
43
|
+
this.off(event, wrapper);
|
|
44
|
+
listener(payload);
|
|
45
|
+
});
|
|
46
|
+
return this.on(event, wrapper);
|
|
47
|
+
}
|
|
48
|
+
/** Emit an event, calling all registered listeners with the payload. */
|
|
49
|
+
emit(event, payload) {
|
|
50
|
+
const set = this.listeners.get(event);
|
|
51
|
+
if (!set)
|
|
52
|
+
return;
|
|
53
|
+
for (const listener of set) {
|
|
54
|
+
listener(payload);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Remove all listeners for a specific event, or all events if none specified. */
|
|
58
|
+
clear(event) {
|
|
59
|
+
if (event) {
|
|
60
|
+
this.listeners.delete(event);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.listeners.clear();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=event-emitter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-emitter.js","sourceRoot":"","sources":["../../src/core/event-emitter.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,0DAA0D;AAC1D,EAAE;AACF,wBAAwB;AACxB,sEAAsE;AACtE,0EAA0E;AAC1E,sEAAsE;AACtE,8EAA8E;AAE9E;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,YAAY;IACf,SAAS,GAAG,IAAI,GAAG,EAGxB,CAAC;IAEJ,8DAA8D;IAC9D,EAAE,CACA,KAAQ,EACR,QAAuC;QAEvC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QACvC,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC;QACvC,GAAG,CAAC,GAAG,CAAC,QAAoC,CAAC,CAAC;QAE9C,8CAA8C;QAC9C,OAAO,GAAG,EAAE;YACV,GAAG,CAAC,MAAM,CAAC,QAAoC,CAAC,CAAC;QACnD,CAAC,CAAC;IACJ,CAAC;IAED,uCAAuC;IACvC,GAAG,CACD,KAAQ,EACR,QAAuC;QAEvC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,QAAoC,CAAC,CAAC;IAC1E,CAAC;IAED,gFAAgF;IAChF,IAAI,CACF,KAAQ,EACR,QAAuC;QAEvC,MAAM,OAAO,GAAG,CAAC,CAAC,OAAmB,EAAE,EAAE;YACvC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACzB,QAAQ,CAAC,OAAO,CAAC,CAAC;QACpB,CAAC,CAAkC,CAAC;QAEpC,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAED,wEAAwE;IACxE,IAAI,CAA0B,KAAQ,EAAE,OAAmB;QACzD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,KAAK,MAAM,QAAQ,IAAI,GAAG,EAAE,CAAC;YAC1B,QAA0C,CAAC,OAAO,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,kFAAkF;IAClF,KAAK,CAAC,KAAqB;QACzB,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;CACF"}
|