Build a Calculator Component in Svelte

And use it in your current website

0

There’s no need to go all or nothing with Svelte. You can build a few components in it and use them in your existing website. Here, we’ll build a calculator app in Svelte and deploy it in a basic HTML website. Here it is on the right: it’s live! Give it a try!

This fairly full-featured calculator component is based on the iPhone calculator app, but with the rectangular buttons of the Mac calculator app. While this project is more complex than is needed just to learn Svelte, I find it of interest in itself, and it’s more real-world than the trivial examples often used. I am assuming you are familiar with HTML, CSS, and JavaScript. This is not a total beginner’s introduction to Svelte—for that, go to the great tutorial by Svelte. If you are more interested in embedding Svelte components into existing apps, you can skim over most of this calculator post, or just visit Embed Our Calculator Component into Your Existing Website, and download the finished calculator code as explained there.

Let’s start with a little planning. We’re building a Calculator component. The calculator has a bunch of buttons (19, to be precise) and a display. So let’s create components for the buttons, which we’ll call Calcbtn, and for the display, which we’ll call Display (in real life, we’d probably call it CalcDisplay). Note that Svelte components are required to start with a capital letter. To start off with, we’ll just write enough code to create a static display, and then we’ll add functionality to it.

There are four columns of buttons, so we’ll use CSS grid to do our layout. Some of the buttons have different colored backgrounds, so we’ll create classes for them.

Being lazy by nature, I’m going to build this in the Svelte REPL environment rather than having us set up a Svelte development environment right now. We’ll do that in my upcoming post on using MySQL in SvelteKit.

So open another browser window, go to https://svelte.dev/, and click on REPL from the main menu. This brings up a tab named App.svelte on the left, and a Result tab on the right. In order to save your work, click on the right where it says “Log in to save and log in.

Eventually, we will change App.svelte to display a Calculator component, but in order to avoid error messages, we’ll leave it alone for now. Let’s begin by creating our subcomponents. Click on the + near the upper left next to App.Svelte, and rename the new tab to Calculator.svelte. We’ll just l”eave it blank for the moment. Click the + again, and rename this new tab to Display.svelte. Enter the following code:

<script>
	let val = 0;
	let fontSize = "3.2em"
	const toDispString = (val) => {
		if (val == 0) return "0";
	}
</script>
<div style = "font-size: { fontSize }">
	{ toDispString(val) }
</div>
<style>
	div {
		grid-column-start: 1;
		grid-column-end: 5;
		background-color: #444;
		color: white;
		font-weight: 100;
		text-align: right;
		align-self: end;
		padding: 0 var(--fontSize) 0 0;
	}
</style>

Each Svelte component (like this one) can contain up to three sections: a script tag containing JavaScript code, HTML code, and a style tag with CSS code. (There can also be a module-level script tag, which I will introduce below). Each instance of the component will get a copy of the HTML and have access to the JavaScript and the CSS. One of the gifts of Svelte is that the CSS is scoped, so that it only applies to the component within which it appears. This eliminates the need for complicated methods of dealing with scoping CSS such as BEM or Tailwind.

The script is basic JavaScript, with a few tweaks to add Svelte functionality. The function toDispString will eventually take the value computed by the calculator and format it for the display, but for now, it always returns "0". The HTML, as we see here, allows Javascript values to be embedded by surrounding them with curly braces. So the font-size will initially be set to 3.2em, and the div content will be "0". The CSS here is straightforward, making the display span the grid and setting text display qualities.

Now click on the plus sign again to create our other subcomponent, and name this tab Calcbtn.svelte. Enter the following code:

<script>
	export let width = "";
	export let use = "number";
</script>
<button class="{width} {use}" >
    <slot></slot>
