Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Help with CRP buttons in repeating section - "No ability was found for X"

Evening all,  Back again with yet more misunderstandings i'm sure. In my custom character sheet I have a repeating section called "repeating_attacks", inside of which is an action button named "act_attackcheck" for triggering a custom roll. I've succesfully got a Custom Roll Parsing button and sheetworker setup on some static (read. non repeating) buttons but i'm falling down on the buttons inside a repeating section. When I click my buttons roll20 throws an error to the chat:          "No ability was found for %{Test|repeating_attacks_attackcheck}" I can surmise that my issue is something to do with my naming of my repeating button but I can't quite spot what. Any assitance appreciated. (Code below cut down to just the relevant sections) html: < fieldset class = "repeating_attacks f-col" >   < div class = "f-col" >   < h4 > Roll </ h4 >     < button type = "roll" name = "roll_attack" class = "text-btn" value = "@{attackcheckattrib}" ></ button >     <!-- Hidden attribute & buttons for attack action buttons-->     < input type = "hidden" name = "attr_attackcheckattrib" value = "%{Name|repeating_attacks_rowid_attackcheck}" >     < button type = "action" name = "act_attackcheck" class = "hidden" ></ button >     <!-- Hidden attribute & buttons for attack action buttons-->   </ div > </ fieldset > sheetworkers:   //When character name changes or sheet opens, update attack roll buttons with new name.   on ( "change:character_name sheet:opened" , function () {     //Get id's for each existant attack     getSectionIDs ( "attacks" , function ( ids ) {       for ( var i = 0 ; i < ids . length ; i ++) {         let rowid = ids [ i ];         let attackcheckname = "repeating_attacks_" + rowid + "_attackcheck" ;         let attackcheckattrib = "repeating_attacks_" + rowid + "_attackcheckattrib" ;         getAttrs ([ "character_name" ], function ( charname ) {           let revision_attack_value = "%{" + charname . character_name + "|" + attackcheckname + "}" ;           setAttrs ({             [attackcheckattrib]: revision_attack_value           });         });       }     });   });   on ( "clicked:repeating_attacks:attackcheck" , function () {     getAttrs ([ "attackname" , "attackdamage" , "attackspeed" , "attacktype" , "attackrange" , "attackap" , "attacktags" ], function ( attackdetails ) {       console . log ( attackdetails )     });   });
1700764993

Edited 1700765027
If it's at all illuminating this is from the html inspector on the live sheet:
1700766494

Edited 1700766855
GiGs
Pro
Sheet Author
API Scripter
I've never included more than one class in the initial fieldset name. You can probably do this, but you have a div inside the fieldset so you can probably avoid it. Im talking about < fieldset class = "repeating_attacks f-col" > which I'd have just made < fieldset class = "repeating_attacks" > Your naming scheme looks okay to me, but I'm wondering about the sheet Name defined here:  <input type="hidden" name="attr_attackcheckattrib" value="%{Name|repeating_attacks_rowid_attackcheck}"> Is there a global attribute on the sheet called Name that contains the sheetname? Also where is rowid coming from? Is that defined attribute name? PS: that first sheet worker is raising more questions the longer I look at it. But lets answer these questions first.
1700766596
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
so, this is one of the weird bits of R20 code that is a gotcha sometimes. Adding additional classes to a fieldset beyond the name of the repeating section will break action buttons in the repeating section. Try removing the fcol class from your fieldset.
1700766733

