Less Simple Repeating Sections

In the last post, we built a very simple repeating section for tracking encumbrance. But what if you don’t want to count up everything in the section, or want to sort them into containers. We’ll do that here.

In this section, we’ll use HTML, and CSS to arrange the columns.

<div class="gear-block">
   <div class="gear-section">
      <h4>Item</h4>
      <h4>Cost</h4>
      <h4>Weight</h4>
      <h4>#</h4>
      <h4>Where?</h4>
   </div>
   <fieldset class="repeating_gear">
     <div class="gear-section">
        <input type="text" name="attr_item" value="" placeholder="item">
        <input type="number" name="attr_cost" value="0">
        <input type="number" name="attr_weight" value="0">
        <input type="number" name="attr_quantity" value="0">
        <select name="attr_container">
            <option value="0" selected>?</option>
            <option>Body</option>
            <option>Backpack</option>
            <option>Mount</option>
            <option>Home</option>
        </select>
     </div>
   </fieldset>
   <div class="gear-totals">
      <span>Total Value</span>
      <input type="number" name="attr_total_cost" value="0">
      <span>Total On Body</span>
      <input type="number" name="attr_total_body" value="0">
      <span>Total in Backpack</span>
      <input type="number" name="attr_total_backpack" value="0">
      <span>Total on Mount</span>
      <input type="number" name="attr_total_mount" value="0">
      <span>Total at Home/span>
      <input type="number" name="attr_total_home" value="0">
      <span>Total Weight</span>
      <input type="number" name="attr_total_weight" value="0">
   </div>
</div>  Code language: HTML, XML (xml)

We start with the HTML. Placing a DIV containing the headings above the repeating section, then a DIC inside the section with the same class name allows us to use the same CSS style for both. This makes it very idea to line them up with CSS Grid.

div.gear-section {
   display: grid;
   grid-template-columns: 80px repeat(3, 50px) 80px;
   column-grid: 5px;
}
div.gear-section h4 {
   text-align: center;
}
div.gear-totals {
   display: grid;
   grid-template-columns: 240px 80px;
   column-grid: 5px;
}
div.gear-totals span {
   font-weight: bold;
}Code language: CSS (css)

With the CSS, we arrange the headings and contents into 5 columns, and make sure the headings are center-aligned. The gear-totals block shows the totals – the sheet worker will fill those totals. The spans are made bold, to count as headings.

Some people will use the label element in place of the span. I don’t like labels – they are made for form elements which don’t work on roll20 and have built in styling you need to modify. Spans are very modifiable.

Okay, you can see what the HTML and CSS look like. On to the sheet worker!

Sheet Worker

const section_attribute = (section, id, field) => `repeating_${section}_${id}_${field}`;
on('change:repeating_gear:cost change:repeating_gear:weight change:repeating_gear:quantity change:repeating_gear:container', () => {
   getSectionIDs('repeating_gear', id_gear => {
      const fields = [];
      id_gear.forEach(id => {
         fields.push(section_attribute('gear', id, 'cost'));
         fields.push(section_attribute('gear', id, 'weight'));
         fields.push(section_attribute('gear', id, 'quantity'));
         fields.push(section_attribute('gear', id, 'container'));
      });
      getAttrs(fields, values => {
         const output = {};
         const stats = ['cost', 'body', 'backpack', 'home', 'weight'];
         stats.forEach(stat => output[`total_${stat}`] = 0);
         id_gear.forEach(id => {
            const weight = +values[section_attribute('gear', id, 'weight')] || 0;
            const quantity = +values[section_attribute('gear', id, 'quantity')] || 0;
            const container = values[section_attribute('gear', id, 'container')];
            output[`total_${container.toLowerCase()}`] += weight * quantity;
            output.total_cost += (+values[section_attribute('gear', id, 'cost')] || 0);
         });
         stats.forEach(stat => {
            if(stat != 'cost' && stat != 'weight') {
                output.total_weight += output[`total_${stat}`];
            }
         });
         setAttrs(output);
      });
   });
});Code language: JavaScript (javascript)

This looks very complex, so lets break it down step by step.

Section Attribute Function

The worker starts with this line.

const section_attribute = (section, id, field) => `repeating_${section}_${id}_${field}`;Code language: JavaScript (javascript)