</button>
<style>
	button {
		background: #777;
		height: 100%;
		color: white;
		font-size: 140%;
		font-weight: 200;
		border: none;
		margin: 0;
	}
	button:active {
		background:#aaa;
	}
	.twowide {
		grid-column-end: span 2;
		text-align: left;
		padding-left: 1.3em;
	}
	.oper {
		background: #f94;
		font-size: 180%;
		padding-top: .31em;
	}
	.oper.held {
		background: #c72;
	}
	.fn, .plusminus {
		background: #555;
	}
	button.oper:active {
		background: #c72;
	}
	button.fn:active, button.plusminus:active {
		background: #777;
	}
</style>

The export statement in the <script> section signifies that these are props (properties) that will be passed to the component when this component is instantiated by a caller. The initial values are defaults. When we create a Calcbtn component, we will invoke it like this: <Calcbtn use="fn">%</Calcbtn>. The content between the opening and closing tags (in this case, the percent sign) will replace the <slot></slot> portion of the HTML code.

So Let’s See Some Results!

Now that we’ve defined our subcomponents, we’re ready to define our Calculator component. Click back on the App.svelte tab, and enter the following code:

<script>
	import Calcbtn from './Calcbtn.svelte';
	import Display from './Display.svelte';
	export let calcFontSize = "16px";
	let calc;
</script>
<div class="calc" style="--fontSize:{calcFontSize};" bind:this={calc}>
	<Display />
	<Calcbtn use="fn">AC</Calcbtn>
	<Calcbtn use="plusminus"><sup>&plus;</sup>/<sub>&minus;</sub></Calcbtn>
	<Calcbtn use="fn">%</Calcbtn>
	<Calcbtn use="oper">&div</Calcbtn>
	<Calcbtn>7</Calcbtn>
	<Calcbtn>8</Calcbtn>
	<Calcbtn>9</Calcbtn>
	<Calcbtn use="oper">&times;</Calcbtn>
	<Calcbtn>4</Calcbtn>
	<Calcbtn>5</Calcbtn>
	<Calcbtn>6</Calcbtn>
	<Calcbtn use="oper">&minus;</Calcbtn>
	<Calcbtn>1</Calcbtn>
	<Calcbtn>2</Calcbtn>
	<Calcbtn>3</Calcbtn>
	<Calcbtn use="oper">&plus;</Calcbtn>
	<Calcbtn width="twowide">0</Calcbtn>
	<Calcbtn>.</Calcbtn>
	<Calcbtn use="oper">=</Calcbtn>
</div>
<style>
	@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@200&display=swap');
	.calc {
		font-size: var(--fontSize);
		display: inline-grid;
		justify-content: center;
		grid-template-columns: repeat(3, 3.875em) 4.06em;
		grid-template-rows: 5em repeat(5, 3.125em);
		margin: 0 auto;
		gap: 0.125em;
		background: #444;
		font-family: 'Work Sans', sans-serif;
	}
</style>

Almost done! Go back to the App.svelte tab, and replace all of its code with this:

<script>
	import Calculator from './Calculator.svelte'
</script>

<Calculator calcFontSize="16px" />

Hooray! If you have followed so far, you should see the calculator in the result window on the right. Clicking on the buttons doesn’t yet do anything other than change their background color, but we’ll take care of that soon enough.

In the script section of the Calculator component, we import its two subcomponents. Our HTML section has a div holding the Display and our Calcbtns. The statement bind:this={calc} sets the calc variable to point to this instance of the component—we will need that later, if we embed more than one Calculator instance on a page. The style section imports our font and creates the overall layout for the calculator.

One item of note is the line early in the style section that reads font-size: var(--fontSize); If you look at the last line of App.svelte, you will see that we set the value of --fontSize when we invoke the Calculator component. This allows us to set the overall size of our calculator independently each time it is invoked.

Now that we can see our calculator, let’s start getting it to work! Our first step is to set up communication between the buttons and the display. Rather than requiring all props to be sent up and down the chain to the top-level component, Svelte makes inter-component communication easy by using a store. A store is just an object with a subscribe method that lets interested components be notified whenever the store value changes. When we click buttons, the value on the display should change. Let’s set this up. Click the + at the right of the list of tabs, and rename the new tab to stores.js. Add the following two lines of code to it:

