We recently had the opportunity to build a relatively small prototype application with the front-end JavaScript framework Svelte. Along the way, we took note of some issues we came across as Svelte beginners in hopes that we could help other developers that might come across similar issues. In this article, we are going to talk about how to approach a scenario where you need access to intermediate variables inside of an {#each} block.
What is an {#each} block
First things first, what is an {#each} block? An {#each} block is a Svelte mechanism for iterating over data sets to generate value specific HTML. In some ways {#each} blocks are similar to JavaScript’s forEach method.
For example, the following JavaScript code:
['1','2','3','4'].forEach(val => {
const node = document.createElement("div");
const textnode = document.createTextNode(` JS: ${val}`);
node.appendChild(textnode);
document.body.appendChild(node);
})
Is roughly equivalent to:
{#each ['1','2','3','4'] as item}
<div>Svelte {item} </div>
{/each}
As you can see Svelte’s {#each} blocks reduce the code and complexity needed to generate HTML from simple data sets. However, as we found out, there are cases where we do not have all of the data defined upfront. Sometimes we need to derive data from our data sets as we render inside these {#each} blocks. Unfortunately, Svelte does not provide a way for us to do so. Let’s take a look at one case where we need to derive data while rendering our UI.
SAMPLE APPLICATION
Our sample applications below generate the slash lines for a set of baseball players. Given a set of data we need to display the results of the following calculations.
- Batting Average (AVG) = Number of Hits / Total at Bats;
- On Base Percentage (OBP) = (Hits + Walks + Hit by Pitch) / (At Bats + Walks + Hit by Pitch + Sacrifice Flies).
- Slugging Percentage (SLG)= ([Singles] + [Doubles x 2] + [Triples x 3] + [Home Runs x 4])/[At Bats];
- On Base Plus Slugging (OPS) = On Base Percentage + Slugging Percentage;
- Slash Line = ${AVG} / ${OBP} / ${SLG} / ${OPS}
Note: If it helps to have context, I’ve added a section at the bottom of this article describing these stats.
IN THIS APPLICATION WE ARE:
- Defining our data set
- Defining a number of functions that we can use to calculate AVG, OBP, SLG, OPS for a set of data
- Iterating over our data set using an {#each} block to calculate AND display a player’s slash lines
<script>
const players = [
{name: 'Yasmani Grandal', atBats:161, hits:37, doubles:7, triples:0, homeRuns:8, walks:30, hitByPitch:1, sacrificeFlies:2},
{name: 'José Abreu', atBats:240, hits:76, doubles:15, triples:0, homeRuns:19, walks:18, hitByPitch:3, sacrificeFlies:1},
{name: 'Nick Madrigal', atBats:103, hits:35, doubles:3, triples:0, homeRuns:0, walks:4, hitByPitch:3, sacrificeFlies:0},
{name: 'Tim Anderson', atBats:208, hits:67, doubles:11, triples:1, homeRuns:10, walks:10, hitByPitch:2, sacrificeFlies:1},
{name: 'Yoan Moncada', atBats:200, hits:45, doubles:8, triples:3, homeRuns:6, walks:28, hitByPitch:1, sacrificeFlies:2},
{name: 'Eloy Jimenez', atBats:213, hits:63, doubles:14, triples:0, homeRuns:14, walks:12, hitByPitch:0, sacrificeFlies:1},
{name: 'Luis Robert', atBats:202, hits:47, doubles:8, triples:0, homeRuns:11, walks:20, hitByPitch:1, sacrificeFlies:2}
]
const formatPercentage = (val) => val.toFixed(3).substring(1);
const calcBattingAverage = ({hits, atBats}) => hits/atBats;
const calcOnbasePercentage = ({hits, walks, hitByPitch, atBats, sacrificeFlies}) =>
(hits + walks + hitByPitch) / (atBats + walks + hitByPitch + sacrificeFlies );
const calcSingles = ({hits, doubles, triples, homeRuns}) => hits - doubles - triples - homeRuns;
const calcTotalBases = ({singles, doubles, triples, homeRuns}) => singles + (doubles * 2) + (triples * 3) + (homeRuns * 4);
const calcSlugging = ({totalBases, atBats}) => totalBases/ atBats;
</script>
{#each players as player}
<div>
{player.name}
{formatPercentage(calcBattingAverage(player))} /
{formatPercentage(calcOnbasePercentage(player))} /
{formatPercentage(
calcSlugging({
...player,
totalBases: calcTotalBases({...player, singles: calcSingles(player)})
})
)
} /
{formatPercentage(calcOnbasePercentage(player) + calcSlugging({
...player,
totalBases: calcTotalBases({...player, singles: calcSingles(player)})
})
)}
</div>
{/each}
The first two calculations that we need to make in order to generate a slash line, AVG, and OBP, are very straightforward. All of the data needed to calculate these two values already exist in our data set.
THE PLOT THICKENS
When we move on to SLG things get a little more complicated. For this calculation, we need to add up the total number of bases a player has reached on their hits. Notice that our data set includes the total number of doubles, triples, and home runs, but the number of singles a player has hit is missing from our data set. We can calculate the number of singles a player has hit by adding the number of extra-base hits (doubles, triples, and home runs) and subtracting that total from the total number of hits. Once we have this value, we have all the data we need to calculate the slugging percentage.
At this point, we have our players AVG, OBP, and SLG. The last value we need to calculate is OPS which is OBP + SLG. This should be easy since we have already calculated and displayed these values. However, as we noticed when developing our POC, svelte currently does not offer a mechanism for us to store any intermediate values. While we have already calculated and displayed OBP and SLG, we do not have access to these values in our svelte template.
If we were writing our code in plain JavaScript, we could create intermediate values that we could use to cache values for reuse in the current iteration:
...
players.forEach(player => {
const node = document.createElement("div");
const avg = calcBattingAverage(player);
const obp = calcOnbasePercentage(player)
const singles = calcSingles(player);
const totalBases = calcTotalBases({...player, singles);
const slg = calcSlugging({
...player,
totalBases
});
const ops = obp + slg;
const slashLine = `${formatPercentage (avg)} / ${formatPercentage(obp)} / ${formatPercentage(slg)} / ${formatPercentage(ops)}`;
const textnode = document.createTextNode(slashLine);
node.appendChild(textnode);
document.body.appendChild(node);
})
...
But because there is no, way of storing intermediate variables inside of an {#each} block, this application needs to re-calculate, OBP, number of singles, total bases, and SLG twice for each player.
After doing some research we found that when people asked about using intermediate variables inside of an {#each} block, there were two common responses:
- This is a sign that you should break your application/component into smaller components.
- You should prepare your data ahead of time, so you don’t run into this type of situation.
Let’s explore what our application would look like in each of these cases.
BREAKING THE APPLICATION INTO SMALLER COMPONENTS:
This is an example of what our application could look like if we broke it down into smaller components:
App.Svelte
<script>
import SlashLine from './SlashLine.svelte';
const players = [
{name: 'Yasmani Grandal', atBats:161, hits:37, doubles:7, triples:0, homeRuns:8, walks:30, hitByPitch:1, sacrificeFlies:2},
{name: 'José Abreu', atBats:240, hits:76, doubles:15, triples:0, homeRuns:19, walks:18, hitByPitch:3, sacrificeFlies:1},
{name: 'Nick Madrigal', atBats:103, hits:35, doubles:3, triples:0, homeRuns:0, walks:4, hitByPitch:3, sacrificeFlies:0},
{name: 'Tim Anderson', atBats:208, hits:67, doubles:11, triples:1, homeRuns:10, walks:10, hitByPitch:2, sacrificeFlies:1},
{name: 'Yoan Moncada', atBats:200, hits:45, doubles:8, triples:3, homeRuns:6, walks:28, hitByPitch:1, sacrificeFlies:2},
{name: 'Eloy Jimenez', atBats:213, hits:63, doubles:14, triples:0, homeRuns:14, walks:12, hitByPitch:0, sacrificeFlies:1},
{name: 'Luis Robert', atBats:202, hits:47, doubles:8, triples:0, homeRuns:11, walks:20, hitByPitch:1, sacrificeFlies:2}
]
</script>
{#each players as player}
<SlashLine {player}/>
{/each}
SlashLine.Svelte
<script>
export let player = {};
const formatPercentage = (val) => val.toFixed(3).substring(1);
const calcBattingAverage = ({hits, atBats}) => hits/atBats;
const calcOnbasePercentage = ({hits, walks, hitByPitch, atBats, sacrificeFlies}) =>
(hits + walks + hitByPitch) / (atBats + walks + hitByPitch + sacrificeFlies );
const calcSingles = ({hits, doubles, triples, homeRuns}) => hits - doubles - triples - homeRuns;
const calcTotalBases = ({singles, doubles, triples, homeRuns}) => singles + (doubles * 2) + (triples * 3) + (homeRuns * 4);
const calcSlugging = ({totalBases, atBats}) => totalBases/ atBats;
$:({name, hits, atBats, hits, doubles, triples, homeRuns, walks, hitByPitch, sacrificeFlies} = player);
$:battingAverage = calcBattingAverage(player);
$:onBasePercentage = calcOnbasePercentage(player);
$:slugging = calcSlugging(
{totalBases: calcTotalBases({...player, singles: calcSingles(player)}),
...player
}
);
$:onBasePlusSlugging = onBasePercentage + slugging;
</script>
<div>
{name}
{formatPercentage(battingAverage)}/
{formatPercentage(onBasePercentage)}/
{formatPercentage(slugging)}/
{formatPercentage(onBasePlusSlugging)}
</div>
In this version of our application, the main application iterates over our players data set with the {#each} block. Each player data set is passed into a new instance of our new SlashLine component which is responsible for calculating and displaying the slash line for one set of player data.
To me, this solution is easier to reason through than our original application and does a good job of breaking up the responsibility of calculating and displaying a slash line from iterating over a set of data. This approach will also allow us to use slash line logic in other parts of the application. For example, given a complete set of data, we could reuse this component to show the slash line for a team or an entire league.
While there are benefits to this type of approach, there could be some downsides depending on your situation. In this case, we have a very small application. To prevent redundant calculations, we need to create an entirely new component, that only exists to allow us to use intermediate variables. Breaking a component up into smaller components just to work around the lack of intermediate variables may be more work than it’s worth and may cause maintenance issues down the road.
Prepare your data ahead of time:
This is an example of what our application could look like if we prepared our data to include slash line data ahead of time.
<script>
const players = [
{name: 'Yasmani Grandal', atBats:161, hits:37, doubles:7, triples:0, homeRuns:8, walks:30, hitByPitch:1, sacrificeFlies:2},
{name: 'José Abreu', atBats:240, hits:76, doubles:15, triples:0, homeRuns:19, walks:18, hitByPitch:3, sacrificeFlies:1},
{name: 'Nick Madrigal', atBats:103, hits:35, doubles:3, triples:0, homeRuns:0, walks:4, hitByPitch:3, sacrificeFlies:0},
{name: 'Tim Anderson', atBats:208, hits:67, doubles:11, triples:1, homeRuns:10, walks:10, hitByPitch:2, sacrificeFlies:1},
{name: 'Yoan Moncada', atBats:200, hits:45, doubles:8, triples:3, homeRuns:6, walks:28, hitByPitch:1, sacrificeFlies:2},
{name: 'Eloy Jimenez', atBats:213, hits:63, doubles:14, triples:0, homeRuns:14, walks:12, hitByPitch:0, sacrificeFlies:1},
{name: 'Luis Robert', atBats:202, hits:47, doubles:8, triples:0, homeRuns:11, walks:20, hitByPitch:1, sacrificeFlies:2}
]
const formatPercentage = (val) => val.toFixed(3).substring(1);
const calcBattingAverage = ({hits, atBats}) => hits/atBats;
const calcOnbasePercentage = ({hits, walks, hitByPitch, atBats, sacrificeFlies}) =>
(hits + walks + hitByPitch) / (atBats + walks + hitByPitch + sacrificeFlies );
const calcSingles = ({hits, doubles, triples, homeRuns}) => hits - doubles - triples - homeRuns;
const calcTotalBases = ({singles, doubles, triples, homeRuns}) => singles + (doubles * 2) + (triples * 3) + (homeRuns * 4);
const calcSlugging = ({totalBases, atBats}) => totalBases/ atBats;
$:playerData = players
.map((player) => ({ ...player, battingAverage: calcBattingAverage(player)}))
.map((player) => ({ ...player, onBasePercentage: calcOnbasePercentage(player)}))
.map((player) => {
const singles = calcSingles(player);
const totalBases = calcTotalBases({singles, ...player})
return {...player, sluggingPercentage: calcSlugging({totalBases, ...player})}
}).map((player) => ({...player, onBasePlusSlugging: player.onBasePercentage + player.sluggingPercentage}))
</script>
{#each playerData as {name, battingAverage, onBasePercentage, sluggingPercentage,onBasePlusSlugging}}
<div>
{name}
{formatPercentage(battingAverage)}/
{formatPercentage(onBasePercentage)}/
{formatPercentage(sluggingPercentage)}/
{formatPercentage(onBasePlusSlugging)}
</div>
{/each}
In this example, we apply a number of map functions to our players data set to build a new data set of players with slash line data.
After the series of projections, a player record like this:
{name: 'José Abreu', atBats:240, hits:76, doubles:15, triples:0, homeRuns:19, walks:18, hitByPitch:3, sacrificeFlies:1}
Would yield a record that looked like something like this:
{name: 'José Abreu', atBats:240, hits:76, doubles:15, triples:0, homeRuns:19, walks:18, hitByPitch:3, sacrificeFlies:1, battingAverage:0.317 , onBasePercentage: 0.370 , sluggingPercentage; 0.617 , onBasePlusSlugging:0.987}
Once all of our players were run through our projection functions, we have access to all the relevant data which will be rendered in our template. While the template code is much easier to read, and all of the code is assembled in one application, the projection code may be difficult to reason through and debug at first glance. It is also the case that, while likely not a big deal, we are iterating over our data set twice, in an effort to work around a missing feature.
WHAT APPROACH SHOULD YOU CHOOSE?
That completely depends on your application and what is trying to be accomplished. While I like both solutions, in this case, I would tend to lean towards the solution that is easiest for other developers to read and reason through; I would break my application into smaller components.
With that being said help may be on the way. There are open discussions on how to best add intermediate variables to svelte. We may soon see a new {@const} or {#with} block constructs that will allow us to create intermediate variables in cases like this where we want to derive data inside of our Svelte template.
Baseball Terminology / Formulas
Slash Line:
“Slash line is a colloquial term used to represent a player’s batting average, on-base percentage, and slugging percentage. Those three stats are often referenced together in baseball media with forward slashes separating them, which is where the term slash line comes from.”
A slash line generally looks like this
AVG / OBP / SLG
.317/ .370 /.617
Note: We are going to include an additional metric, On Base Plus Slugging, to our slash line to help with our example.
Batting Average (AVG):
“One of the oldest and most universal tools to measure a hitter’s success at the plate, batting average is determined by dividing a player’s hits by his total at-bats for a number between zero (shown as .000) and one (1.000). In recent years, the league-wide batting average has typically hovered around .250.”-)
The formula for a player batting average is:
Batting Average = Number of Hits / Total at Bats;
On Base Percentage:
“OBP refers to how frequently a batter reaches base per plate appearance. Times on base include hits, walks, and hit-by-pitches, but do not include errors, times reached on a fielder’s choice, or a dropped third strike.”
The full formula for On Base Percentage is
On Base Percentage = (Hits + Walks + Hit by Pitch) / (At Bats + Walks + Hit by Pitch + Sacrifice Flies).
Slugging Percentage:
“Slugging percentage represents the total number of bases a player records per at-bat. Unlike on-base percentage, slugging percentage deals only with hits and does not include walks and hit-by-pitches in its equation.”
The formula for Slugging Percentage is:
Slugging Percentage = ([Singles] + [Doubles x 2] + [Triples x 3] + [Home Runs x 4])/[At Bats];
On-Base Plus Slugging
“OPS adds on-base percentage and slugging percentage to get one number that unites the two. It’s meant to combine how well a hitter can reach base, with how well he can hit for average and for power.”-
The formula for On Base Plus Slugging is
Base Plus Slugging = On Base Percentage + Slugging Percentage;