Disable a button using its custom_id

Can I disable a button (that has not been assigned to a variable) by using it's custom_id? I am creating buttons dynamically with a for loop with the following code:
if(similarMatchesList){
for (let a = 0; a < similarMatchesList.length; a++){
const current = similarMatchesList[a];
buttons.push(
new ButtonBuilder()
.setCustomId(`${buttonPrefix}${current.id}`)
.setLabel(`${current.name} [${current.id}]`)
.setStyle(ButtonStyle.Secondary)
);
}
}
if(similarMatchesList){
for (let a = 0; a < similarMatchesList.length; a++){
const current = similarMatchesList[a];
buttons.push(
new ButtonBuilder()
.setCustomId(`${buttonPrefix}${current.id}`)
.setLabel(`${current.name} [${current.id}]`)
.setStyle(ButtonStyle.Secondary)
);
}
}
I am able to pass the buttons into an ActionRowBuilder. Then send the ActionRowBuilder with the message correctly, and I am able to "collect" when the button is interacted with. But I am struggling with disabling the button (adding setDisabled(true) to it) that was just clicked since I don't have a variable associated with the button. I was hoping to disable the button using it's custom_id, but haven't been able to find a way to do that. I thought about assigning each button to a new variable as I go through the for loop, but haven't been able to think of a way of doing that. Thanks for the help in advance! Discord.js version: 14.14.1 Node version: 20.8.0
40 Replies
d.js toolkit
d.js toolkit5mo ago
- What's your exact discord.js npm list discord.js and node node -v version? - Not a discord.js issue? Check out #other-js-ts. - Consider reading #how-to-get-help to improve your question! - Explain what exactly your issue is. - Post the full error stack trace, not just the top part! - Show your code! - Issue solved? Press the button! - Marked as resolved by OP
Ashish
Ashish5mo ago
When do you disable the button? After you click it or before? Oh nevermind Anyway, you get a ButtonInteraction when a button is clicked. - You can use ButtonInteraction.message to get the message that was associated to the button that was clicked. - Now, you can all your action rows by using message.components which returns an array of action rows. - Loop over each action row and access the component property from ActionRow.components - Use the ButtonBuilder.from() method to convert each button into a builder - Find the button you want to disable by comparing the customId - Update the interaction with the new data
Toldi
Toldi5mo ago
Hi, thanks for your detailed reply! Stil a bit lost because when I grab onto the message.message.components of the ButtonInteraction I am only getting a single ActionRow, even though I initially sent more (I assume those get returned if I click on another button that is contained within another ActionRow?). But then if a button isn't clicked in those ActionRows, how would I go about disabling the remaining buttons? I might be going about this backwards... What I am trying to do is when people search for a unit from a game, there might be similar named matches that I want to return (as separate buttons, so the user can click on the button and get a new embed with new unit info). I need to be able to both disable the button that was clicked (sometimes multiple buttons) and then disable all the remaining buttons at some point so that people can't click it once the time (the one in ms) that I set runs out.
Toldi
Toldi5mo ago
A demo of how it looks like:
No description
Toldi
Toldi5mo ago
And then clicking one of the buttons should just return an embed with no further buttons (so don't need to send buttons again for that as well)
Ashish
Ashish5mo ago
If it's always a single action row, you can just access the first action row.... Otherwise use a loop
I assume those get returned if I click on another button that is contained within another ActionRow
No this is not the case, you'll receive all the action rows from the bot' msg
Toldi
Toldi5mo ago
Weird, lemme test it again. I only got 1 ActionRow back even though I sent the embed with 4 ActionRows
Ashish
Ashish5mo ago
Alright If it still persists, show me your code
Toldi
Toldi5mo ago
Sure, thanks for the help so far - it is very much appreciated 👍 Getting these buttons to work has eaten up the better part of two days now
Ashish
Ashish5mo ago
Haha no worries, happens to everyone once a while
Toldi
Toldi5mo ago
Ran it again for one of the commands that returns 25 buttons (in 5 Action Rows) and now I got 5 Action Rows returned with 5 buttons in each:
No description
Toldi
Toldi5mo ago
Thinking through your suggestion to use ButtonBuilder.from() - in this case from you mean the JS Array.from() method, right?
Ashish
Ashish5mo ago
Button Builder would be the class from djs
Toldi
Toldi5mo ago
So the point of using the ButtonBuilder.from() on each of the buttons is to recreate the buttons, but this time have setDisabled(true) so that they get disabled. And this is what I would do to wrap up once I get the 'end' message for the collector
Ashish
Ashish5mo ago
Yes exactly
Toldi
Toldi5mo ago
Will give it a shot, thanks once again for your help 👍
Ashish
Ashish5mo ago
Sure no problem
Toldi
Toldi5mo ago
Been trying to get this to work for the last three days, but haven't been able to quite figure it out. Currently stuck on this: (This is inside my collector that triggers on 'collect' - it will be moved to the collector that triggers on 'end', but since that emits/returns my original message and not the bot message with the embed and buttons, I will need to refactor it.)
var buttonArray = Array.from(message.message.components.ActionRow.components);
var arrayLength = buttonArray.length;
console.log('Array length: ' + arrayLength);
const buttons = [];
for (var i = 0; i < arrayLength; i++){
const current = buttonArray[i]
buttons.push(
new ButtonBuilder()
.setCustomId(`${i}`)
.setLabel(`${i}`)
.setStyle(ButtonStyle.Secondary),
.setDisabled(true)
);
}
// The buttonWrapper function takes an array of buttons and turns them into rows with the help of ActionRowBuilder
const buttonsArray = buttonWrapper(buttons);

response.edit({
components: buttonsArray
})
var buttonArray = Array.from(message.message.components.ActionRow.components);
var arrayLength = buttonArray.length;
console.log('Array length: ' + arrayLength);
const buttons = [];
for (var i = 0; i < arrayLength; i++){
const current = buttonArray[i]
buttons.push(
new ButtonBuilder()
.setCustomId(`${i}`)
.setLabel(`${i}`)
.setStyle(ButtonStyle.Secondary),
.setDisabled(true)
);
}
// The buttonWrapper function takes an array of buttons and turns them into rows with the help of ActionRowBuilder
const buttonsArray = buttonWrapper(buttons);

response.edit({
components: buttonsArray
})
I am stuck on correctly using the Array.from() and properly selecting the buttons from inside the ActionRow. Based on how nested the buttons are, I thought I was grabbing onto them with message.message.components.ActionRow.components, but I think I am wrong. Console.log-ing that usually gives undefined. Probably I am being blind to the obvious solution/rewrite
ْكِج
ْكِج5mo ago
Can't you just loop through message.components and then loop through each of them to change the disabled to true instead of doing all this hassle?
d.js docs
d.js docs5mo ago
Documentation suggestion for @Toldi:property Message#components An array of action rows in the message. (more...)
ْكِج
ْكِج5mo ago
Your first line seems to be invalid as components is an array of action rows and I don't really know why you still use var? but that's not the topic here.
Toldi
Toldi5mo ago
That's more of a bad habit I picked up after running into issues with const - will replace the var's with let's. Yeah, I realized I am not grabbing onto it correctly, trying to refactor my code now
ْكِج
ْكِج5mo ago
Good luck and let us know if you still face any issues
Toldi
Toldi5mo ago
Thanks, will do. It's just a bit too much with the array inside the array inside the array - russian doll style 🤣 even when I go to message.message.components I still have to go inside an array that contains arrays of ActionRows, with each ActionRow having an array of data and an array of components inside it Minor success in finally being able to disable all buttons in one go after a single button is clicked:
let arrayOfActionRows = message.message.components;
const buttons = [];
for (const actionRow of arrayOfActionRows){
const componentsRow = actionRow.components;
console.log(componentsRow[0].data);
for (let a = 0; a < componentsRow.length; a++){
const current = componentsRow[a];
buttons.push(
new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true)
);
}
let arrayOfActionRows = message.message.components;
const buttons = [];
for (const actionRow of arrayOfActionRows){
const componentsRow = actionRow.components;
console.log(componentsRow[0].data);
for (let a = 0; a < componentsRow.length; a++){
const current = componentsRow[a];
buttons.push(
new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true)
);
}
Now I'll try to add the setDisabled(true) to only the button that was actually clicked (probably through an if statement? not sure on that yet) and then try to run the last for loop once the 'end' is emitted (although that doesn't contain the bot button information, so will need to get creative for that) I am disabling only the clicked button with this:
for (const actionRow of arrayOfActionRows){
const ComponentsRow = actionRow.components;
for (let a = 0; a < ComponentsRow.length; a++){
const current = ComponentsRow[a];
if(messageCustomID === current.data.custom_id){
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true);
buttons.push(buttonBuilder);
} else {
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(false);
buttons.push(buttonBuilder)
};
}
for (const actionRow of arrayOfActionRows){
const ComponentsRow = actionRow.components;
for (let a = 0; a < ComponentsRow.length; a++){
const current = ComponentsRow[a];
if(messageCustomID === current.data.custom_id){
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true);
buttons.push(buttonBuilder);
} else {
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(false);
buttons.push(buttonBuilder)
};
}
Now I just need to figure out how to run the last for loop once the 'end' collector emits.
ْكِج
ْكِج5mo ago
I don't get what you mean but I assume you don't need help now am I right?
Toldi
Toldi5mo ago
Might need help with the 'end' collector. When you use createMessageComponentCollector(), you can set a time (for when the the bot should stop collecting). But the message that is emitted when that timer runs out is user original message and not the message that the bot sent
ْكِج
ْكِج5mo ago
You send the bot message in the same code right? so just use it show code
d.js docs
d.js docs5mo ago
property ButtonInteraction#message The message to which the component was attached
Toldi
Toldi5mo ago
This is what I have in the collector on 'end':
collector.on('end', (message) => {
let arrayOfActionRows = message.message.components;
const buttons = [];
for (const actionRow of arrayOfActionRows){
const componentsRow = actionRow.components;
console.log(componentsRow[0].data);
for (let a = 0; a < componentsRow.length; a++){
const current = componentsRow[a];
buttons.push(
new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true)
);
}
const buttonsArray = buttonWrapper(buttons);

response.edit({
components: buttonsArray
})
})
collector.on('end', (message) => {
let arrayOfActionRows = message.message.components;
const buttons = [];
for (const actionRow of arrayOfActionRows){
const componentsRow = actionRow.components;
console.log(componentsRow[0].data);
for (let a = 0; a < componentsRow.length; a++){
const current = componentsRow[a];
buttons.push(
new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true)
);
}
const buttonsArray = buttonWrapper(buttons);

response.edit({
components: buttonsArray
})
})
(the same code that worked when I was testing the on('collect')) Ah... the message that is returned on 'end' is a collection map with slightly different structure then the button interaction message
Toldi
Toldi5mo ago
No description
ْكِج
ْكِج5mo ago
So what's left is to edit the message no? ButtonInteraction#message#edit
Toldi
Toldi5mo ago
The edit is in there (code got truncated when I copy pasted), I am just not grabbing onto the components inside the collection map correctly - messing around with that now. The Collection(1) [Map] {...} is throwing me off from the message
Toldi
Toldi5mo ago
When I run console.log(message.map) I get back [Function: map], which led me here: https://discord.js.org/docs/packages/collection/1.5.1/Collection:Class But I am not sure it is relevant or how to make sense of it (been staring at this for too long in one sitting). Might leave it for the morning and tackle it again then
collection | Collection
A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has an ID, for significantly improved performance and ease-of-use.
Toldi
Toldi5mo ago
Btw, thanks for all your help so far. I really appreciate it 👍
ْكِج
ْكِج5mo ago
Show code
Toldi
Toldi5mo ago
Just running this (while I make sense of map and collectors):
collector.on('end', (message) => {
console.log(message);
})
collector.on('end', (message) => {
console.log(message);
})
And the message is what returns the Collection(1) [Map] { 'messageId' => ButtonInteraction { ... }. I saw in the documentation that I am supposed to map over it, but it is just not clicking to me what I should be mapping over to get to the components (with the ActionRows with the buttons)
No description
ْكِج
ْكِج5mo ago
why do you always define the button interaction as message, that will just confuse you even more The map is what returned from the collector as a result to your filter applied to it. so if you do console.log(message.first()) you should get the first button clicked.
Toldi
Toldi5mo ago
Ah, that's because I use both slash commands (where you would get an interaction) and message listeners (so if a user started a message with a certain prefix then the bot listens to that and runs will try that, thanks 👍 This really helped, thanks once again! When I was reading the documentation I guess the words just weren't making sense to me and I kept looking it over without getting anywhere. I grabbed onto the interaction with:
const arrayOfActionRows = message.first().message.components;
const arrayOfActionRows = message.first().message.components;
and then I was able to run my previous code that went through the buttons and disabled them one by one once the timer ran out. Now I am just refactoring this:
let arrayOfActionRows = message.message.components;
const buttons = [];
for (const actionRow of arrayOfActionRows){
const ComponentsRow = actionRow.components;
for (let a = 0; a < ComponentsRow.length; a++){
const current = ComponentsRow[a];
if(messageCustomID === current.data.custom_id){
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true);
buttons.push(buttonBuilder);
} else {
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(false);
buttons.push(buttonBuilder)
};
}
}
const buttonsArray = buttonWrapper(buttons);

