Animated Timer: The Svelte Experience
Tags: svelte, react, tutorial, webdev
Published: 2023-09-04 05:14:33

Welcome, everyone, to the introductory article of a small two-article series that aims towards demonstrating not only the incredibly beautiful experience that **Svelte** provides, but also the contrast against #1 favorite world-wide: **React**.

**DISCLAIMER**: I am a good developer, but I specialize in the back-end. Still, I'm a strong overall performer. Hell, I'm a great performer. This, however, doesn't guarantee that I'll be writing the best possible **Svelte**, or the best possible **React**. It doesn't matter, actually. This actually works in the series' best interest: To describe my experience while working with the **Svelte** framework vs working with the **React** library. If I were the foremost expert on any of the two, then my experience wouldn't be representative of the majority of us developers out there.

> **Too impatient?** You can see the completed timer in the live demo at the bottom of the article.

## Motivation

I am writing about this because I am in the process of trying to get **Svelte** approved at work. Part of my efforts include demonstrating the benefits of it when compared to what is currently approved, which is **React**. One of said benefits, in many people's opinion is its ease of use. I was given a time slot of 25 minutes, so I figured that a live demonstration in the form of a timer could go a long way.

## Step 1: Basic Markup and Basic Styling

So first thing first: I want a timer in the form `HH:MM:SS` that counts down after pressing a **Start** button. I also want it to be inline, so I can use it in paragraphs, or as a separate element. This is just my developer reflex kicking in, always trying to create reusable code. It is not an actual requirement for my demo as the demo will use a timer as a block element.

This is what I start with:

```xml
<script>
	let hh = 0;
	let mm = 0;
	let ss = 0;
</script>

<span class="timer">
	<span class="value">
		{hh}
	</span>:<span class="value">
		{mm}
	</span>:<span class="value">
		{ss}
	</span>
</span>

<style>
	span.timer {
		padding: 0 0.2em;
	}
</style>
```

This shows up as `0 :0 :0`. Let's format to two digits.

## Step 2: Digit Formatting

Formatting should be simple enough: Add a left-sided zero for quantities less than 10. This would be the updated component:

```xml
<script>
	let hh = 0;
	let mm = 0;
	let ss = 0;

	function f(value) {
		if (value < 10) {
			return `0${value}`;
		}
		return value.toString();
	}
</script>

<span class="timer">
	<span class="value">
		{f(hh)}
	</span>:<span class="value">
		{f(mm)}
	</span>:<span class="value">
		{f(ss)}
	</span>
</span>

<style>
	span.timer {
		padding: 0 0.2em;
	}
</style>
```

This shows up as `00 :00 :00`. We are now ready to make the functional aspect work.

## Step 3: Add Counting Functionality

