DEV Community

MartinJ
MartinJ

Posted on

NgSysV2-2.2: Creating a simple Reactive Svelte webapp

This post series is indexed at NgateSystems.com. You'll find a super-useful keyword search facility there too.

Last reviewed: Nov '24

1. Introduction

The previous post created a Svelte webapp made up entirely of simple HTML. It didn't contain any Javascript and thus didn't use any of Svelte's sophisticated dynamic HTML generation facilities. This post will only scratch the surface of Svelte features but should give you a feel for the power of the language.

2. State and Reactivity

Viewed objectively, a webapp is a complex hierarchy of HTML pages, popups, tables, forms and buttons. This dizzying assembly of visual elements is "informed" by data. Tables are inflated with JavaScript array content. Popups appear and vanish at the behest of flag fields. Forms blossom with data when "poked" by mouse and keyboard input. In short - it's complicated. How can some sort of order be established over this situation?

Collectively, the body of data defining the appearance of a webapp at any moment in time is referred to as the webapp's state. Much effort has been expended on software development technology designed to make it easier to write webapps that react efficiently when state changes. Platforms like Svelte are expressly designed to deliver good reactivity.

Let's look at Svelte's approach to the definition of State and its implementation of Reactivity.

2.1 A simple reactive webapp

A common feature on a web page is a pop-up window that is displayed (usually annoyingly) when you start it up, but which then, obligingly, disappears when you click on it. Let's see how Svelte would let you use State and Reactivity to implement this in a webapp.

In Javascript, you need to define a variable before you can use it. So for example, a command like

console.log(myDataItem);
Enter fullscreen mode Exit fullscreen mode

will throw an error unless somewhere earlier in the code you had defined it using a statement like

let myDataItem;
Enter fullscreen mode Exit fullscreen mode

As an aside, you'll find it useful to have access to a "playground" that lets you quickly try out bits of JavaScript like this. The "inspect" view of any browser page provides one such playground - open the inspector on a random page and select the "console" tab on its menu bar. Alternatively, you might try the javascript console at PlayCode.

There's lots to say about JavaScript's let keyword (try asking chatGPT about "scope") but let has an additional significance in Svelte because this is how you define State. In a Svelte program, any variable defined with let becomes part of the program's State.

So what? Well, I said earlier that State variables - variables that define the appearance of a webapp - are "reactive". This means that when their value changes, the appearance of the webapp is directed automatically to change accordingly. Suppose you use a popupVisible boolean variable to define the state of the popup. Could Svelte - a reactive platform - use the variable's value to determine whether or not the popup is visible? Let's try.

Here's the code I propose to use. I'll explain what it does in a minute:

//src/routes/+page.svelte - remove this line before running
<script>
    let popupVisible = true;

    function togglePopup() {
        popupVisible = !popupVisible;
    }
</script>