response.edit({
components: buttonsArray
})
let arrayOfActionRows = message.message.components;
const buttons = [];
for (const actionRow of arrayOfActionRows){
const ComponentsRow = actionRow.components;
for (let a = 0; a < ComponentsRow.length; a++){
const current = ComponentsRow[a];
if(messageCustomID === current.data.custom_id){
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(true);
buttons.push(buttonBuilder);
} else {
const buttonBuilder = new ButtonBuilder()
.setCustomId(`${current.data.custom_id}`)
.setLabel(`${current.data.label}`)
.setStyle(ButtonStyle.Secondary)
.setDisabled(false);
buttons.push(buttonBuilder)
};
}
}
const buttonsArray = buttonWrapper(buttons);

response.edit({
components: buttonsArray
})
Since the way I have it now, when I click the first button, it gets disabled correctly, but if I click a second button, I inadvertently reenable the first clicked button (and disable the second button that was clicked) Thinking about storing the custom_id of the clicked buttons in an array and that matches, then disabling those buttons as well
ْكِج
ْكِج5mo ago
No need to store any data, just filter out the disabled buttons current.data.disabled iirc
Toldi
Toldi5mo ago
Stepped away from the laptop for 2 hours and thought of the same thing, thanks for all the help 👍 looks like it is working as it should. Adding the 'OR' to check if it was disabled, and if it was, then keep it disabled as I loop over it.
if(messageCustomID === current.data.custom_id || current.data.disabled){
if(messageCustomID === current.data.custom_id || current.data.disabled){