Edited 1700766800
GiGs
Pro
Sheet Author
API Scripter
PS, this part getSectionIDs("attacks", function(ids) { you should always include the repeating_ part. I know that the official documentation says its okay not to, but this is incorrect. Every now and then (not commonly, just sometimes), there'll be an error, so that should be getSectionIDs("repeating_attacks", function(ids) {
1700767274

Edited 1700767930
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
GiGs said: PS: that first sheet worker is raising more questions the longer I look at it. But lets answer these questions first. It looks to be based on the draggable action button workaround, but you're right there are some issues with it although they aren't related to this specific problem. Sam, what probably got GiGs attention is that you have a getAttrs and a setAttrs inside a loop. This is almost never what you want to do as it causes excessive network calls and can actually slow down your sheet (especially when it's in something that fires at sheet:opened).  I'd recommend changing the character name propagation sheetworker to this: //When character name changes or sheet opens, update attack roll buttons with new name. // You'll also want to trigger this on some or all attributes in the repeating section so that it sets the button correctly for new rows. on("change:character_name sheet:opened", function () { //Get id's for each existant attack getSectionIDs("repeating_attacks", function (ids) { // switched to using object destructuring function syntax to just extract the character_name from the getAttrs values getAttrs(["character_name"], function ({character_name}) { // store all the changes in an object const setObj = ids.reduce((memo, rowid) => { // also changed to use template literals for better readability let attackcheckname = `repeating_attacks_${rowid}_attackcheck`; let attackcheckattrib = `repeating_attacks_${rowid}_attackcheckattrib`; memo[attackcheckattrib] = `%{${character_name}|${attackcheckname}}` return memo; }, {}); // apply all changes at once setAttrs(setObj); }); }); }); And seconding GiGs on the "use full repeating section name in getSectionIDs".
Thanks both,  In terms of resolving my immediate issue, GiGs your hunch and Scott's assertion were correct. Removing the additional flexbox class from the fieldset immediately made the buttons start working. Re the rest, unfortunately you've had a peak behind the curtain of someone that doesn't really understand what they're playing with, just enough to get me where i'm going... To answer some questions:  <input type="hidden" name="attr_attackcheckattrib" value="%{Name|repeating_attacks_rowid_attackcheck}"> The value here is nothing more than a placeholder when the repeating field is generated, a reminder for me of the correct formatting of the final value and it gets overwritten immediately once the sheetworker runs. Whether "Name" could cause issues somewhere... i'm honestly not certain, perhaps best for me to remove but no I don't have any global variables that relate to it. Scott your reworked sheetworker makes some sense to me and there are some insights into dealing with javascript objects I can glean for other places in my script but by and large, I can see what it's doing but I don't understand how it's doing it... so perhaps i'll spell out how I read it and maybe i'll have learnt something by the end. We've got the output of getSectionIDs in an array called "ids". We've also got the "character_name" value extracted directly from the getAttrs function which is now available as a variable "character_name". So far so different to what I had written originally. Next is where I fall off, you've initialised a new object called setObj which is equal to a method (reduce) invoked on the array "ids", with values "memo" & "rowid". So setObj will be the result of that method invocation. As a note before I go further, I don't see you actually initialise a variable called rowid, is this an intrinsic property of getSectionIDs I was unaware of? From what I can google, the reduce method executes a function on an array to return to a single value. From the syntax "memo" appears arbitray, it's just what you are calling the output object? Either way, i'm feeling like "memo" could be anything so long as the subsequent codeblock returns the same name. The codeblock itself makes a certain amount of sense to me... "memo[attackcheckattrib] = X" is setting a property(key) on the "memo" object named "attackcheckattrib" and that property(key) has a value equal to the defined string. Then you return the memo object to the calling function. I assume, that as a result of reduce the whole of the script block is run over each object in the array similar to a foreach loop and the memo objects are aggregated into an array, or perhaps a single object with multiple key pairs.
1700778189
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Sam S. said: Scott your reworked sheetworker makes some sense to me and there are some insights into dealing with javascript objects I can glean for other places in my script but by and large, I can see what it's doing but I don't understand how it's doing it... so perhaps i'll spell out how I read it and maybe i'll have learnt something by the end. We've got the output of getSectionIDs in an array called "ids". We've also got the "character_name" value extracted directly from the getAttrs function which is now available as a variable "character_name". So far so different to what I had written originally. Yes, this change is mostly just syntactic sugar so that we don't need to do charname.character_name like you had in your script. There's no actual functional difference between the two though. Next is where I fall off, you've initialised a new object called setObj which is equal to a method (reduce) invoked on the array "ids", with values "memo" & "rowid". So setObj will be the result of that method invocation. As a note before I go further, I don't see you actually initialise a variable called rowid, is this an intrinsic property of getSectionIDs I was unaware of? From what I can google, the reduce method executes a function on an array to return to a single value. From the syntax "memo" appears arbitray, it's just what you are calling the output object? Either way, i'm feeling like "memo" could be anything so long as the subsequent codeblock returns the same name. The codeblock itself makes a certain amount of sense to me... "memo[attackcheckattrib] = X" is setting a property(key) on the "memo" object named "attackcheckattrib" and that property(key) has a value equal to the defined string. Then you return the memo object to the calling function. I assume, that as a result of reduce the whole of the script block is run over each object in the array similar to a foreach loop and the memo objects are aggregated into an array, or perhaps a single object with multiple key pairs. So, I think there are a couple parts here that I didn't explain adequately. Inside the reduce method, we're just defining a callback function similar to what you've done getAttrs and getSectionIDs. The (memo,rowid) => {...} is a function syntax known as arrow functions. These are just a short way to define a new function. There are some functional differences between arrow functions and classic functions, but nothing that affects our current implementations Just like with other functions, we can define the arguments of an arrow function to be whatever we want it to be. I personally like descriptive argument names, but we just as easily could have done (a,b) => {...}, as long as we use those arguments correctly. In this case, because this is a callback function to the reduce method, the arguments in the callback function get specific values. A reduce method call has several parts: ids.reduce : This just says what array we're going to iterate over for the reduce operation reduce((memo,rowid) => {...},{}) : In the actual reduce call, we define our callback function as the first argument we pass to the reduce method and define the starting value of memo (an empty object in this case) as the second argument that is passed to the reduce memo (you nearly always want to define a starting value). {...} : The contents of the callback function are where we do the work and logic of how to transform the array. The contents of the function will be run for each index of the array (each section ID in this case), and the return value of the callback function will be the memo for the next iteration. An equivalent way to write the use of the reduce method would be with a forEach or a for loop. That would look like: const setObj = {}; ids.forEach(function(rowid){ let attackcheckname = `repeating_attacks_${rowid}_attackcheck`; let attackcheckattrib = `repeating_attacks_${rowid}_attackcheckattrib`; setObj[attackcheckattrib] = `%{${character_name}|${attackcheckname}}`; }); So, if we had a character named "Tester" and an ids array of ['-12j5', '-i89s', '-o906ty'], we'd wind up with a setObj that looks like this at the end of either set of code: { 'repeating_attacks_-12j5_attackcheckattrib': '%{tester|repeating_attacks_-12j5_attackcheck}', 'repeating_attacks_-i89s_attackcheckattrib': '%{tester|repeating_attacks_-i89s_attackcheck}', 'repeating_attacks_-o906ty_attackcheckattrib': '%{tester|repeating_attacks_-o906ty_attackcheck}', }
Many thanks Scott,  I've seen the reduce method being used in a couple places throughout various forum posts but this explanation has helped solidify it a bit for me. Glad to read that my intuited understanding wasn't too far off the mark.