The component now needs a prop that the parent uses to tell the timer for how long it should tick. Furthermore, it needs a way to tick. If you have learned **Svelte** by consuming the entirety of the [learning website](https://learn.svelte.dev), then you probably remember the example about the readable store. Let's use that store as means of counting! The specific lesson is [here](https://learn.svelte.dev/tutorial/readable-stores). Click the `store.js` file.

Ok, this is what we add now to our component:

```javascript
    // Add the import line to the top of the script tag.
    import { readable } from 'svelte/store';

	// Props
	export let countFrom = 0;
	
	const remaining = readable(countFrom, function start(set) {
		const interval = setInterval(() => {
			set(new Date());
		}, 1000);
		
		return function stop() {
		clearInterval(interval);
		};
	});
```

Of course, the code we borrowed set date objects in the store, and we want the remaining countdown in seconds. To modify this, it occurs to me that we can calculate an end date object whenever the prop `countFrom` changes. Then we count down as the difference between this calculated end time and the current time. So, let's:

```javascript
	// Reactive to account for changes in countFrom:
	$: endDate = (function(secs) {
		const e = Date.now() + secs * 1000;
		return new Date(e);
	})(countFrom);
```

Ok, this may be more complex-looking that what I made it sound. If you don't recognize the syntax at play here, it is an **IIFE** (Immediately-Invoked Function Expression). Why? You have to make sure that the reactive code written will expose the use of `countFrom` in the calculation to the **Svelte** compiler's static code analysis algorithm to ensure reactivity. This code analysis algorithm, among others, will pick up variable use when used as a function argument, so this will correctly wire up reactivity. Yes, maybe a code block works too:

```javascript
	// Reactive to account for changes in countFrom:
    let endDate;
	$: {
		const e = Date.now() + countFrom * 1000;
		endDate = new Date(e);
	}
```

I did not test this, but I think it satifies the reactivity requirement of `endDate` being assigned a new value. I like the **IIFE** better. I feel it to be more succint.

> **NOTE**: At this point you should notice that we have implicitly required that `countFrom` be a number that represents **seconds**.

With the end date now calculated, let's modify our readable store:

```javascript
	const remaining = readable(countFrom, function start(set) {
		const interval = setInterval(() => {
			const r = Math.round((endDate - new Date()) / 1000);
			set(r);
		}, 1000);
		
		return function stop() {
		clearInterval(interval);
		};
	});
```

Now we can proceed to calculate the hours, minutes and seconds:

```javascript
	$: hh = Math.floor($remaining / 3600);
	$: mm = Math.floor(($remaining - hh * 3600) / 60);
	$: ss = $remaining - hh * 3600 - mm * 60;
```

Testing the component now will produce a fully functional timer! There is a problem, however: The counter goes beyond zero into negative values. Let's fix by not letting it and by stopping the timer once we reach zero:

```javascript
	const remaining = readable(countFrom, function start(set) {
		const interval = setInterval(() => {
			let r = Math.round((endDate - new Date()) / 1000);
			r = Math.max(r, 0);
			set(r);
			if (r <= 0) {
				clearInterval(interval);
			}
		}, 1000);
		
		return function stop() {
		clearInterval(interval);
		};
	});
```

Before calling it done, I really want to cover two things:

1. I want a `timesup` event to notify the parent component that the timer finished.
    
2. I want to get rid of the nasty spaces between the numbers and the colons (`:`).
    

## Step 4: Finishing up

The spacing issue is very simple: Because we tend to write markup with relatively short markup lines, the resulting HTML ends up with a space because whitespace in text nodes is collapsed into a single space. So by just re-writing the markup like the following does the trick:

```xml
<span class="timer">
	<span class="value">
		{f(hh)}</span>:<span class="value">
		{f(mm)}</span>:<span class="value">
		{f(ss)}
	</span>
</span>
```

Not very good-looking, but does the job. If anyone knows a better solution, I'm all ears!

The other feature is an event. Thanks to **Svelte**, this is super easy:

```javascript
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher();

	$: if ($remaining === 0) {
		dispatch('timesup');
	}
```

Since I did not write that as shown, but instead the lines are interleaved with previous code, allow me to show the entire component file as it stands up to this point:

```xml
<script>
	import { readable } from 'svelte/store';
	import { createEventDispatcher } from 'svelte';

	// Props
	export let countFrom = 0;

	const dispatch = createEventDispatcher();

	// Reactive to account for changes in countFrom:
	$: endDate = (function(secs) {
		const e = Date.now() + secs * 1000;
		return new Date(e);
	})(countFrom);
	
	const remaining = readable(countFrom, function start(set) {
		const interval = setInterval(() => {
			let r = Math.round((endDate - new Date()) / 1000);
			r = Math.max(r, 0);
			set(r);
			if (r <= 0) {
				clearInterval(interval);
			}
		}, 1000);
		
		return function stop() {
		clearInterval(interval);
		};
	});
	
	$: hh = Math.floor($remaining / 3600);
	$: mm = Math.floor(($remaining - hh * 3600) / 60);
	$: ss = $remaining - hh * 3600 - mm * 60;

	$: if ($remaining === 0) {
		dispatch('timesup');
	}

	function f(value) {
		if (value < 10) {
			return `0${value}`;
		}
		return value.toString();
	}
</script>

<span class="timer">
	<span class="value">
		{f(hh)}</span>:<span class="value">
		{f(mm)}</span>:<span class="value">
		{f(ss)}
	</span>
</span>

<style>
	span.timer {
		padding: 0 0.2em;
	}
</style>
```

Ok, time to test the whole thing. Let's create a parent component that:

1. Provides a numeric input box for us to set the initial seconds for the timer.
    
2. Consumes the `timesup` event.
    

This is very *svelty* (synonym for simple):

```xml
<script>
	import Timer from './Timer.svelte';

	let countFrom = 0;

	function handleTimesup() {
		console.log("Time's up!!");
	}
</script>

<label>Timer's Initial count:
	<input type="number" bind:value={countFrom} />
</label>
<br />
<Timer {countFrom} on:timesup={handleTimesup} />
```

Great, now let's type a number in the numeric input box and ... doesn't work. Why? The readable store is constant in our code, and it is created on component creation, where the value of `countFrom` is zero. It needs to react to changes of `countFrom`. The change is trivial, again thanks to **Svelte**. We just remove the `const` keyword and replace it with **Svelte**'s reactive syntax:

```javascript
	$: remaining = readable(countFrom, function start(set) { ...
```

Test again. All works as expected. Let's call this moment **Milestone 1**. We will define this milestone in the next article once we reach to a **React** equivalent.

**Milestone 1** has a total of `59 lines - 12 blank or comment lines = 47 lines of code`. Now, **React** doesn't really provide CSS and CSS is provided by third party libraries or tools, so let's also remove those lines from the count. So **Milestone 1**'s total line count is **42**.

## But I Said "Animated" Timer

Ah yes, so we are not really finished. True. Animation is also not found in **React**, so I purposedly left it out of **Milestone 1**. Achieving animation will be **Milestone 2**. I am still debating in my head whether or not I want to learn how to animate in **React**, which of course would entail studying the different options out there and selecting one, then learning it, then applying it. Honestly, the "don't learn any more **React**" side is winning... by a lot.

Anyway, let's animate. I want that, every time the values of `hh`, `mm`, or `ss` change, the old values fly away in the **up** direction, and the new values fly in from **below**.

### About Svelte's `fly` Transition

I haven't explored the source code for `fly()`, but I tested it quickly and things didn't fly as expected. A quick search told me that **Svelte** animations only work properly on block elements, not inline elements (see this [SO question](https://stackoverflow.com/questions/65218417/how-to-use-svelte-to-vertical-fly-in-and-out-element)). This is a pickle because all of our elements in the timer are inline elements so far, but fear not as we can use CSS to make them `inline-block` elements.

To test our CSS setup for animation, however, we should have the actual animation in place because how can we test it if we don't have it? So let's do that first.

### Adding the `fly` Transition

**Svelte** transitions only play whenever the DOM element is created or destroyed. If the contents change but the element is not removed, no transition takes place. To force the transition animation, we'll use **Svelte**'s `{#key}` block.

This is the new code:

```xml
<script>
	import { fly } from 'svelte/transition';

	// Animation-related.
	const duration = 180;
	const delay = 90;
</script>

<span class="timer">
	{#key hh}
		<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(hh)}</span>
	{/key}
	:
	{#key mm}
		<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(mm)}</span>
	{/key}
	:
	{#key ss}
		<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(ss)}</span>
	{/key}
</span>

<style>
	span.timer {
		padding: 0 0.2em;
	}
	span.value {
		display: inline-block;
	}
</style>
```

> This code snippet, although it appears to be the whole component, it is not. It is not showing the rest of the JavaScript code we have written so far. Hopefully, this is not an issue and you are following the process just fine, yes?

Note how we are making our span elements animation-compatible by setting their `display` attribute to `inline-block`.

With the changes for animation, we see the extra spacing betweeen numbers and colons resurface, but that's a minor thing compared to what's going on with the animation: The timer expands to accommodate the two versions of the seconds for brief moments, making the timer grow its width. This is not good. Why does this happen?

### Fixing the `fly` Transition

To fix the problem, let's understand the problem: **Svelte**, by means of the `{#key}` block, is instructed to create a new DOM element that will replace the previous one so the transition animation fires on value change. This means that, while transitioning, we have two versions of the same `<span>` element. The container, which is another `<span>`, grows to the right for the duration of the animation to accomodate for the extra element, as per its default behavior.

One could think that adding the CSS `overflow: hidden` would work here, but it wouldn't because it must be paired with a maximum or fixed width, and that would require to measure the width of the text within the child element. Too complex.

Thankfully, we have flex at our disposal. If we give the animated span elements a new parent that forbids more than one element per line, then the growing pain goes away.

This is the ammended code, which also fixes the problem of the spaces around the colons, again, using the same fix we did before:

```xml
<span class="timer">
	<span class="value">
		{#key hh}
			<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(hh)}</span>
		{/key}
	</span>:<span class="value">
		{#key mm}
			<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(mm)}</span>
		{/key}
	</span>:<span class="value">
		{#key ss}
			<span class="value" in:fly={{ delay, duration, y: '1em'}} out:fly={{ duration, y: '-1em'}}>{f(ss)}</span>
		{/key}
	</span>
</span>

<style>
	span.timer {
		padding: 0 0.2em;
	}
	span.value {
		display: inline-flex;
		flex-flow: column;
	}
	span.value > span {
		display: inline-block;
	}
</style>
```

We have created span elements for each of the timer parts that contain the animated span element. We have also configured via CSS this new container as a flex container that accommodates the elements vertically (`flex-flow: column`), not horizontally.

Test again. Success! Beautiful-looking timer that we have now. We are done with **Milestone 2**. Or are we? We are not!

Add something below the timer. I added a paragraph:

```xml
<script>
	import Timer from './Timer.svelte';

	let countFrom = 0;

	function handleTimesup() {
		console.log("Time's up!!");
	}
</script>

<label>Timer's Initial count:
	<input type="number" bind:value={countFrom} />
</label>
<br />
<Timer {countFrom} on:timesup={handleTimesup} />
<p>The quick brown fox jumps over the lazy dog.</p> <!-- THIS -->
```

Now this paragraph is the one jumping around. Why? The container grows down. Same problem, different direction. The difference here, though, is that we can easily fix the container's height with CSS:

```css
	span.value {
		display: inline-flex;
		flex-flow: column;
		height: 1em; /* Like this!  No calculations needed. */
	}
```

Now we are truly done with **Milestone 2**. Let's do the code line count: **66** lines with CSS, or **53** without it. Animation costed **11** lines of code, plus some CSS. Not too bad. The main factor driving the line increase was adding the `{#key}` blocks and the extra span elements. **8** of those **11** lines are found in the HTML markup.

---

Ok, so this was fun, as it is common with **Svelte**. If you would like to see the timer live, head to [this REPL](https://svelte.dev/repl/6f543749f2f24e4fb28427dec562fd8a?version=4.2.0) or the below live demo:

{% embed https://stackblitz.com/edit/animated-svelte-timer?embed=1&file=README.md&view=preview %}

Let's meet together again in article 2 of the series, where we'll explore how to replicate this in **React** and then make a fair comparison between the two.

Happy coding!