<div>
    {#if popupVisible}
        <button
            style="height: 6rem; width: 25rem; background-color: blue"
            on:click={togglePopup}>
        </button>
    {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

The code here is divided into two sections. The upper "script" section, defined by the <script></script> tags, declares a popupVisible javascript variable and a togglePopup() function. The lower "template" section, specifies HTML "moderated" by a Svelte {#if} {/if} logic block. Svelte code can reference variables and functions defined in the <script> section to guide HTML code generation. References are enclosed by curly {} brackets, except when they're used directly within a logic block - see Svelte docs at Basic Markup and Logic Blocks for full details.

The code displayed above is very crude. A "pop-up" is normally defined by a <div> tag formatted with a border and some positioning CSS. Here I've used a <button> tag - something that would be more commonly used to define the "submit" element on a form. I've done this simply because it enables me to avoid some technicalities that might distract you. I've coloured the button "popup" blue so that you can see it.

If you still have a copy of the skeleton svelte-dev Svelte project created in Post 2.1, try using this code to replace the entire content of src/routes/+page.svelte. When you open a terminal on the project and start the dev server as before, your browser should now leap into life and display the screen shown below:

Graphic to illustrate Svelte reactivity

The blue rectangle is the "pop-up". It is displayed at startup because at this point the popupVisible variable is set to true and the Svelte logic driving the page's HTML generation is instructed to create a <button> tag in this case.

Now try clicking the pop-up. Magic! It disappears. This is because the <button> tag specifying the popup contains an onClick clause that makes a click on the element run the togglePopup function. This in turn sets the popupVisible variable to false.

I said earlier that changing a Svelte "state" variable causes Svelte to refresh the screen by re-running the page's template section. So, the template section re-runs and now, since the value of popupVisible is false, the <button> code displaying the pop-up is bypassed leaving an empty page.

Take a moment now to let this sink in. If you'd still been working with the primitive DOM-manipulation code I introduced in Post 1.1 this effect could only have been achieved by writing a good deal of fairly technical (and probably rather inefficient) low-level code. Here, you've just had to change the value of a variable. Sveltekit has handled the intricate consequences automatically (and, rest assured, efficiently).

In short, Svelte gives you a high-level language that lets you leave the tedious details of browser screen management to the framework.

In October '24, Svelte 5 introduced a series of refinements to its reactivity-definition arrangements. The let construct described above still works fine for everything in this post series, but a new runes concept is now available to handle more complex requirements. See What are Runes for details.

2.2 Data Input

Let's make the pop-up a bit more interesting. Imagine that you had the job of maintaining a "products" list for a manufacturing company. You'd want a utility that displays the current list and enables you to add new products. In practice, of course, you'd also want it to be able to edit and delete entries, but let's keep things simple for now.

Have a look at the following code. It will probably stretch your JavaScript knowledge but the internal comments should help.

//src/routes/+page.svelte - remove this line before running
<script>
    let popupVisible = false;
    let newProductNumber = "";
    let products = []; // An array of product objects {productNumber: productNumber}
</script>

<div style="text-align: center">

    <h1>Inventory Maintenance Page</h1>

    {#if !popupVisible}  <!-- the start position - popup not visible-->

        <p>Currently-registered Product Numbers:</p>

        {#each products as product}  <!-- display the current list of  product numbers-->

            <p style="margin: 0">{product.productNumber}</p>
        {/each}
        <button
            style="margin-top: 1rem"
            on:click={() => {
                // a function to toggle the popup on
                popupVisible = true;
            }}
            >Add Another Product
        </button>

    {:else}   <!-- display the product registration form-->

        <div style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;">
            <form>
                <p>Product Registration Form</p>
                <label>
                    Product Number
                    <input bind:value={newProductNumber} />
                </label>
                <button
                    type="button"
                    on:click={() => {
                        // a function to add the product number to the array and hide the popup
                        products.push({ productNumber: newProductNumber });
                        popupVisible = false;
                        newProductNumber = ""; // Clear the input field
                    }}
                    >Register
                </button>
            </form>

        </div>
    {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

The "template" section here begins by checking the value of popupVisible. If this is false (which it will be on startup because the variable has just been initialised in the "script" section), the webapp displays the webapp's current list of "products" (an array of productNumbers stored in a products array).

The code now move on and displays an "Add another Product"button - a normal one now, not the freakish version used in the previous example. But, as before, it has an associated on:click function, and this one sets the value of popupVisible to true.

What will happen if the button is clicked? Why, since popupVisible is a reactive variable, the webapp will refresh its screen.

This time, the products-display section of the form is ignored and control will move straight to the second part of the template section.

Here, you'll see something more like a conventional popup. This one uses a <form> tag within a container div> to collect input for a newProductNumber state variable. A special Svelte "bind" qualifier" is employed here here to synchronize newProductNumber with the user's keyboard typing. Every time the user types a character into the form's <input> field the newProductNumber is updated.

When the user finally clicks the form's "Register" button, its on:click function is thus able to push a fully-updated newProductNumber into the products array and reset the popupVisible field.

Finally, since popupVisible is a reactive variable, the "template" section of the code re-runs and the popup is replaced by the updated list of product numbers.

One or two other things may puzzle you. In the previous example, anon:click qualifier referenced a function defined in the webapp's <script> section. In this new version of +page.svelte, however, the function is defined explicitly within the <button> field's HTML spec. Both approaches are perfectly valid. The version used here probably looks a bit strange to you, but it is a widely-used arrangement that has the advantage that the function's logic is defined at its point of invocation. This makes it much easier to see what's going on.

For reference, the expression:
"() => { .... }" is saying "here's a function defined by the following JavaScript statements: "{ .... }". There are many variants. Ask chatGPT to give you a tutorial on "arrow" functions .

Moving on, you'll also note that I've been much more adventurous with my "styling". There's a heading, for example, and everything is nicely centred (CSS "inheritance" ensures that the style="text-align: center" of the enclosing <div> is also applied to everything within the <div> block). Then there's the horrific style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;" attribute applied to the <div> tag enclosing the Registration form. Ask chatGPT for a description of what this does. A tutorial on the "border box" model might be handy too.

But that's enough background. Let's see the webapp in action. Paste the new code over the current content of your +page.svelte file, save it and (if necessary) open a terminal on your project and restart your dev server with the npm run dev -- --open command. Here's what you should see in your browser::

Graphic showing the appearance of the Inventory Maintenance Home page

And here's what you should see when you click the button:

Graphic showing the appearance of the Inventory Maintenance popup

Since there's no validation on the newProductNumber field, you will find that you can enter characters as well as numbers - we'll fix this in a future post. Even so, when you click the "Register" button, you should see the popup replaced by the original Inventory display page, with your new product "number" added to the list.

This scrap of Svelte is almost starting to look like an information system!

3. Onward and Upward

This post has given you a brief glimpse of the key concepts embodied in the Svelte language. Unfortunately, there's a snag. As I'm sure you will have noticed, the "Inventory" example you've just seen is effectively useless because every time you close the webapp its product list disappears! The next step, therefore, is to introduce you to Google's Firestore database technology. This will let you create persistent storage on a server host.

Postscript (a): When things go wrong - Investigating Layout issues with the Chrome Inspector

"When things go wrong" in Post 2.1 described how to handle some of the grosser problems you'll encounter when developing a webapp. But now that you're working at a more advanced level you need a more sophisticated tool. This section introduces you to the "Chrome Inspector", an invaluable aid when correcting problems with your webapp's screen layout and logic errors in your JavaScript (and much more besides, as you'll see later in this series).

The Inspector is launched on a webapp Page by right-clicking on the page and selecting the "Inspect" option. Here's an example of its output:

Screenshot illustrating the use of the Chrome Inspector element-debugging tools

In this screenshot, I have:

  • Launched the Inspector, as above. wThe Inspection Window is now hiding the bottom half of the webapp window. This can be inconvenient because the Inspector may now be hiding some webapp elements that I want to inspect! This is partially handled by "sizing" the inspector window by clicking and dragging on its upper border. Alternatively, you can the "Dock Side" tool under the triple-dot icon on the extreme right of the menu to dock the Inspector to the side of the screen.
  • Selected the "Elements" tab from the Inspector's menu bar
  • Expanded the <body> and <div> lines in the HTML tag hierarchy displayed in the left-hand inspection panel and moused over the <h1> tag showing the webapp's heading

If you try this yourself, you'll note that the heading has acquired some coloured graphics. These show you the size and position of the margin, border and padding that have been added to your text. The right-hand inspection panel provides more detail. If you click the computed tab in its menu, you'll get a graphic that explains the colour coding and detailed sizings for the various elements. If you click the "styles" tab you'll see details of the styles attached to the text. The first set of entries in the list confirms those styles set explicitly with the tag's style= clause (none in this case). After that, you'll see any styles inherited in other ways.

The beauty of this panel is that you can use the inspector to see the effect of changing and adding styles. For example, suppose you wanted to try out a red font on the heading, click somewhere in the element.style entry at the top of the panel and enter color: red. The heading, obligingly, now turns red. Note the opportunity to use autocompletion to try out different colours.

I don't want to labour the point, but I think you should be starting to see how this facility can give you a precise and intuitive tool for investigating and fixing layout issues. For more information check out Google's documentation on the inspector at DevTools.

Be prepared to spend quite some time on this. The Inspector is a complex tool so it will be a while before you can use it confidently. But this will be time well spent. The Inspector has certainly saved me countless hours of blind experimentation with source-code settings. The inspector's graphic display makes visible all the hidden properties of element widths, margins and padding.

Postcript (b): Investigating Logic issues with the Chrome Inspector

The Inspector can also help you find logic errors by enabling you to view your source code and set "breakpoints" on lines where you would like execution to halt. After refreshing the webapp (with the Inspector still open), you can inspect field values at each breakpoint. Here's a screenshot of the Inspector in action:

Screenshot illustrating the use of the Chrome Inspector's source-debugging tools

In this screenshot, I've:

  • Launched the Inspector as before.
  • Clicked the "Sources" tab above the left-hand panel and expanded the source hierarchy for localhost in the page view to enable me to click on the italicised +page.svelte entry for my route in the src/routes line.
  • Noted that the right-hand panel now displays the source for +page.svelte and clicked on the let popupVisible = false; line to set a breakpoint (confirmed by a blue highlight on its line number).
  • Refreshed the page in the browser and noted that the webapp is now displaying a "paused in Debugger Message" that includes a "Resume execution" button and a "Step over next function call" button. You'll also see that the let popupVisible = false; line is now highlighted

The effect of all this has been to put the webapp into "code debug" mode. Right now, I'm debugging the <script> section of the webapp, which is a bit pointless since it only runs once and you know exactly what it does. But let's go through the motions because they'll help you get a feel for what the debugger can do for you in JavaScript source code.

Currently, the webapp is halted before execution of the first statement and so hasn't done anything useful. But advance to the next statement by clicking the "Step over next" button and note that the highlight has now moved to the second line. Mouse over the popupVisible field in the first line and note that a tooltip is now displayed showing its current value - false, of course. Repeat the trick to confirm the settings of newProductNumber and products.

The value of the Inspector is more clearly seen if you set breakpoints in Javascript functions defined in the <script> section. These run every time the function is referenced and the Inspector is great for tracking progress through these.

Unfortunately, debugging with breakpoints in the "template" section of a +page.svelte file isn't so straightforward. Here, the code runs every time the page re-renders following a change in a reactive variable, but the code that the "sources" tab displayed here (ie the original Sveltekit code of your +page.svelte) is not what is now actually running. The original code has been replaced by Svelte scripts generated by the local server. Consequently, there's only limited scope for you to get information about what is happening.

If you want to know more about the rendering process and why your +page. svelte file behaves like this in the Inspector, skip across briefly to the end of Post 4.4. But, be warned, it's complicated!

In my experience, you can usually set breakpoints on Sveltekit statements and thus track the path taken during a re-render. But these won't display field values.

You can, however, usefully set breakpoint in embedded functions. For example, try setting a breakpoint in the products.push({productNumber: newProductNumber}); line for the "Register" button. Note that the entire block of <button> code gets highlighted (in grey) reminding you that this is an "anonymous function" (get chatGPT to explain this to you sometime). Now rerun the webapp and observe how the breakpoint behaves just as it did in the <script> section. Having asked the webapp to register a newProductNumber of 123 and clicked the "Register" button you'll see the webapp halt on the breakpointed line. "Step over next function call" and mouse over the products.push({productNumber: newProductNumber}); line. You'll note that it now shows you that the products array now contains the single value 123.

If all else fails and you still need to check the value of a variable in the template section, the equivalent of a "console.log" for variable "x" would be a <p>Value of x is {x}</p> statement.

Note that the buttons on the "Paused in debugger" line are duplicated in the Inspector menu bar and are supplemented by a "Step into next function" button to enable you to traverse nested functions.

As you'll quickly realise, breakpoint settings persist in webapp files. Breakpointed lines are toggled off by clicking them again. A "right-click" in the entry for a file in the Inspector's "Breakpoints" panel on the right of the screen will reveal a master toggle to remove all breakpoints.

Finally. note that when a webapp hits a major problem and "crashes", opening the Inspector will reveal a red "error count" at the right-hand end of its menu bar. When this happens, details of the problem can be viewed in the Inspector's "console" tab.

Top comments (0)