Skip to content

Commit c524bfb

Browse files
✨ feat(components): implement comprehensive documentation for components
1 parent 6a85912 commit c524bfb

18 files changed

Lines changed: 6045 additions & 0 deletions
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
---
2+
title: Buttons
3+
description: |
4+
Build interactive buttons with static IDs and URL links for reliable, event-driven interactions.
5+
Learn the two button types, handler wiring, style options, emoji configuration, and rendering with ButtonService.
6+
order: 2
7+
category: Components
8+
---
9+
10+
## Button types
11+
12+
Spraxium provides two button variants, each with its own decorator and use case. Static buttons have a fixed *customId* and fire a handler when clicked. Link buttons open a URL in the user's browser and do not fire any handler.
13+
14+
Both types are defined as decorated classes and rendered through *ButtonService*. The visual properties, label, style, emoji, and disabled state, are configured in the decorator and stored as metadata. The renderer reads this metadata at build time and produces the appropriate discord.js *ButtonBuilder* instance.
15+
16+
<Table>
17+
<TableHead>
18+
<TableRow>
19+
<TableCell header>Type</TableCell>
20+
<TableCell header>Decorator</TableCell>
21+
<TableCell header>Has handler</TableCell>
22+
<TableCell header>ID format</TableCell>
23+
</TableRow>
24+
</TableHead>
25+
<TableBody>
26+
<TableRow>
27+
<TableCell>Static</TableCell>
28+
<TableCell>*@Button*</TableCell>
29+
<TableCell>Yes, via *@ButtonHandler*</TableCell>
30+
<TableCell>Fixed *customId* string</TableCell>
31+
</TableRow>
32+
<TableRow>
33+
<TableCell>Link</TableCell>
34+
<TableCell>*@LinkButton*</TableCell>
35+
<TableCell>No (opens URL)</TableCell>
36+
<TableCell>No *customId*, uses *url*</TableCell>
37+
</TableRow>
38+
39+
</TableBody>
40+
</Table>
41+
42+
## Static buttons
43+
44+
A static button is the simplest interactive component. You decorate a class with *@Button*, providing a *customId* and a *label* at minimum. The *customId* must be unique across your entire bot because the dispatcher matches incoming interactions by exact *customId* equality. The *style* field maps to Discord's button styles and defaults to `'primary'` if omitted.
45+
46+
<CodeTabs>
47+
<Tab label="Component" language="typescript">
48+
```typescript filename="src/components/buttons/confirm.button.ts"
49+
import { Button } from '@spraxium/components';
50+
51+
@Button({
52+
customId: 'confirm_action',
53+
label: 'Confirm',
54+
style: 'success',
55+
emoji: '',
56+
})
57+
export class ConfirmButton {}
58+
```
59+
</Tab>
60+
<Tab label="Handler" language="typescript">
61+
```typescript filename="src/components/buttons/confirm.handler.ts"
62+
import { ButtonHandler, Ctx } from '@spraxium/components';
63+
import type { ButtonInteraction } from 'discord.js';
64+
import { ConfirmButton } from './confirm.button';
65+
66+
@ButtonHandler(ConfirmButton)
67+
export class ConfirmButtonHandler {
68+
async handle(@Ctx() interaction: ButtonInteraction): Promise<void> {
69+
await interaction.reply({ content: 'Action confirmed.', ephemeral: true });
70+
}
71+
}
72+
```
73+
</Tab>
74+
</CodeTabs>
75+
76+
The handler class must be decorated with *@ButtonHandler* and pass the component class as its argument. This creates a metadata link that the dispatcher resolves during bootstrap. The *handle* method receives parameters through decorators: *@Ctx()* injects the raw *ButtonInteraction*, and *@FlowContext()* injects the context object when present.
77+
78+
## Button configuration reference
79+
80+
<Table>
81+
<TableHead>
82+
<TableRow>
83+
<TableCell header>Field</TableCell>
84+
<TableCell header>Type</TableCell>
85+
<TableCell header>Required</TableCell>
86+
<TableCell header>Purpose</TableCell>
87+
</TableRow>
88+
</TableHead>
89+
<TableBody>
90+
<TableRow>
91+
<TableCell>*customId*</TableCell>
92+
<TableCell>`string`</TableCell>
93+
<TableCell>Yes</TableCell>
94+
<TableCell>Unique identifier used for interaction routing. Must be unique across the bot.</TableCell>
95+
</TableRow>
96+
<TableRow>
97+
<TableCell>*url*</TableCell>
98+
<TableCell>`string`</TableCell>
99+
<TableCell>Yes (link)</TableCell>
100+
<TableCell>URL opened when the link button is clicked. No handler fires.</TableCell>
101+
</TableRow>
102+
<TableRow>
103+
<TableCell>*label*</TableCell>
104+
<TableCell>`string`</TableCell>
105+
<TableCell>Yes</TableCell>
106+
<TableCell>Text displayed on the button face.</TableCell>
107+
</TableRow>
108+
<TableRow>
109+
<TableCell>*style*</TableCell>
110+
<TableCell>`'primary' | 'secondary' | 'success' | 'danger'`</TableCell>
111+
<TableCell>No</TableCell>
112+
<TableCell>Visual style. Not applicable to link buttons. Defaults to `'primary'`.</TableCell>
113+
</TableRow>
114+
<TableRow>
115+
<TableCell>*emoji*</TableCell>
116+
<TableCell>`string | { id?, name?, animated? }`</TableCell>
117+
<TableCell>No</TableCell>
118+
<TableCell>Unicode emoji string or custom emoji object displayed beside the label.</TableCell>
119+
</TableRow>
120+
<TableRow>
121+
<TableCell>*disabled*</TableCell>
122+
<TableCell>`boolean`</TableCell>
123+
<TableCell>No</TableCell>
124+
<TableCell>When `true`, the button is rendered as grayed-out and non-interactive.</TableCell>
125+
</TableRow>
126+
</TableBody>
127+
</Table>
128+
129+
## Link buttons
130+
131+
Link buttons open a URL in the user's browser without firing any handler on the bot side. They are useful for directing users to documentation, dashboards, or external tools. Because Discord requires a different button style for links, the *@LinkButton* decorator omits *customId* and *style* in favor of a *url* field.
132+
133+
```typescript filename="src/components/buttons/docs-link.button.ts"
134+
import { LinkButton } from '@spraxium/components';
135+
136+
@LinkButton({
137+
url: 'https://docs.example.com',
138+
label: 'View Documentation',
139+
emoji: '📖',
140+
})
141+
export class DocsLinkButton {}
142+
```
143+
144+
Link buttons can be mixed with static buttons in the same action row. You pass all button classes to *ButtonService.build* and the renderer produces the correct builder type for each one based on the metadata.
145+
146+
147+
## Rendering with ButtonService
148+
149+
*ButtonService* is the injectable service that transforms decorated classes into discord.js action rows. It provides two methods for different rendering scenarios. All methods read component metadata via reflection, so the service does not need explicit configuration beyond the decorated classes.
150+
151+
<Table>
152+
<TableHead>
153+
<TableRow>
154+
<TableCell header>Method</TableCell>
155+
<TableCell header>Signature</TableCell>
156+
<TableCell header>Purpose</TableCell>
157+
</TableRow>
158+
</TableHead>
159+
<TableBody>
160+
<TableRow>
161+
<TableCell>*build*</TableCell>
162+
<TableCell>`build(input: Class | Class[], context?): ActionRowBuilder`</TableCell>
163+
<TableCell>Renders one or more static/link buttons into a single action row. Attaches context to all buttons when provided.</TableCell>
164+
</TableRow>
165+
<TableRow>
166+
<TableCell>*rowWithContext*</TableCell>
167+
<TableCell>`rowWithContext(context, ...Classes): ActionRowBuilder`</TableCell>
168+
<TableCell>Convenience wrapper that calls *build* with context as the first argument and spreads classes.</TableCell>
169+
</TableRow>
170+
171+
</TableBody>
172+
</Table>
173+
174+
<Callout type="tip">
175+
Discord limits action rows to five buttons. The service throws an error if you pass more than five classes to *build*.
176+
For layouts with more buttons, create multiple rows by calling *build* multiple times and pass each row individually in the *components* array.
177+
</Callout>
178+
179+
```typescript filename="src/commands/moderation.command-handler.ts"
180+
import { Ctx, SlashCommandHandler } from '@spraxium/common';
181+
import { ButtonService, ContextService } from '@spraxium/components';
182+
import type { ChatInputCommandInteraction } from 'discord.js';
183+
import { ConfirmButton } from '../components/buttons/confirm.button';
184+
import { CancelButton } from '../components/buttons/cancel.button';
185+
import { DocsLinkButton } from '../components/buttons/docs-link.button';
186+
import { ModerationCommand } from './moderation.command';
187+
188+
@SlashCommandHandler(ModerationCommand)
189+
export class ModerationCommandHandler {
190+
constructor(
191+
private readonly buttons: ButtonService,
192+
private readonly context: ContextService,
193+
) {}
194+
195+
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
196+
const ctx = await this.context.create({ targetUserId: '123456' });
197+
const actionRow = this.buttons.rowWithContext(ctx, ConfirmButton, CancelButton);
198+
const linkRow = this.buttons.build(DocsLinkButton);
199+
200+
await interaction.reply({
201+
content: 'Review the moderation action below.',
202+
components: [actionRow, linkRow],
203+
});
204+
}
205+
}
206+
```
207+
208+
## Button style reference
209+
210+
The *style* field accepts one of four named values that map to Discord's *ButtonStyle* enum. Each style controls the button's color and visual prominence. Link buttons always use the link style automatically and do not accept a *style* field.
211+
212+
<Table>
213+
<TableHead>
214+
<TableRow>
215+
<TableCell header>Name</TableCell>
216+
<TableCell header>Discord Style</TableCell>
217+
<TableCell header>Color</TableCell>
218+
<TableCell header>Use case</TableCell>
219+
</TableRow>
220+
</TableHead>
221+
<TableBody>
222+
<TableRow>
223+
<TableCell>`'primary'`</TableCell>
224+
<TableCell>*ButtonStyle.Primary*</TableCell>
225+
<TableCell>Blurple</TableCell>
226+
<TableCell>Default action, general purpose interactions.</TableCell>
227+
</TableRow>
228+
<TableRow>
229+
<TableCell>`'secondary'`</TableCell>
230+
<TableCell>*ButtonStyle.Secondary*</TableCell>
231+
<TableCell>Gray</TableCell>
232+
<TableCell>Lower-emphasis actions like cancel or dismiss.</TableCell>
233+
</TableRow>
234+
<TableRow>
235+
<TableCell>`'success'`</TableCell>
236+
<TableCell>*ButtonStyle.Success*</TableCell>
237+
<TableCell>Green</TableCell>
238+
<TableCell>Confirmation and positive outcome actions.</TableCell>
239+
</TableRow>
240+
<TableRow>
241+
<TableCell>`'danger'`</TableCell>
242+
<TableCell>*ButtonStyle.Danger*</TableCell>
243+
<TableCell>Red</TableCell>
244+
<TableCell>Destructive or irreversible actions like bans and deletes.</TableCell>
245+
</TableRow>
246+
</TableBody>
247+
</Table>
248+
249+
## Module registration
250+
251+
Only handler classes need to be registered, and they go in the *handlers* array of your feature module. Component classes decorated with *@Button* or *@LinkButton* are metadata-only and are never added to any module. The dispatcher reads their customId and configuration directly from the class reference stored inside each *@ButtonHandler* decorator, with no DI instantiation required.
252+
253+
```typescript filename="src/modules/moderation/moderation.module.ts"
254+
import { Module } from '@spraxium/common';
255+
import { ConfirmButtonHandler } from './components/confirm.handler';
256+
import { CancelButtonHandler } from './components/cancel.handler';
257+
import { UserBanButtonHandler } from './components/user-ban.handler';
258+
259+
@Module({
260+
handlers: [
261+
ConfirmButtonHandler,
262+
CancelButtonHandler,
263+
UserBanButtonHandler,
264+
],
265+
})
266+
export class ModerationModule {}
267+
```
268+
269+
## Next page
270+
271+
Continue with [Select Menus](/guide/components-selects).

0 commit comments

Comments
 (0)