The previous series has a lot of deep-dive posts, looking at a particular topic in great detail. But there are some things that you need to know that aren’t worth a full post. Here we collect them all.
Attribute Names
When naming an attribute, you can use whatever you want as a name. But it’s still a good idea to choose names that work as JavaScript variables. So, being with a letter, can include letters, numbers, and underscores.
However, repeating sections have a couple of extra concerns:
You can use the same name in multipler repeating sections and globally. But you shouldn’t use any name that matches a global attribute name. So if you have an attribute named, say, strength, don’t name something inside repeating_buffs also strength.
This is because sheet workers trigger on the global name and the repeating attribute name. If you have on(‘change:strength’, it would trigger on both the global attribute change, and any repeating attribute which is also named strength. This is ineffecient and might have unforeseeen consequences.
You can use the same name in multiple repeating sections, because on(‘change:repeating_buffs:strength’ will only trigger when that one attribute changes.
- avoid using repeating_spells_ID_spell_name when you can use repeating_spells_ID_name
- avoid using gloal names inside repeating sections, change:repeating_section – can be a problem. You prtobably dont want it to fire at every change
Position CSS
This is a fairly obscure and pretty rare problem, but it’s definitely a bug.
If you use the CSS position property while inside a repeating section, that element will be bumped out of the repeating section and events will not trigger off it.
This might only affect action buttons, but it’s something to be careful of.
sheet:opened and row ID
The sheet:opened event can be useful, but often isn’t needed. But one mistake people often make is to combine sheet:opened with shortened repeating section syntax. Never start a sheet worker like this:
on('change:repeating_example:mystery_row sheet:opened', () => {
getAttrs(['repeating_example_mystery_row'], values => {
Code language: JavaScript (javascript)
When this sheet worker is triggered, it must be triggered from within a repeating section, so Roll20 knows which row the trigger came from and can apply it to the getAttrs line. But when triggered on sheet:opened, no row is selected, so no row can be assigned, and this worker fails.
You should never combine the shorted repeating section syntax and sheet:opened. You can comine sheet:opened with workers that use getSectionIDs because that goes through every row – no row needs to be selected.
eventInfo
It’s not obvious to me why this is, but the contents of eventInfo are different between repeating sections and global attributes, and between action buttons and all other triggers (roll buttons and attributes).
If you are tryig to find the attribute’s trigger, in the old days sourceAttribute was the property to check for, but now you should always check for triggerName.
Asynchronous Attributes
It’s been mentioned already but it’s worth repeating. Where possible, make sure your workers contain no more than one of each asynchronous function – getAttrs, setAttrs, and getSectionIDs.
This is because each one requires a separate call to Roll20 servers and your code hangs until that completes. If your sheet contains a lot of these, or the Roll20’s servers are overloaded, your sheet will hang, ruining the user experience.
Two ways people do this are putting getSectionIDs inside getAttrs (and looping them), or calling a function that contains getAttrs and setAttrs. I’ll give examples of both.
The second of the problems here isn’t strictly related to repeating sections, but it’s listed here because of the first.
Looping in getSectionIDs
Lets say you have a repeating section, gear, which has a bunch of equipment, and you want to count up their weights. Since getSectionIDs gives an aray of row IDs, it’s pretty logical (and wrong!) to think of this approach:
on('change:gear:weight, ()=> {
getSectionIDs('repeating_gear', id_array => {
let weight_total = 0;
id_array.forEach(id => {
getAttrs([`repeating_gear_${id}_weight`], values => {
const weight = +values[`repeating_gear_${id}_weight`] || 0;
weight_total += weight;
});
});
setAttrs({
weight_total: weight_total
});
});
});
Code language: JavaScript (javascript)
That looks like the sensible approach. You loop through every row of the repeating section. But you call getAttrs for every row of the section. That is slow and inefficient.
The proper way to do this is actually a lot less straightforward. getAttrs accepts an array of attribute names – you can do getAttrs with as many names at once as you like – even hundreds of attributes. This is faster than even two separate getAttrs calls! So you can do this:
on('change:gear:weight, ()=> {
getSectionIDs('repeating_gear', id_array => {
const fields = [];
id_array.forEach(id => {
fields.push(`repeating_gear_${id}_weight`);
});
getAttrs(fields, values => {
let weight_total = 0;
fields.foreach(field => {
const weight = +values[field] || 0;
weight_total += weight;
});
});
setAttrs({
weight_total: weight_total
});
});
});
Code language: JavaScript (javascript)
This is very similar to the first. But you first loop through the getSectionIDs array to get a full list of all the attribute names, do getAttrs on that, and then do a second loop on the attributes found.
It’s the same process, but a lot more efficient.
Functions in Workers
Lets say you have a function that calculates your stat modifiers, and it works the same way for each attribute, so you might have something like this:
const calc_modifier = (stat, modifier = 0) => {
getAttrs([stat], values => {
const score = (+values.stat || 0) + modifier;
const modifier = Math.floor(score/2) -5 ;
setAttrs({
[stat]: score,
[`{stat}_modifier`]: modifier
});
});
});
on('change:str', () => {
calc_modifier('str');
});
on('change:dex', () => {
calc_modifier('dex');
});
on('change:con', () => {
calc_modifier('con');
});
Code language: JavaScript (javascript)
Here we have a function that calculates a stat modifier, Supply it the name of a stat, and it grabs that value and creates a modifier. You can even give it an optional modifier (calc_modifier(‘str’, 3)) and it will calculate the new score.
On the face of it, this looks okay. The problem is how easy it enables grroup operations. Lets say you have an action button that buffs all attributes by one, and another which reduces them by 1. That could look like:
on('clicked:buff', () => {
calc_modifier('str', 1);
calc_modifier('dex', 1);
calc_modifier('con', 1);
});
Code language: JavaScript (javascript)
This performs the desired operation, but now you have a sheet worker that calls getAttrs and setAttrs 3 times each – more if there are more stats. This is bad.
The way to handle this kind of thing is to have your stat workers grab the attribute values and do the setAttrs in their root function, and have the calc_modifier return a value. That might look like this:
const calc_modifier = score => {
const modifier = Math.floor(score/2) -5 ;
return modifier;
});
on('change:str', () => {
getAttrs(['str'], values => {
const mod = calc_modifier(+values.str || 0);
setAttrs({
str_mod: mod
});
})
});
// repeat for each stat
Code language: JavaScript (javascript)
This is a lot more laborious. There are ways to streamline it (see Universal Sheet Workers), but the basic idea is that the getAttrs and setAttrs are in the original sheet worker, and functions are only used to calculate and return values. This way you can avoid accidentally including multiple asynchronous operations in the same worker.
setSectionOrder
There is a function called setSectionOrder which is meant to let you reorder the rows of a repeating section. Unfortunately, it’s buggy and should not be used. You’ll have to rely on players manually reording sections (that way, they get the order they want) or maybe writing your own reordering function.
See the warning from the above link.
Copies of a Repeating Section
You can place multiple copies of a repeating section in your sheet, just as you can with an attribute.
Each attribute inside the repeating section must also be created. But this has several advantages: you can show some attributes and not others, you can show different styles (attributes in one place might be readonly, and only editable in another place).
You could create a repeating section to showcase a character’s special powers, and show only the power names and descriptions in one place, and in another place show the names, levels, and full combat information.
This possibly needs a full post with a detailed example and description of the gotchas.