The getSectionObject Custom Function

This post is pretty heavy, and if you want to skip all that, jump to Custom Function heading.

The reason handling multile repeating sections is hard is because sections are asynchronous. A recent improvement to JavaScript included support for async functions. If we had that in Roll20 character sheet code (we don’t), we could write a sheet worker like this:

on('change:repeating_one:something', (eventInfo) => {
   getSectionIDs('repeating_one', async ids);
   const fields = [];
   ids.forEach(id => fields.push(section_name('one', id, 'something')));
   getAttrs(fields, async values());
   let something = values.something;
   //do something with something, transforming its value
   setAttrs({
      something:something
   });   
});Code language: JavaScript (javascript)

Notice the almost complete lack of nesting, Each function with async halts the code – it cannot procede past that point until that function is complete, and saves the calculation in way that can be used in the rest of the worker.

To be clear: this ability doesn’t exist (yet). But if it did, writing workers would be a lot simpler. This is what a worker written now looks like:

on('change:repeating_one:something', eventInfo => {
   getSectionIDs('repeating_one', ids => {
      const fields = [];
      ids.forEach(id => fields.push(section_name('one', id, 'something')));
      getAttrs(fields, values() => {
         let something = values.something;
         //do something with something, transforming its value
         setAttrs({
            something:something
         });   
      });   
   });   
});Code language: JavaScript (javascript)

Hidden Code

What should be clear now is that the functions getAttrs, getSectionIDs, setAttrs, and yes, even on, are each functions which each hide code we can’t see. Those functions do things and we don’t see the code for that.

All these functions know which character to operate on, getAttrs has hidden code to scan a sheet and extract their attribute values, and so on. These are functions most of which return a value, and that value is a variable we name – by convention eventInfo, ids, and values. These values are callbacks which we use in our own code, and so these are callback functions.

While tricky, we can make our own callback functions that work in the same way. There’s at least one such on the roll20 wiki (getSectionIDsOrdered), and we will make one in this post.

Custom Function For Multiple Sections

This code should be dropped into your script block near the start, along with any other custom functions. It looks mysterious now, but I’ll explain it step by step below.

const getSectionObject = (sections, callback) => {
    const fields_object = {};
    fields_object.fields = [];
    let keys = Object.keys(sections);
    const burndown = () => {
        let section = keys.shift();
        getSectionIDs(`repeating_${section}`, ids => {
            ids.forEach(id => 
               sections[section].forEach(field => 
                  fields_object.fields.push(`repeating_${section}_${id}_${field}`)));
            fields_object[section] = ids;
            if(keys.length) {
                burndown();
            } else {
                callback(fields_object);
            }
        });
    };
    burndown();
};Code language: JavaScript (javascript)

Note that there are fancier ways to write this function, but I want to keep it somewhat explainable!

How To Use The Function

We’ll see how to use the function now, and explain it afterwards. You don’t really need to understand it to use it.

We first set up an event line (the on function) as with any sheet worker.

Then for the first change: we create an object variable to hold details of the repeating sections we are interested in. This is the only real difference from a typical sheet worker.

In this object, create one key for each repeating section (weapons, gear), and its values are an array containing the field names we want to use (here they are both the same [‘weight’, ‘cost’] but they don’t have to be).

We then use getSectionObject instead of getSectionIDs. It looks exactly the same.

on('change:repeating_weapons:cost change:repeating_weapons:weight change:repeating_gear:cost change:repeating_gear:weight remove:repeating_weapons remove:repeating_gear', () => {
   const my_sections = {
      weapons: ['weight', 'cost'],
      gear: ['weight', 'cost']
   };
   getSectionObject(my_sections, data => {
       
   });
});Code language: JavaScript (javascript)

With getSectionIDs, you get an ids variable which is an array of all the row ids of that one section.

With getSectionObject, you get a data variable which contains several pieces of data. It contains a fields key which is an array of all the full attribute names from all sections defined, then you have one key for each section which contains an array of all the row ids for that section, as you would with getSectionIDs for one section.

So with the above worker you’d get an object like this:

data = {
   fields: [/* array of all full attribute names */];
   weapons: [/* an array of row ids, just as if you had done getSectionIDs('repeating_weapons', */];
   gear: [/* an array of row ids, just as if you had done getSectionIDs('repeating_gear', */];
};Code language: JavaScript (javascript)

If you understand object variables (and if working in JavaScript, you really should), the rest is easy.