This is a simple function to save typing later. You’ll often need to type `repeating_gear_${id}_an_attribute_name’. With this function, you just need to type section_attribute(‘gear’, id, an_attribute_name) for the same effect. You’d pobably use a shorter name than section_attribute to reduce typing even further- I wanted to make it obvious what it was for.

Event Line

Here we use the syntax that allows us to respond to specific attributes within the repeating section.

on('change:repeating_gear:cost change:repeating_gear:weight change:repeating_gear:quantity change:repeating_gear:container', () => {Code language: JavaScript (javascript)

The name attribute isnt used, so it isn’t included here.

Also a useful tip: avoiding reusing a stat name you have used outside the repeating section. In Roll20 change:cost and change:repeating_gear:cost both trigger if you change the cost attribute. If you have a cost attribute in multiple repeating sections, and as a global attribute, it might trigger multiple sheet workers unneccesarily. Avoid this!

getSectionIDs

When doing anything involving more than one row of a repeating section, you need to use getSectionIDs.

   getSectionIDs('repeating_gear', id_gear => {Code language: JavaScript (javascript)

It’s handy to give the ids array a name that fits the section. We could just use ids or id_array or anything really, but ids_gear tells us two things:

  1. The variable contains ids.
  2. The variable is associated with the gear section.

That last point is handy to know, espeically if you are coding multiple sections, even multiple sections in the same sheet worker.

Push

Now you need to build an array of all the needed attribute names for getAttrs. Push is a great function for this. It’s not the only method, but it is easy.

      const fields = [];
      id_gear.forEach(id => {
         fields.push(section_attribute('gear', id, 'cost'));
         fields.push(section_attribute('gear', id, 'weight'));
         fields.push(section_attribute('gear', id, 'quantity'));
         fields.push(section_attribute('gear', id, 'container'));
      });Code language: JavaScript (javascript)

The push function accepts a comma separated list, so this could be done with one push command:

      const fields = [];
      id_gear.forEach(id => {
         fields.push(
             section_attribute('gear', id, 'cost'), 
             section_attribute('gear', id, 'weight'),
             section_attribute('gear', id, 'quantity'),
             section_attribute('gear', id, 'container')
         );
      });Code language: JavaScript (javascript)

The spacing and linebreaks here are just to make it easier to understand. You can nest functions inside each other easily, so this could also be written as:

      const fields = [];
      const field_names = ['cost', 'body', 'backpack', 'home'];
      id_gear.forEach(id => {
         field_names.forEach(field => {
            fields.push(section_attribute('gear', id, field); 
         })
      });Code language: JavaScript (javascript)

Here we create an array of the field names, then loop over that to build the section attribute names.

Taking advantage of the ability to string single line functions on one line we can make it even shorter:

      const fields = [];
      const field_names = ['cost', 'weight', 'quantity', 'container'];
      id_gear.forEach(id => field_names.forEach(field =>
         fields.push(section_attribute('gear', id, field)
      ));Code language: JavaScript (javascript)

Reduce instead of Push

There is a function called reduce which is extremely powerful and versatile. But it’s very complicated, so I wont explain it in detail. I’ll just show how it might be used.

      const fields = id_gear.reduce((all,id) => 
          [...all, section_attribute('gear', id, 'cost'), 
                   section_attribute('gear', id, 'weight'), 
                   section_attribute('gear', id, 'quantity'),
                   section_attribute('gear', id, 'container')], []);Code language: JavaScript (javascript)

You can nest reduce functions inside reduce functions- but it gets even more complicated.

Function Body

Everything before this has just been about collecting the attributes and their values that we’ll need to do the work. Now we do the actual work.

Collect Attribute Values

      getAttrs(fields, values => {Code language: JavaScript (javascript)

getAttrs accepts an array, and when using it you usually create the array as part of the process (like getAttrs([‘stat’], but if the stat names are already in an array, you don’t need the [ ] part.

Using an Output Object

         const output = {};Code language: JavaScript (javascript)

Here we create an object variable to hold multiple stat names and their values, and to avoid triggering setAttrs multile times. In some sheet workers, this might be called setObj or settings, and can be called anything at all. I always use output because that’s what it is.

Initalising the Totals Attributes

         const stats = ['cost', 'body', 'backpack', 'home', 'weight'];
         stats.forEach(stat => output[`total_${stat}`] = 0);Code language: JavaScript (javascript)

Here I demonstrate who ti create an array on the fly to make your code shorter. The attributes are called total_cost, total_body, total_backpack, total_home, and total_weight. We could write code to do all five of this, but with a simple forEach loop we can do all five in one line.

Looping Through the Repeating Section Values

         id_gear.forEach(id => {Code language: JavaScript (javascript)

We need to loop through the entire repeating section. We don’t know how many rows there are, so we need to do something for each row. The id_gear varianble was created earlier by getSectionIds, and contained each row id. So we know this will run once per row.

Initialising the Containers

            const weight = +values[section_attribute('gear', id, 'weight')] || 0;
            const quantity = +values[section_attribute('gear', id, 'quantity')] || 0;
            const container = values[section_attribute('gear', id, 'container')];Code language: JavaScript (javascript)

Within each row, we have to construct the names of the attributes again. They are in the values object. There are other ways to grab them (we could filter every object with the same row id, for example), but since we know what they are and what we plan to do with them, this is as good a way as any.

Ntice container is ot a number – it’s the name of the container, so we dont do the usual tricks to coerce into a number.

Adding To A Container

            output[`total_${container.toLowerCase()}`] += weight * quantity;Code language: JavaScript (javascript)

Now here we do something clever. When we grab an item from a row, it has a container. So we save the value to that container (or add the weight to that container).

Wee might modify the code to check if the container exists first. Users might not have set a container. I’ll leave that as an exercise for the readeer.

Calculating The Cost

            output.total+cost += (+values[section_attribute('gear', id, 'cost')] || 0);Code language: JavaScript (javascript)

By contrast, calculating the cost contribution is easy. It’s a simple value we need to extract from the values object then add to the cost

Adding Up All The Weights

         stats.forEach(stat => {
            if(stat != 'cost' && stat != 'weight') {
                output.total_weight += output[`total_${stat}`];
            }
         });Code language: JavaScript (javascript)

Earlier we created a stats array that contains all the total names. We want to add all the weights together, and skip cost – because it is not a weight. We also skip weight, because including it would lead to double dipping. If included, tt would be counted twice. (Ask me how I know…)

Finishing the Worker

         setAttrs(output);
      });
   });
});Code language: JavaScript (javascript)

Finally we save all the attributes we have created. This is where the output object shines.

Remember to close all the brackets. It can be complicated – this is where properly indenting your code comes in really handy!

Conclusion

This has been a very comprehensive overview of making a repeating section sheet worker with some variation (some items might or might not be included. some things multiply by others, etc.). But we aren’t finished yet.

There are special considerations for changing attributes within a single row, for combining multiple sheet workers and global stats, and styling the layout of a repeating section. Stay tuned for future posts on these topics.

Leave a Reply

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