import { writable } from 'svelte/store';
export const display = writable(0);

This variable, display, will hold a numeric value to be shown on the display. Now the Display component needs to update the display’s contents whenever they change. We’ll start with a simple implementation of toDispString. Replace the contents of the script tags with the following:

	import { display } from './stores.js';
	let fontSize = "3.2em"
	const toDispString = (val) => {
		if (val == 0) return "0";
		return String(val);
	}

Next replace the div with the following:

<div style = "font-size: { fontSize }">
	{ toDispString($display) }
</div>

The dollar sign in front of the variable display is a Svelte feature saying that whenever the store value prefixed with the $ changes value, its containing statment will be rerun. Now, we just have to update the store when we click on the calculator buttons.

Let’s start by handling entering a number. A number will consist of a series of digits, optionally followed by a decimal point and another series of digits. If a second decimal point is entered during this process, we will ignore it. We’ll use a variable, inDecimal, to show whether we are entering digits beyond a decimal point and how many digits past it we are. To support this count, we will create a couple of module-level variables. Every time a button is pressed, any variables in our script section will be reinitialized, but variables in a module context hold their values throughout invocation, like static variables in a C++ class. At the top of the file, enter the following:

<script context="module">
	 let lastBtn = ""
	 let inDecimal = 0;
</scrìpt>

Change the first line of the HTML to call a function when a button is clicked:

<button class="{width} {use}" on:click={(a) => calcClick(a)}>

Then replace the contents of the script section with this code:

	export let width = "";
	export let use = "number";
	import { display } from './stores.js';
	const calcClick = (a) => {
		const btn = a.target.innerHTML;
		if (use == "plusminus") { 	// +/- key 
			display.set(-$display)
			return(0);
		}
		if (btn == "%") { 	
			display.set($display / 100)
			return(0);
		} 
		if ("0" <= btn && btn <= "9") {
			if (lastBtn != "number" ) {
				display.set(0);
			}
			if (inDecimal == 1) {
				display.set(Number(String($display) + "." + btn))
				++inDecimal
			} else {
				if (inDecimal) {
					display.set(Number(String($display) + btn))
					++inDecimal		
				} else {
					display.set($display * 10 + Number(btn))
				}
			}
			lastBtn = "number"			
		} else {
			switch (btn) {
			case ".":		
				if (inDecimal == 0) {
					inDecimal = 1;
				}
				break
			}
		}
	}

We have imported display from stores.js so we can set its value, which will lead Display.svelte to change the content of the display. We set it by calling display.set and passing the numeric value to be displayed. At this point, you can type numbers into the calculator, like 123.45, and see them appear. Progress! However, we can also see a bug. Start typing in digits, and by the time you get to ten digits, the number runs off the display! Let’s add some code to Display.svelte to deal with this. If numbers get long, we’ll shrink their size. We’ll round off digits to the right of the decimal place that won’t fit, and we’ll use scientific notation for numbers too large to fit. Go to the Display.svelte tab and replace the script section with the following:

	import { display } from './stores.js';
	let rounded;
	let fontSize = "3.2em"
	let maxDigits = 13;	// how many digits can display show
	const toDispString = (val) => {
		if (val == 0) return "0";
		let leftDigits = Math.max(Math.floor(Math.log10(val)), 0) + 1;
		if (leftDigits > 10) {
			return val.toExponential(8);
		}
		if (maxDigits > leftDigits) {
			rounded = val.toFixed(maxDigits - leftDigits)
		} else {
			rounded = val
		}
		let dispString = Number(rounded).toLocaleString("en-US", 
			{ maximumSignificantDigits: 12 });
		let digits = dispString.split("").filter(digit => digit >= "0" && digit <= "9")
		fontSize =  digits.length > 8 ? "2.1em" : "3.2em";
		return dispString;
	}

