- Anatomy of a Sheet Worker
- Events, and watching Attributes
- Variables – How to Name Things
- Arithmetic in Sheet Workers
- What If? in Sheet Workers
- JavaScript Objects
- Getting Loopy With JavaScript
- Logging in the Browser Console
- Strings, Arrays, and Loops
- Asynchronicity and Things to Avoid With Loops
- Changes and the eventInfo Object
- Action Buttons
- setAttrs and Saving Attributes
- Castle Falkenstein Design – Sheet Workers
- The Perils of Sheet Worker Functions
- The Script Block and Identifying Characters
- Arrays and Dropdowns
- Undefined and Other Error Values
- The Ternary Operator – The One-Line If
- Template Literals
- Functions and the Fat Arrow
- Strings in Sheet Workers
- A Sheet Worker Reprise
Loops are almost as fundamental as if statements, and just like those, the basic concept is self-explanatory. What if you want to repeat the same process over and over, and you don’t know ahead of time how many repeats you want to do?
Coding this simple concept is often anything but straightforward, however. In this post, I’ll describe two common kinds of loops and show you can code things you probably never imagined you could (I know it was a mind-blowing moment for me).
For Loops and Visible Boxes
Bear with me, this will take some explaining.
Let’s imagine you want a sheet that tracks how many spells per level you get, and lets you write details about each. Say you can have up to 5 spells at each level. Your level 1 spell section might look like this:
<h3>Level 1 Spells</h3>
<div>
<span>Name:</span>
<input type="text" name="attr_spell_name_1_1" text="" title="Spell Name">
<span>Description:</span>
<input type="text" name="attr_spell_desc_1_1" text="" title="Spell Description">
</div>
<div>
<input type="text" name="attr_spell_name_1_2" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_2" text="" title="Spell Description">
</div>
<div>
<input type="text" name="attr_spell_name_1_3" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_3" text="" title="Spell Description">
</div>
<div>
<input type="text" name="attr_spell_name_1_4" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_4" text="" title="Spell Description">
</div>
<div>
<input type="text" name="attr_spell_name_1_5" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_5" text="" title="Spell Description">
</div>
Code language: HTML, XML (xml)
This lets the player enter a name and description for each spell. In reality, there’d be more details but for this example, this will do. Each div separates the details from each spell. As you’ll see soon, that is very useful.
With minimal additions and styling, this would look like this:
(It would be a good idea to include the number of the spell, not to mention other spell details. But this is a barebones example.)
A drawback here is that all 5 spells are always shown, but the PC might have 3, 2, or even no spells. So, the sheet author has the bright idea to have an attribute which tracks the number of spells at each level a character can have.
<h3>Spells Per level</h3>
<span>Level 1</span><input type="number" name="attr_spells_at_level_1" value="0">
<span>Level 2</span><input type="number" name="attr_spells_at_level_2" value="0">
<!-- and so on, one input for each level -->
Code language: HTML, XML (xml)
The input can have a value of 0 to 5. It might be set manually by the player, or be assigned by another stat (the class and level of a character, for example). But for this example, let’s assume the player enters these numbers manually.
In Roll20, many solutions use all of HTML, CSS, and sheet workers. This is especially true when you start working with sheet workers. The method the GM chooses here may seem a little convoluted, but it’s a standard Roll20 method.
First, expand the HTML. Notice how each diiv now has a class, and before each, there is a hidden input?
<h3>Level 1 Spells</h3>
<input type="hidden" value="0" class="spells-toggle" name="attr_spells_hide_1_1">
<div class="hide-spells">
<input type="text" name="attr_spell_name_1_1" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_1" text="" title="Spell Description">
</div>
<input type="hidden" value="0" class="spells-toggle" name="attr_spells_hide_1_2">
<div class="hide-spells">
<input type="text" name="attr_spell_name_1_2" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_2" text="" title="Spell Description">
</div>
<input type="hidden" value="0" class="spells-toggle" name="attr_spells_hide_1_3">
<div class="hide-spells">
<input type="text" name="attr_spell_name_1_3" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_3" text="" title="Spell Description">
</div>
<input type="hidden" value="0" class="spells-toggle" name="attr_spells_hide_1_4">
<div class="hide-spells">
<input type="text" name="attr_spell_name_1_4" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_4" text="" title="Spell Description">
</div>
<input type="hidden" value="0" class="spells-toggle" name="attr_spells_hide_1_5">
<div class="hide-spells">
<input type="text" name="attr_spell_name_1_5" text="" title="Spell Name">
<input type="text" name="attr_spell_desc_1_5" text="" title="Spell Description">
</div>
Code language: HTML, XML (xml)
Now we create some CSS. The + selector here says that only the following element is affected, and it is hidden (display:none) if the value of the input is set to 0.
.charsheet input[type="hidden].spells-toggle[value="0"] + div.hide-spells {
display: none;
}
Code language: CSS (css)
So, using if the hidden inputs are set to 0, the div will be hidden. Now we need a sheet worker that sets those inputs to 0 or 1 (it could be anything but we choose 1) based on how many spells a character has.
Here’s a sheet worker that does this. This sheet worker has a lot of clever code in it that we’ll describe later in more detail.
on('change:spells_at_level_1', function() {
getAttrs(['spells_at_level_1'], function(values) {
const how_many = +values.spells_at_level_1 || 0;
const output = {};
for(let num = 1; i <= 5; i++) {
if(num <= how_many) {
output['spells_hide_1_' + num] = 1;
} else {
output['spells_hide_1_' + num] = 0;
}
}
setAttrs(output);
});
});
Code language: JavaScript (javascript)
The first 3 lines mean the sheet worker runs whenever the spells_at_level_1 attribute changes, and the current value of that attribute is stored in the how_many variable.
In the last post, we learned how to create empty Objects, and how setAttrs expects an object. We can use that to create a basic object that will be expanded during the loop, then save the stats it creates.
const output = {};
/* code omitted for clarity */
setAttrs(output);
});
});
Code language: JavaScript (javascript)
Now we can finally talk about the actual For Loop. The basic structure of a For Loop looks like this:
for(let counter = start; counter <= maximum; counter++) {
}
Code language: JavaScript (javascript)
First, create a variable as the counter and set its initial value (usually 0 or 1). Then set the maximum value of the counter. And finally, set a rule for how the counter increments. ++ means it adds 1 for each loop.
Inside these lines, you describe what happens on every loop. So our code looks like this:
for(let num = 1; i <= 5; i++) {
if(num <= how_many) {
output['spells_hide_1_' + num] = 1;
} else {
output['spells_hide_1_' + num] = 0;
}
}
Code language: JavaScript (javascript)
We create a counter and name it num. It starts at 1 and stops at 5, so it will loop 5 times. We chose 5 because there are five possible spells.
Now we ask what if the counter (num) is less than or equal to how_many spells this character has. If so, set that spell’s hidden toggle attribute to 1, else set it to 0. Remember, output will hold all the attributes we plan to save during setAttrs.
So, the process can be thought of as this:
- Build the HTML to include an attribute that will be triggered, and a toggle attribute for each div.
- In the loop, check toggle attributes to 1 or 0
- In the CSS, hide attributes whose attribute is 0
You need the HTML, CSS, and sheet worker to all work together to make this work. There are simpler ways to use loops, of course, but this gets across the potential.
Other Loops
This just scratches the surface of what is possible (and the syntax can be simpler). Here are some other loops you might encounter.
The standard loop is described here. Other methods are often ore advanced or efficient, but this gets the job done and is a perfectly fine approach.
This is probably the most common kind of loop after for loops. If works with arrays, and will loop through the entire array, one item at a time.
const stats = ['str', 'dex', 'wis'];
stats.forEach(myFunction(this_stat) {
/* do something with this_stat */
}
Code language: PHP (php)
You can see an extravagant Roll20 use of forEach at Universal Sheet Workers.
A simpler Fat Arrow syntax eliminates the function name and looks like this:
const stats = ['str', 'dex', 'wis'];
stats.forEach(this_stat => {
/* do something with this_stat */
}
Code language: JavaScript (javascript)
In a standard for loop, you create a counter and run declare how many times the loop runs.
With a for(of) loop, you don’t use a counter. Instead, you look at an array or object, and the code automatically counts through the correct number of elements, assigning a name to the current one.
const stats = ['str', 'dex', 'wis'];
for (const this_stat of stats) {
// do something with this_stat
}
Code language: JavaScript (javascript)
There are also for…in loops, if you want to look them up.
If you’re interested in loops, you might find it useful to also look up:
- Do Loop
- While Loop
- For…in (this is subtly different from For…of loops!)
- Map
- Filter
- Reduce
You don’t need to know any of these, though some will be covered in other posts.
Now watch out for the post on Asynchronicity – there, we’ll cover something you absolutely should not do with a loop, and why.