The sheet worker looks a lot like the one from the last post, with tiny modifications to account for this data variable.

getAttrs(data.fields, values => {
   const output = {};
   output.weight = 0;
   output.cost = 0;
   data.weapons.forEach(id => {
      output.weight += num(v[section_name('weapons', id, 'weight')]);
      output.cost += num(v[section_name('weapons', id, 'cost')]);
   });
   data.gear.forEach(id => {
      output.weight += num(v[section_name('gear', id, 'weight')]);
      output.cost += num(v[section_name('gear', id, 'cost')]);
   });
   setAttrs(output);
});Code language: JavaScript (javascript)

Compare this part of the sheet worker to the last post. The only differences are using data.fields instead of fields and data.weapons and data.gear instead of ids_weapons and ids_gear. That data object contains everything you need from as many repeating sections as you want to use.

How getSectionObject Works

Buckle up, things are going to get even more complicated. Bear in mind, you don’t need to understand this to use the function. You can just drop in the function and use it. If you can use getSectionIDs, you can use getSectionObject.

const getSectionObject = (sections, callback) => {Code language: JavaScript (javascript)

On the first line we set up how the functioon is called. sections here is the object variable that holds the repeating sections. In the function, we’ll work our way through it, section by section, to get the details we need.

callback is a variable created by the function itself, and will be passed to the user when the function ends.

    const fields_object = {};
    fields_object.fields = [];
    let keys = Object.keys(sections);Code language: JavaScript (javascript)

Here we initialise values the rest of the function will need. Object.keys is a function that creates an array. So here we create an array of all the repeating section names we are interested in. It is created with let rather than const, because after each section is checked, it will be discarded.

    const burndown = () => {
        /* skipped a lot of stuff */
    };
    burndown();Code language: JavaScript (javascript)

This is a very important part of the structure. The first line const burndown creates a function, but that function doesn’t actually do anything yet. All the code inside is dormant until it is run.

Then in the if statement, we run the function.

        let section = keys.shift();
        /* some code skipped */
        if(keys.length) {
            burndown();
        } else {
            callback(fields_object);
        }Code language: JavaScript (javascript)

Inside the function is this structure. It starts by grabbing the function name. shift grabs the first item in an array and removes it from that array. So the size of the keys array drops by 1,

Then the if statement checks the size of the keys array. If there any keys left, the burndown function is run again. In this way, it will run once for each repeating section.

If there are no keys left, it runs callback – this is what ends the function, and passes the found data to the user. We’ll talk a little bit more about that later.

        getSectionIDs(`repeating_${section}`, ids => {
            ids.forEach(id => 
               sections[section].forEach(field => 
                  fields_object.fields.push(`repeating_${section}_${id}_${field}`)));
            fields_object[section] = ids;Code language: JavaScript (javascript)

This is the last part of the function. Remember that section is the name of the current repeating section (grabbed with the shift command). The getSectionIDs function is used to get all row ids of that repeating section. We loop through them and add full section names to the fields variable, and finish off by creating a new key for the section with all of its ids.

Notice how it is populating a fields_object variable?

So, putting it all together, here is the complete function again.

const getSectionObject = (sections, callback) => {
    const fields_object = {};
    fields_object.fields = [];
    let keys = Object.keys(sections);
    const burndown = () => {
        let section = keys.shift();
        getSectionIDs(`repeating_${section}`, ids => {
            ids.forEach(id => 
               sections[section].forEach(field => 
                  fields_object.fields.push(`repeating_${section}_${id}_${field}`)));
            fields_object[section] = ids;
            if(keys.length) {
                burndown();
            } else {
                callback(fields_object);
            }
        });
    };
    burndown();
};Code language: JavaScript (javascript)

We run this exactly like getSectionIDs, and it finishes with the callback(fields_object) line. That’s the part of the function that is way too complicated to explain here. Just know that in JavaScript, functions are just a type of variable (object, really) and can be passed from one function to another just as if they were any other variable.

Think of it like this: it’s magic!

Summary

In this post, we created a drop-in function, getSectionObject, that works just like getSectionIDs, except that it will cope with as many repeating sections as we define. Just drop the function into any script block and then use it as often as we need to. We never need to worry about nesting repeating sections or setting up external global attributes. With this function, and the creation of a simple sections object, we can manage as many repeating sections as we need to.

That’s it for multiple repeating sections. But we aren’t finished with repeating sections. Stay tuned for the next post in the series, which will be a little less code-intensive!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.