By calling toLocaleString, we not only limit maximumSignificantDigits, but also get commas in numbers four or more digits to the left of the decimal point.

Now l’t’s try adding some operators, so we can add, subtract, multiply, divide, and see the result of our calculation with the equal button. Head back to Calcbtn.svelte, and let’s think this through.

Imagine someone enters 4 + 5 × 6 = into the calculator. What should happen? Some calculators interpret this mathematically, and give multiplication higher priority than addition, so they show 34. But most calculators function sequentially. When the user enters the × symbol, the display shows the evaluation of 4 + 5 (i.e. 9), then the 6 is multiplied by 9, giving 54. We will take this approach. This means pressing a function key carries out any pending operation and displays the result, as well as saving itself to apply to the next operand.

Begin by adding these two lines of code to Calcbtn.svelte after let lastBtn = "":

	 let lastOper = ""
	 let operand = ""

Now add the following code after the break statement near the bottom of the script section:

			case "AC":
				lastOper = "" // fall through!	
			case "C":
				display.set(0)
				lastBtn = "number"
				a.target.innerHTML = "AC"
				inDecimal = 0
				break
			case "+":
			case "\u2212":	// Minus
			case "\u00D7": // Multiply
			case "\u00F7":	// Divide
			case "=":
			// dispatch('func', {
			// 	symbol: btn,
			// });
				switch (lastOper) {
					case "":
						operand = $display
						break;
					case "+":
						operand += $display
						break;
					case "\u2212":	// Minus
						operand -= $display
						break;
					case "\u00D7":	// Multiply
						operand *= $display
						break;
					case "\u00F7":	// Divide
						operand /= $display
						break;
				}
				display.set(operand)
				lastBtn = "operator"
				lastOper = btn;
				inDecimal = 0
				if (btn === "=") {
					lastOper = "";
					operand = 0;
				}
				break;

Now try out your calculator. It adds! It multiplies! It follows the rules we set out above for chained calculations. However, we still have a few loose ends to tie up. The AC button should work as follows: It begins as an AC button, but should change to C when a number is entered. If the C (Clear) button is pressed, it clears the currently entered number, but also changes its symbol to AC (All Clear), and if pressed in that state, it clears all pending data. To support this, we will dispatch a custom event from Calcbtn.svelte that will trigger an event handler in our top level component. To do this, add this line near the top of the script section of Calcbtn.svelte: import { createEventDispatcher } from 'svelte';

After the first if statement in calcClick, send the event:

		if ("0" <= btn && btn <= "9") {
			dispatch('ac', {
				symbol: btn,
			});

We modify each of the number buttons and the decimal point button in App.svelte to react to this ac event, with an on statement: <Calcbtn on:ac={setClear}>9</Calcbtn> Then in the App.svelte tab, we add this code:

	const setClear = (event) => {
		let selected = event.detail.symbol;
		let opers = document.getElementsByClassName('fn');
		opers[0].innerHTML = 'C';
	};

Another loose end: when the user presses the + key in the previous example and then goes on to enter the 5, we want the plus key to continue to appear pressed, as a reminder of the operation being performed. In order to do this, Calcbtn will send another custom event to the top-level component. Those three lines of code are already present below the line case "=": so just uncomment them. Next, in the top-level component, modify each of the Calcbtn statements with user="oper" like this: <Calcbtn use="oper" on:func={setOperColor}>+</Calcbtn> and add the function to the script section above:

  const setOperColor = (event) => {
		let selected = event.detail.symbol;
		if (selected === "=")
			selected = "";
		let opers = document.getElementsByClassName("oper");
		for(var i = 0, length = opers.length; i < length; i++) {
				opers[i].classList.remove("held")
			if (opers[i].innerHTML === selected) {
					opers[i].classList.add("held")
			}
		}
	}

Well, that about wraps it up! There are certainly a few bugs left, and features that could be added (like handling keystroke entry), but we’ve built a functional calculator. Now go to the Embed post to learn how to embed this component in a conventional web page.

Next→