Skip to content

Commit 14d83e0

Browse files
📝 docs(en): fix frontmatter, add missing logger config, fix modals nav chain
- logger-transports: fix opening --- (was --); add commandLogging and timestampFormat to SpraxiumLoggerConfig table; add Timestamp format section with preset table and examples - slash-commands-handlers-injection: fix opening --- (was --) - modals-overview: replace deprecated @field section with handler field injection intro using new typed decorators; update dispatch flow step to use new decorator names - modals-fields: fix next-page link to point to handlers-injection (was dynamic modals) - modals-handlers-injection: add Next page link to dynamic modals; unorphan the page - modals-dynamics/validation/cache: fix order numbers (3→4, 4→5, 5→6) to resolve duplicate order conflict
1 parent 892b615 commit 14d83e0

8 files changed

Lines changed: 314 additions & 41 deletions

en/logger/logger-transports.mdx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
--
1+
---
22
title: Transports and Configuration
33
description: |
44
Configure the logger with custom levels, Discord transports, and token masking.
@@ -69,9 +69,44 @@ The _levels_ array registers custom log levels with their console colors. These
6969
Configuration for sending logs to Discord. Omit to disable.
7070
</TableCell>
7171
</TableRow>
72+
<TableRow>
73+
<TableCell>*commandLogging*</TableCell>
74+
<TableCell>`boolean`</TableCell>
75+
<TableCell>`false`</TableCell>
76+
<TableCell>When `true`, registers a listener that logs every slash command execution at the `command` level. See [Command logging](#command-logging) below.</TableCell>
77+
</TableRow>
78+
<TableRow>
79+
<TableCell>*timestampFormat*</TableCell>
80+
<TableCell>`'default' | 'iso' | 'time-only' | (d: Date) => string`</TableCell>
81+
<TableCell>`'default'`</TableCell>
82+
<TableCell>Controls how the timestamp prefix is rendered in console output. See [Timestamp format](#timestamp-format) below.</TableCell>
83+
</TableRow>
7284
</TableBody>
7385
</Table>
7486

87+
## Timestamp format
88+
89+
The `timestampFormat` option controls how the date/time prefix looks in every console log line. Choose one of the three named presets or supply a custom function.
90+
91+
| Value | Example output | When to use |
92+
|---|---|---|
93+
| `'default'` | `12/04/2026 - 14:32:05` | Human-readable, good for most bots |
94+
| `'iso'` | `2026-04-12T14:32:05.000Z` | Structured logs, easy to parse programmatically |
95+
| `'time-only'` | `14:32:05` | Minimal output when the date is not needed |
96+
| `(d) => string` | *(custom)* | Full control — return any string from the `Date` argument |
97+
98+
```typescript filename="config/logger.config.ts"
99+
import type { SpraxiumLoggerConfig } from '@spraxium/logger';
100+
101+
export const loggerConfig: SpraxiumLoggerConfig = {
102+
// Use ISO 8601 timestamps for structured log pipelines
103+
timestampFormat: 'iso',
104+
105+
// Or provide a custom format
106+
// timestampFormat: (d) => d.toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' }),
107+
};
108+
```
109+
75110
## The Discord transport
76111

77112
The Discord transport sends log entries as embeds to a text channel or webhook. It only forwards messages whose level matches the _levels_ array in its configuration. This lets you send errors and warnings to Discord while keeping info and debug messages in the console only.

en/modals/modals-cache.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Modal Cache
33
description: |
44
Persist submitted field values between modal reopens with @ModalCache and ModalService.buildFor.
55
Learn cache key format, TTL configuration, the manual cache API, and combining cache with dynamic modals.
6-
order: 5
6+
order: 6
77
category: Modals
88
---
99

en/modals/modals-dynamics.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Dynamic Modals
33
description: |
44
Generate modal fields at runtime with @ModalDynamic, @ModalDynamicFields, and @ModalWhen.
55
Build context-aware modals that adapt their structure based on user data or application state.
6-
order: 3
6+
order: 4
77
category: Modals
88
---
99

en/modals/modals-fields.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,4 +447,4 @@ export function createPriorityChoices() {
447447

448448
## Next page
449449

450-
Continue with [Dynamic Modals](/guide/modals-dynamics).
450+
Continue with [Handlers and Field Injection](/guide/modals-handlers-injection).
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
---
2+
title: Handlers and Field Injection
3+
description: |
4+
Process modal submissions with @ModalHandler and inject field values directly into
5+
handle() parameters. Learn @ModalField, the ten typed field decorators, and @Ctx.
6+
order: 3
7+
category: Modals
8+
---
9+
10+
## The handler class
11+
12+
A handler class is a standalone class decorated with _@ModalHandler_ that contains the submission logic for a modal. The decorator receives the modal component class as its argument, creating a metadata link between the component schema and the submission logic. The handler class is instantiated by the DI container during module loading, so constructor parameters are resolved automatically from the module providers.
13+
14+
The handler must expose a _handle_ method. When a modal submission arrives whose `customId` matches the component's `id`, the _ModalDispatcher_ calls this method after the guard pipeline passes. Method parameters are populated from metadata: _@Ctx()_ injects the raw `ModalSubmitInteraction`, and field decorators inject the resolved value for each named field.
15+
16+
```typescript filename="src/modules/feedback/handlers/feedback.handler.ts"
17+
import { Ctx } from '@spraxium/common';
18+
import { ModalHandler, ModalTextField, type ModalContext } from '@spraxium/components';
19+
import { FeedbackModal } from '../components/feedback.modal';
20+
21+
@ModalHandler(FeedbackModal)
22+
export class FeedbackHandler {
23+
async handle(
24+
@ModalTextField('subject') subject: string,
25+
@ModalTextField('message') message: string,
26+
@Ctx() ctx: ModalContext,
27+
): Promise<void> {
28+
await ctx.reply({
29+
content: `Thanks for your feedback on *${subject}*!`,
30+
flags: 'Ephemeral',
31+
});
32+
}
33+
}
34+
```
35+
36+
`ModalContext` is exported from `@spraxium/components` and is a direct alias for `ModalSubmitInteraction`. Use whichever fits your team's import style; both are interchangeable.
37+
38+
## The @ModalHandler decorator
39+
40+
_@ModalHandler_ accepts the modal component class as its only argument. There is no routing configuration — each handler owns exactly one component, identified by the component's `id` string.
41+
42+
```typescript
43+
@ModalHandler(FeedbackModal)
44+
export class FeedbackHandler {
45+
async handle(...): Promise<void> { ... }
46+
}
47+
```
48+
49+
Spraxium reads the `id` from the _@ModalComponent_ decorator on `FeedbackModal` and registers this handler under that id. When Discord sends a `MODAL_SUBMIT` interaction, the dispatcher matches `customId` to the registered id and calls `handle`.
50+
51+
## Parameter injection with @Ctx and field decorators
52+
53+
Method parameters are resolved by the dispatcher at call time. Two categories of decorators control what each position receives.
54+
55+
**@Ctx()** injects the raw `ModalSubmitInteraction` (aliased as `ModalContext`). It can appear at any position and is the entry point to the full Discord API surface for the interaction — reply, deferReply, followUp, and the raw `fields` manager.
56+
57+
**Field decorators** inject the submitted value for a specific field, identified by the property name defined in the modal component class. The generic _@ModalField(fieldId)_ works for any field type. The typed variants — documented in the next section — carry the intent of the field type explicitly and should be preferred when the field type is known at compile time.
58+
59+
```typescript filename="src/modules/profile/handlers/profile.handler.ts"
60+
import { Ctx } from '@spraxium/common';
61+
import {
62+
ModalCheckboxField,
63+
ModalCheckboxGroupField,
64+
ModalHandler,
65+
ModalRadioGroupField,
66+
ModalStringSelectField,
67+
type ModalContext,
68+
} from '@spraxium/components';
69+
import { ProfileModal } from '../components/profile.modal';
70+
71+
@ModalHandler(ProfileModal)
72+
export class ProfileHandler {
73+
async handle(
74+
@Ctx() ctx: ModalContext,
75+
@ModalStringSelectField('role') role: string,
76+
@ModalRadioGroupField('timezone') timezone: string | null,
77+
@ModalCheckboxGroupField('notifications') notifications: string[],
78+
@ModalCheckboxField('acceptedRules') acceptedRules: boolean,
79+
): Promise<void> {
80+
if (!acceptedRules) {
81+
await ctx.reply({ content: '❌ You must accept the rules.', flags: 'Ephemeral' });
82+
return;
83+
}
84+
await ctx.reply({
85+
content: [
86+
'✅ Profile saved!',
87+
`**Role:** ${role}`,
88+
`**Timezone:** ${timezone ?? 'Not set'}`,
89+
`**Notifications:** ${notifications.join(', ') || 'None'}`,
90+
].join('\n'),
91+
flags: 'Ephemeral',
92+
});
93+
}
94+
}
95+
```
96+
97+
## Typed field decorators
98+
99+
Ten typed parameter decorators are provided, each corresponding to a specific field type in the modal component class. Using them makes the intended field type explicit in handler code without any runtime overhead — all typed decorators store the same `{ index, fieldId }` metadata as the generic _@ModalField_.
100+
101+
<Table>
102+
<TableHead>
103+
<TableRow>
104+
<TableCell header>Decorator</TableCell>
105+
<TableCell header>Component field type</TableCell>
106+
<TableCell header>Resolved TypeScript type</TableCell>
107+
</TableRow>
108+
</TableHead>
109+
<TableBody>
110+
<TableRow>
111+
<TableCell>`@ModalTextField(fieldId)`</TableCell>
112+
<TableCell>*@ModalInput*</TableCell>
113+
<TableCell>`string`</TableCell>
114+
</TableRow>
115+
<TableRow>
116+
<TableCell>`@ModalStringSelectField(fieldId)`</TableCell>
117+
<TableCell>*@ModalSelect*</TableCell>
118+
<TableCell>`string | null` (single) or `string[]` (multi)</TableCell>
119+
</TableRow>
120+
<TableRow>
121+
<TableCell>`@ModalUserSelectField(fieldId)`</TableCell>
122+
<TableCell>*@ModalUserSelect*</TableCell>
123+
<TableCell>`User | null` (single) or `User[]` (multi)</TableCell>
124+
</TableRow>
125+
<TableRow>
126+
<TableCell>`@ModalRoleSelectField(fieldId)`</TableCell>
127+
<TableCell>*@ModalRoleSelect*</TableCell>
128+
<TableCell>`Role | null` (single) or `Role[]` (multi)</TableCell>
129+
</TableRow>
130+
<TableRow>
131+
<TableCell>`@ModalMentionableSelectField(fieldId)`</TableCell>
132+
<TableCell>*@ModalMentionableSelect*</TableCell>
133+
<TableCell>`User | Role | null`</TableCell>
134+
</TableRow>
135+
<TableRow>
136+
<TableCell>`@ModalChannelSelectField(fieldId)`</TableCell>
137+
<TableCell>*@ModalChannelSelect*</TableCell>
138+
<TableCell>`GuildBasedChannel | null` (single) or `GuildBasedChannel[]` (multi)</TableCell>
139+
</TableRow>
140+
<TableRow>
141+
<TableCell>`@ModalRadioGroupField(fieldId)`</TableCell>
142+
<TableCell>*@ModalRadioGroup*</TableCell>
143+
<TableCell>`string | null`</TableCell>
144+
</TableRow>
145+
<TableRow>
146+
<TableCell>`@ModalCheckboxGroupField(fieldId)`</TableCell>
147+
<TableCell>*@ModalCheckboxGroup*</TableCell>
148+
<TableCell>`string[]`</TableCell>
149+
</TableRow>
150+
<TableRow>
151+
<TableCell>`@ModalCheckboxField(fieldId)`</TableCell>
152+
<TableCell>*@ModalCheckbox*</TableCell>
153+
<TableCell>`boolean`</TableCell>
154+
</TableRow>
155+
<TableRow>
156+
<TableCell>`@ModalFileUploadField(fieldId)`</TableCell>
157+
<TableCell>*@ModalFileUpload*</TableCell>
158+
<TableCell>`Attachment[]`</TableCell>
159+
</TableRow>
160+
</TableBody>
161+
</Table>
162+
163+
All typed decorators are imported from `@spraxium/components` alongside `ModalHandler`.
164+
165+
<Callout tone="info" title="Single vs. multi-value selects">
166+
For string, user, role, and channel select fields, whether the injected value
167+
is a single item or an array depends on the _maxValues_ configuration of the
168+
field in the component class. When _maxValues_ is greater than 1, the dispatcher
169+
returns an array; otherwise it returns a single value or `null`.
170+
</Callout>
171+
172+
## Accessing raw fields via @Ctx
173+
174+
For fields that don't have a corresponding field decorator — such as `radio_group` fields accessed outside of injection, or fields read conditionally — use `@Ctx()` and call the appropriate method on `ctx.fields` directly.
175+
176+
```typescript filename="src/modules/survey/handlers/survey.handler.ts"
177+
import { Ctx } from '@spraxium/common';
178+
import { ModalHandler, ModalTextField, type ModalContext } from '@spraxium/components';
179+
import { SurveyModal } from '../components/survey.modal';
180+
181+
@ModalHandler(SurveyModal)
182+
export class SurveyHandler {
183+
async handle(
184+
@ModalTextField('feedback') feedback: string,
185+
@Ctx() ctx: ModalContext,
186+
): Promise<void> {
187+
// Read radio group values directly from the interaction
188+
const rating = ctx.fields.getRadioGroup('rating') ?? 'N/A';
189+
const tags = ctx.fields.getCheckboxGroup('tags');
190+
191+
await ctx.reply({
192+
content: `Rating: ${rating}\nTags: ${tags.join(', ')}\nFeedback: ${feedback}`,
193+
flags: 'Ephemeral',
194+
});
195+
}
196+
}
197+
```
198+
199+
## Combining DI services with field injection
200+
201+
Because handler classes are instantiated through the DI container, any module provider can be injected through the constructor. This is the standard pattern for accessing databases, external APIs, or application services from inside a handler.
202+
203+
```typescript filename="src/modules/ticket/handlers/ticket-submit.handler.ts"
204+
import { Ctx } from '@spraxium/common';
205+
import { ModalHandler, ModalTextField, type ModalContext } from '@spraxium/components';
206+
import { Injectable } from '@spraxium/core';
207+
import { TicketModal } from '../components/ticket.modal';
208+
import { TicketService } from '../ticket.service';
209+
210+
@Injectable()
211+
@ModalHandler(TicketModal)
212+
export class TicketSubmitHandler {
213+
constructor(private readonly tickets: TicketService) {}
214+
215+
async handle(
216+
@ModalTextField('subject') subject: string,
217+
@ModalTextField('description') description: string,
218+
@Ctx() ctx: ModalContext,
219+
): Promise<void> {
220+
const ticket = await this.tickets.create({
221+
userId: ctx.user.id,
222+
subject,
223+
description,
224+
});
225+
226+
await ctx.reply({
227+
content: `✅ Ticket **#${ticket.id}** created!`,
228+
flags: 'Ephemeral',
229+
});
230+
}
231+
}
232+
```
233+
234+
## Module registration
235+
236+
Register handler classes in the _handlers_ array of a feature module. Modal component classes decorated with _@ModalComponent_ are metadata-only and are never registered; Spraxium resolves them transitively from the handler's `@ModalHandler` link.
237+
238+
```typescript filename="src/modules/feedback/feedback.module.ts"
239+
import { Module } from '@spraxium/core';
240+
import { FeedbackHandler } from './handlers/feedback.handler';
241+
242+
@Module({
243+
handlers: [FeedbackHandler],
244+
})
245+
export class FeedbackModule {}
246+
```
247+
248+
<Callout tone="warning" title="Register the handler, not the component">
249+
Only handler classes decorated with *@ModalHandler* go in the *handlers*
250+
array. Adding the modal component class to the module will have no effect and
251+
may produce confusing log output during boot.
252+
</Callout>
253+
254+
## Next page
255+
256+
Continue with [Dynamic Modals](/guide/modals-dynamics).

en/modals/modals-overview.mdx

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -119,42 +119,24 @@ export class FeedbackCommandHandler {
119119
</Tab>
120120
</CodeTabs>
121121
122-
## The @Field parameter decorator
122+
## Handler field injection
123123
124-
The _@Field_ decorator extracts a specific field value from the modal submission by matching the property name defined in the modal component class. The extracted value is always a string for text inputs, or the selected value(s) for select-type fields. This decorator eliminates the need to manually access `interaction.fields.getTextInputValue()` or iterate through components.
124+
The `@ModalHandler` handler class receives submitted field values through parameter decorators. Use typed decorators like `@ModalTextField`, `@ModalStringSelectField`, and `@ModalCheckboxField` to inject each field value directly into the `handle()` method — no manual interaction object parsing needed.
125125
126-
<Table>
127-
<TableHead>
128-
<TableRow>
129-
<TableCell header>Usage</TableCell>
130-
<TableCell header>Resolved value</TableCell>
131-
<TableCell header>Notes</TableCell>
132-
</TableRow>
133-
</TableHead>
134-
<TableBody>
135-
<TableRow>
136-
<TableCell>`@Field('subject')`</TableCell>
137-
<TableCell>Text input value as `string`</TableCell>
138-
<TableCell>
139-
Matches the property name in the modal component class.
140-
</TableCell>
141-
</TableRow>
142-
<TableRow>
143-
<TableCell>`@Field('category')`</TableCell>
144-
<TableCell>Selected value as `string`</TableCell>
145-
<TableCell>
146-
For single-value selects, returns the selected option value.
147-
</TableCell>
148-
</TableRow>
149-
<TableRow>
150-
<TableCell>`@Field('tags')`</TableCell>
151-
<TableCell>Selected values as `string[]`</TableCell>
152-
<TableCell>
153-
For multi-value selects, returns an array of selected values.
154-
</TableCell>
155-
</TableRow>
156-
</TableBody>
157-
</Table>
126+
```typescript
127+
@ModalHandler(FeedbackModal)
128+
export class FeedbackHandler {
129+
async handle(
130+
@ModalTextField('subject') subject: string,
131+
@ModalTextField('message') message: string,
132+
@Ctx() ctx: ModalContext,
133+
): Promise<void> {
134+
await ctx.reply({ content: `Received: ${subject}`, flags: 'Ephemeral' });
135+
}
136+
}
137+
```
138+
139+
The old `@Field` decorator is deprecated in 0.2.0 in favor of the typed variants. For the full list of typed field decorators and their resolved TypeScript types, see [Handlers and Field Injection](/guide/modals-handlers-injection).
158140

159141
## ModalService API reference
160142

@@ -235,7 +217,7 @@ The Discord.js client emits an *interactionCreate* event with a *ModalSubmitInte
235217
</Step>
236218

237219
<Step title="Field resolution and handler invocation">
238-
The dispatcher resolves *@Field* parameter decorators by extracting values from the submission. The *@Ctx* decorator receives the raw *ModalSubmitInteraction*. If a *@FlowContext* decorator is present, the resolved context is injected. The handler's *handle* method is called with all resolved parameters.
220+
The dispatcher resolves field parameter decorators (`@ModalTextField`, `@ModalStringSelectField`, etc.) by extracting values from the submission. The *@Ctx* decorator receives the raw *ModalSubmitInteraction*. If a *@FlowContext* decorator is present, the resolved context is injected. The handler's *handle* method is called with all resolved parameters.
239221
</Step>
240222
</Steps>
241223

0 commit comments

Comments
 (0)