Intro
This is just a quick post about a revelation I have made today about the profound consequences of how Svelte markups work. Or more precisely, how they do not work...
Long Story Short
I am new to Svelte, coming from a React background and that means that I will try things in a 'Reacty' way first (why, if we have words like 'cupboardy', 'Reacty' is fine), just like anyone else, first trying stuff that is familiar.
However Svelte is radically different from React in many ways, and one of these areas is its approach to markup (you know the HTML like thing with the angle brackets).
The Problem
Lets just say I have a bunch of meet-up event objects in an array, which I want to render as a list. There is one tiny twist, when there are multiple events on the same day, I want to collapse these events under the same date (only render the date in front of the very first event on any given day).
React Instincts Kick in
So I thought, ok fine, I can just use an #if to emit the date if it is different from the 'currentDate' that we hold in a variable.
{#each upcomingEvents as event}
{#if event.date !== currentDate }
<label>{event.date}</label>
{/if}
<label>{event.time}</label>
<label>{event.title}</label>
<label>{event.group}</label>
{/each}
Looks promising right? Ok let's add the variable
<script>
let currentDate = null;
<script>
It is again dead easy, we just declare the variable, and since it is null
, the very first item in the array will have a different date, so that is good as we will render it nicely.
Now there is just one more tiny problem to solve, updating this variable with the lastly rendered event's date. Should not be too difficult right?
Try something like:
{#each upcomingEvents as event}
{#if event.date !== currentDate }
<label>{event.date}</label>
{/if}
<label>{event.time}</label>
<label>{event.title}</label>
<label>{event.group}</label>
{currentDate = event.date}
{/each}
(As an astute reader pointed out this is not idiomatic React. Yes true, we would not do an assignment like that from JSX, but we also don't have 'each' (or templating keywords) in React, where you'd probably map the array to JSX. Lets just say the assignment to currentDate is the next logical option that was in my mind (given we don't do mapping to markup in Svelte!) and this article is showing how the revelations unfolded for me, so you get to see the work in progress :)
Svelte Markup != JSX
Too bad, it does not work. And here is the deal - you cannot do this the React way. React's JSX is two-way, code and markup are interchangeable. You can return JSX from functions, you can switch to code entirely within your JSX (that is why you can just call Array.map() to transform a piece of data to markup in the middle of your JSX expression).
Svelte markup is markup only, or we could say it is one directional. It can render data, but there is no way to execute code from markup (this is exactly why Svelte needs a few templating keywords like #each and #if, to be able to apply some minimal logic to rendering).
So how can this problem be solved with Svelte then?
A Moment of Revelation
The answer is separation of concerns. Separating presentation / rendering concerns (done by markup) and model transformations (done by code).
Therefore, to achieve what we want, we will need to create a derivation of our primary model object (the array of event objects) such that our events are grouped by date, and then render this model by a simple markup:
let upcomingEventsGrouped;
$: {
let currentDate = null;
upcomingEventsGrouped = [];
upcomingEvents.forEach(({date, time, title, group}) => {
const _date = (date !== currentDate ? date : null);
upcomingEventsGrouped.push({
time, title, group, date: _date
});
currentDate = date;
});
upcomingEventsGrouped = upcomingEventsGrouped;
}
It is worth noting at this point, that this model derivation is declared reactively (it is recomputed every time the underlying data, upcomingEvents
in this case, changes). That's achieved by using '$:' - isn't that just wonderful?!
Here is the template to render the derived data:
{#each upcomingEventsGrouped as event}
{#if !!event.date }
<label>{event.date}</label>
{/if}
<label>{event.time}</label>
<label>{event.title}</label>
<label>{event.group}</label>
{/each}
Really simple, just mapping the data attributes to markup elements, really easy to read!
And here's a pieace of event code that tests adding a new event in the upcomingEvents
array:
function addEvent(e) {
upcomingEvents = [...upcomingEvents, {
date: '20/06/2020',
time: '18:00',
title: 'Templating with Svelte 2.',
group: 'Svelte Rookies'
}];
}
Note that we need to use assignment in some form wherever we update an array otherwise Svelte will not pick up on the change!
Finally
I think Svelte's take on the markup may come across first as limiting and less powerful than React's JSX, however there is huge power in this - the power of simplicity.
React JSX lends itself for complicated solutions (taking on responsibilities of the view model), while Svelte's markup forces you to offload these concerns to your script element, resulting in a simple 1:1 mapping when it comes to rendering the model.
Thanks for reading this article, hope you enjoyed it and if you have anything to add/correct etc, please do leave a comment!
Top comments (1)
Nice one Janos 👏🏽