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.
Files changed (63) hide show
  1. package/README.md +411 -0
  2. package/dist/core/ajax.d.ts +30 -0
  3. package/dist/core/ajax.d.ts.map +1 -0
  4. package/dist/core/ajax.js +110 -0
  5. package/dist/core/ajax.js.map +1 -0
  6. package/dist/core/event-emitter.d.ts +29 -0
  7. package/dist/core/event-emitter.d.ts.map +1 -0
  8. package/dist/core/event-emitter.js +67 -0
  9. package/dist/core/event-emitter.js.map +1 -0
  10. package/dist/core/message.d.ts +28 -0
  11. package/dist/core/message.d.ts.map +1 -0
  12. package/dist/core/message.js +172 -0
  13. package/dist/core/message.js.map +1 -0
  14. package/dist/core/view.d.ts +49 -0
  15. package/dist/core/view.d.ts.map +1 -0
  16. package/dist/core/view.js +87 -0
  17. package/dist/core/view.js.map +1 -0
  18. package/dist/formatting/formatters.d.ts +9 -0
  19. package/dist/formatting/formatters.d.ts.map +1 -0
  20. package/dist/formatting/formatters.js +109 -0
  21. package/dist/formatting/formatters.js.map +1 -0
  22. package/dist/formatting/registry.d.ts +31 -0
  23. package/dist/formatting/registry.d.ts.map +1 -0
  24. package/dist/formatting/registry.js +49 -0
  25. package/dist/formatting/registry.js.map +1 -0
  26. package/dist/index.d.ts +31 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +104 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/types.d.ts +66 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +5 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/utils/dom.d.ts +38 -0
  35. package/dist/utils/dom.d.ts.map +1 -0
  36. package/dist/utils/dom.js +95 -0
  37. package/dist/utils/dom.js.map +1 -0
  38. package/dist/utils/sprintf.d.ts +16 -0
  39. package/dist/utils/sprintf.d.ts.map +1 -0
  40. package/dist/utils/sprintf.js +116 -0
  41. package/dist/utils/sprintf.js.map +1 -0
  42. package/dist/validation/constraints.d.ts +23 -0
  43. package/dist/validation/constraints.d.ts.map +1 -0
  44. package/dist/validation/constraints.js +131 -0
  45. package/dist/validation/constraints.js.map +1 -0
  46. package/dist/validation/validator.d.ts +45 -0
  47. package/dist/validation/validator.d.ts.map +1 -0
  48. package/dist/validation/validator.js +210 -0
  49. package/dist/validation/validator.js.map +1 -0
  50. package/package.json +26 -0
  51. package/readme.txt +4 -0
  52. package/src/core/ajax.ts +127 -0
  53. package/src/core/event-emitter.ts +84 -0
  54. package/src/core/message.ts +212 -0
  55. package/src/core/view.ts +101 -0
  56. package/src/formatting/formatters.ts +118 -0
  57. package/src/formatting/registry.ts +53 -0
  58. package/src/index.ts +142 -0
  59. package/src/types.ts +85 -0
  60. package/src/utils/dom.ts +105 -0
  61. package/src/utils/sprintf.ts +141 -0
  62. package/src/validation/constraints.ts +168 -0
  63. 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>&ensp;&bull;&ensp;
12
+ <a href="#live-demo">Live Demo</a>&ensp;&bull;&ensp;
13
+ <a href="#modules">Modules</a>&ensp;&bull;&ensp;
14
+ <a href="#api-reference">API Reference</a>&ensp;&bull;&ensp;
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 &middot; ~1,600 lines &middot; strict TypeScript &middot; ES2022 target &middot; 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"}