diff --git a/apps/docs/pages/docs/_meta.json b/apps/docs/pages/docs/_meta.json
index 20d9a17..260d720 100644
--- a/apps/docs/pages/docs/_meta.json
+++ b/apps/docs/pages/docs/_meta.json
@@ -4,5 +4,7 @@
"command-file-setup": "Commands Setup",
"event-file-setup": "Events Setup",
"validation-file-setup": "Validations Setup",
- "migrating-from-djs-commander": "Migrating from DJS-Commander"
+ "migrating-from-djs-commander": "Migrating from DJS-Commander",
+ "buttonkit-example": "ButtonKit Example",
+ "using-signal": "Using Signal"
}
\ No newline at end of file
diff --git a/apps/docs/pages/docs/button-kit.mdx b/apps/docs/pages/docs/button-kit.mdx
new file mode 100644
index 0000000..c1b4b49
--- /dev/null
+++ b/apps/docs/pages/docs/button-kit.mdx
@@ -0,0 +1,77 @@
+import { Tabs } from 'nextra/components';
+
+# ButtonKit
+
+ButtonKit is a class that allows you to use buttons easily by extending native [ButtonBuilder](https://old.discordjs.dev/#/docs/discord.js/main/class/ButtonBuilder) features.
+
+## Usage
+
+
+
+ ```js filename="commands/counter.js" copy
+ const { ButtonKit } = require('commandkit');
+ const { ButtonStyle } = require('discord.js');
+
+ // create a button
+ const button = new ButtonKit()
+ .setEmoji('👍')
+ .setStyle(ButtonStyle.Primary)
+ // custom id is required to use onClick
+ .setCustomId('button');
+
+ // listen to the button interaction right away
+ button.onClick(
+ (interaction) => {
+ // reply to the interaction
+ interaction.reply('You clicked the button!');
+ },
+ { message },
+ );
+ ```
+
+
+ ```js filename="commands/counter.js" copy
+ import { ButtonKit } from 'commandkit';
+ import { ButtonStyle } from 'discord.js';
+
+ // create a button
+ const button = new ButtonKit()
+ .setEmoji('👍')
+ .setStyle(ButtonStyle.Primary)
+ // custom id is required to use onClick
+ .setCustomId('button');
+
+ // listen to the button interaction right away
+ button.onClick(
+ (interaction) => {
+ // reply to the interaction
+ interaction.reply('You clicked the button!');
+ },
+ { message },
+ );
+ ```
+
+
+ ```ts filename="commands/counter.ts" copy
+ import { ButtonKit } from 'commandkit';
+ import { ButtonStyle, ButtonInteraction } from 'discord.js';
+
+ // create a button
+ const button = new ButtonKit()
+ .setEmoji('👍')
+ .setStyle(ButtonStyle.Primary)
+ // custom id is required to use onClick
+ .setCustomId('button');
+
+ // listen to the button interaction right away
+ button.onClick(
+ (interaction: ButtonInteraction) => {
+ // reply to the interaction
+ interaction.reply('You clicked the button!');
+ },
+ { message },
+ );
+ ```
+
+
+
diff --git a/apps/docs/pages/docs/using-signal.mdx b/apps/docs/pages/docs/using-signal.mdx
new file mode 100644
index 0000000..ba42202
--- /dev/null
+++ b/apps/docs/pages/docs/using-signal.mdx
@@ -0,0 +1,475 @@
+import { Tabs } from 'nextra/components';
+
+# Using signal
+
+Signal is a simple way to add states and basic reactivity to your commands. It is partially similar to solidjs signal.
+
+## Creating a signal
+
+Creating a signal is simple, you just need to call the `createSignal` function from `commandkit`. A signal in commandkit is a function that returns the current value of the signal. It also has a `set` function that can be used to update the value of the signal. The `dispose` function can be used to dispose the signal and all its subscribers.
+
+
+
+ ```js filename="signal.js" copy
+ const { createSignal } = require('commandkit');
+
+ const [value, setValue, dispose] = createSignal(0);
+
+ console.log(value()); // 0
+
+ setValue(1);
+
+ console.log(value()); // 1
+
+ dispose(); // dispose subscribers
+ ```
+
+
+ ```js filename="signal.js" copy
+ import { createSignal } from 'commandkit';
+
+ const [value, setValue, dispose] = createSignal(0);
+
+ console.log(value()); // 0
+
+ setValue(1);
+
+ console.log(value()); // 1
+
+ dispose(); // dispose subscribers
+ ```
+
+
+ ```ts filename="signal.ts" copy
+ import { createSignal } from 'commandkit';
+
+ const [value, setValue, dispose] = createSignal(0);
+
+ console.log(value()); // 0
+
+ setValue(1);
+
+ console.log(value()); // 1
+
+ dispose(); // dispose subscribers
+ ```
+
+
+
+
+### Handling side effects
+
+You can also handle side effects with signals, by using the `createEffect` function. Side effects are functions that run every time the signal value changes.
+
+
+
+ ```js filename="signal.js" copy
+ const { createSignal, createEffect } = require('commandkit');
+
+ const [value, setValue, dispose] = createSignal(0);
+
+ // This will run every time the value changes
+ createEffect(() => {
+ console.log(`Current value is ${value()}`);
+ });
+
+ setValue(1); // This will log "Current value is 1"
+ setValue(2); // This will log "Current value is 2"
+
+ dispose(); // dispose subscribers
+
+ setValue(3); // This will not log anything because we disposed the subscribers
+ ```
+
+
+ ```js filename="signal.js" copy
+ import { createSignal, createEffect } from 'commandkit';
+
+ const [value, setValue, dispose] = createSignal(0);
+
+ // This will run every time the value changes
+ createEffect(() => {
+ console.log(`Current value is ${value()}`);
+ });
+
+ setValue(1); // This will log "Current value is 1"
+ setValue(2); // This will log "Current value is 2"
+
+ dispose(); // dispose subscribers
+
+ setValue(3); // This will not log anything because we disposed the subscribers
+ ```
+
+
+ ```ts filename="signal.ts" copy
+ import { createSignal, createEffect } from 'commandkit';
+
+ const [value, setValue, dispose] = createSignal(0);
+
+ // This will run every time the value changes
+ createEffect(() => {
+ console.log(`Current value is ${value()}`);
+ });
+
+ setValue(1); // This will log "Current value is 1"
+ setValue(2); // This will log "Current value is 2"
+
+ dispose(); // dispose subscribers
+
+ setValue(3); // This will not log anything because we disposed the subscribers
+ ```
+
+
+
+
+## Count command example
+
+
+
+ ```js filename="commands/counter.js" copy
+ const {
+ createSignal,
+ createEffect.
+ ButtonKit
+ } = require('commandkit');
+ const { ButtonStyle, ActionRowBuilder } = require('discord.js');
+
+ export const data = {
+ name: 'counter',
+ description: 'A simple counter command',
+ };
+
+ // get the buttons
+ function getButtons() {
+ // decrement button
+ const dec = new ButtonKit()
+ .setEmoji('➖')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('decrement');
+
+ // reset button
+ const reset = new ButtonKit()
+ .setEmoji('0️⃣')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('reset');
+
+ // increment button
+ const inc = new ButtonKit()
+ .setEmoji('➕')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('increment');
+
+ // dispose button
+ const trash = new ButtonKit()
+ .setEmoji('🗑️')
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId('trash');
+
+ // action row
+ const row = new ActionRowBuilder()
+ .addComponents(dec, reset, inc, trash);
+
+ return { dec, reset, inc, trash, row };
+ }
+
+ export const run = async ({ interaction }) => {
+ // create the signal
+ const [count, setCount, dispose] = createSignal(0);
+ // create the buttons
+ const { dec, reset, inc, trash, row } = getButtons();
+
+ // temporary variable to hold button interactions
+ let inter;
+
+ // send the initial message with the buttons
+ const message = await interaction.reply({
+ content: `Count is ${count()}`,
+ components: [row],
+ fetchReply: true,
+ });
+
+ // Now, we subscribe to count signal and update the message every time the count changes
+ createEffect(() => {
+ // make sure to "always" call the value function inside createEffect, else subscription will not occur
+ const value = count();
+
+ // update the message
+ inter?.update(`Count is ${value}`);
+ });
+
+ // let's add a handler to decrement the count
+ dec.onClick((interaction) => {
+ inter = interaction;
+ setCount((prev) => prev - 1);
+ }, { message });
+
+ // let's add a handler to reset the count
+ reset.onClick((interaction) => {
+ inter = interaction;
+ setCount(0);
+ }, { message });
+
+ // let's add a handler to increment the count
+ inc.onClick((interaction) => {
+ inter = interaction;
+ setCount((prev) => prev + 1);
+ }, { message });
+
+ // let's add a handler to dispose the buttons and the signal
+ trash.onClick(async (interaction) => {
+ const disposed = row.setComponents(
+ row.components.map((button) => {
+ // remove 'onClick' handler and disable the button
+ return button
+ .onClick(null)
+ .setDisabled(true);
+ }),
+ );
+
+ // dispose the signal
+ dispose();
+
+ // finally acknowledge the interaction
+ await interaction.update({
+ content: 'Finished counting!',
+ components: [disposed],
+ });
+ }, { message });
+ }
+ ```
+
+
+
+ ```js filename="commands/counter.js" copy
+ import {
+ createSignal,
+ createEffect,
+ ButtonKit
+ } from 'commandkit';
+ import { ButtonStyle, ActionRowBuilder } from 'discord.js';
+
+ export const data = {
+ name: 'counter',
+ description: 'A simple counter command',
+ };
+
+ // get the buttons
+ function getButtons() {
+ // decrement button
+ const dec = new ButtonKit()
+ .setEmoji('➖')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('decrement');
+
+ // reset button
+ const reset = new ButtonKit()
+ .setEmoji('0️⃣')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('reset');
+
+ // increment button
+ const inc = new ButtonKit()
+ .setEmoji('➕')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('increment');
+
+ // dispose button
+ const trash = new ButtonKit()
+ .setEmoji('🗑️')
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId('trash');
+
+ // action row
+ const row = new ActionRowBuilder()
+ .addComponents(dec, reset, inc, trash);
+
+ return { dec, reset, inc, trash, row };
+ }
+
+ export const run = async ({ interaction }) => {
+ // create the signal
+ const [count, setCount, dispose] = createSignal(0);
+ // create the buttons
+ const { dec, reset, inc, trash, row } = getButtons();
+
+ // temporary variable to hold button interactions
+ let inter;
+
+ // send the initial message with the buttons
+ const message = await interaction.reply({
+ content: `Count is ${count()}`,
+ components: [row],
+ fetchReply: true,
+ });
+
+ // Now, we subscribe to count signal and update the message every time the count changes
+ createEffect(() => {
+ // make sure to "always" call the value function inside createEffect, else subscription will not occur
+ const value = count();
+
+ // update the message
+ inter?.update(`Count is ${value}`);
+ });
+
+ // let's add a handler to decrement the count
+ dec.onClick((interaction) => {
+ inter = interaction;
+ setCount((prev) => prev - 1);
+ }, { message });
+
+ // let's add a handler to reset the count
+ reset.onClick((interaction) => {
+ inter = interaction;
+ setCount(0);
+ }, { message });
+
+ // let's add a handler to increment the count
+ inc.onClick((interaction) => {
+ inter = interaction;
+ setCount((prev) => prev + 1);
+ }, { message });
+
+ // let's add a handler to dispose the buttons and the signal
+ trash.onClick(async (interaction) => {
+ const disposed = row.setComponents(
+ row.components.map((button) => {
+ // remove 'onClick' handler and disable the button
+ return button
+ .onClick(null)
+ .setDisabled(true);
+ }),
+ );
+
+ // dispose the signal
+ dispose();
+
+ // finally acknowledge the interaction
+ await interaction.update({
+ content: 'Finished counting!',
+ components: [disposed],
+ });
+ }, { message });
+ }
+ ```
+
+
+
+ ```js filename="commands/counter.ts" copy
+ import {
+ createSignal,
+ createEffect,
+ ButtonKit,
+ type SlashCommandProps
+ } from 'commandkit';
+ import { ButtonStyle, ActionRowBuilder, type ButtonInteraction } from 'discord.js';
+
+ export const data = {
+ name: 'counter',
+ description: 'A simple counter command',
+ };
+
+ // get the buttons
+ function getButtons() {
+ // decrement button
+ const dec = new ButtonKit()
+ .setEmoji('➖')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('decrement');
+
+ // reset button
+ const reset = new ButtonKit()
+ .setEmoji('0️⃣')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('reset');
+
+ // increment button
+ const inc = new ButtonKit()
+ .setEmoji('➕')
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId('increment');
+
+ // dispose button
+ const trash = new ButtonKit()
+ .setEmoji('🗑️')
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId('trash');
+
+ // action row
+ const row = new ActionRowBuilder()
+ .addComponents(dec, reset, inc, trash);
+
+ return { dec, reset, inc, trash, row };
+ }
+
+ export const run = async ({ interaction }: SlashCommandProps) => {
+ // create the signal
+ const [count, setCount, dispose] = createSignal(0);
+ // create the buttons
+ const { dec, reset, inc, trash, row } = getButtons();
+
+ // temporary variable to hold button interactions
+ let inter: ButtonInteraction;
+
+ // send the initial message with the buttons
+ const message = await interaction.reply({
+ content: `Count is ${count()}`,
+ components: [row],
+ fetchReply: true,
+ });
+
+ // Now, we subscribe to count signal and update the message every time the count changes
+ createEffect(() => {
+ // make sure to "always" call the value function inside createEffect, else subscription will not occur
+ const value = count();
+
+ // update the message
+ inter?.update(`Count is ${value}`);
+ });
+
+ // let's add a handler to decrement the count
+ dec.onClick((interaction) => {
+ inter = interaction;
+ setCount((prev) => prev - 1);
+ }, { message });
+
+ // let's add a handler to reset the count
+ reset.onClick((interaction) => {
+ inter = interaction;
+ setCount(0);
+ }, { message });
+
+ // let's add a handler to increment the count
+ inc.onClick((interaction) => {
+ inter = interaction;
+ setCount((prev) => prev + 1);
+ }, { message });
+
+ // let's add a handler to dispose the buttons and the signal
+ trash.onClick(async (interaction) => {
+ const disposed = row.setComponents(
+ row.components.map((button) => {
+ // remove 'onClick' handler and disable the button
+ return button
+ .onClick(null)
+ .setDisabled(true);
+ }),
+ );
+
+ // dispose the signal
+ dispose();
+
+ // finally acknowledge the interaction
+ await interaction.update({
+ content: 'Finished counting!',
+ components: [disposed],
+ });
+ }, { message });
+ }
+ ```
+
+
+
+
+### Result
+
+
diff --git a/apps/docs/public/counter.mp4 b/apps/docs/public/counter.mp4
new file mode 100644
index 0000000..bba6709
Binary files /dev/null and b/apps/docs/public/counter.mp4 differ
diff --git a/packages/commandkit/src/utils/signal.ts b/packages/commandkit/src/utils/signal.ts
index 1f96bfe..c98595f 100644
--- a/packages/commandkit/src/utils/signal.ts
+++ b/packages/commandkit/src/utils/signal.ts
@@ -12,16 +12,20 @@ const context: CommandKitEffectCallback[] = [];
export function createSignal(value?: CommandKitSignalInitializer) {
const subscribers = new Set<() => void>();
+ let disposed = false;
let val: T | undefined = value instanceof Function ? value() : value;
const getter = () => {
- const running = getCurrentObserver();
+ if (!disposed) {
+ const running = getCurrentObserver();
+ if (running) subscribers.add(running);
+ }
- if (running) subscribers.add(running);
return val;
};
const setter = (newValue: CommandKitSignalUpdater) => {
+ if (disposed) return;
val = newValue instanceof Function ? newValue(val!) : newValue;
for (const subscriber of subscribers) {
@@ -31,6 +35,7 @@ export function createSignal(value?: CommandKitSignalInitializer
const dispose = () => {
subscribers.clear();
+ disposed = true;
};
return [getter, setter, dispose] as CommandKitSignal;