DEV Community

MartinJ
MartinJ

Posted on • Edited on

NgSysV2-3.1: Creating a serious Svelte IS: Routes, Layouts and Components

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 design of a complex webapp raises some tricky issues for its developer. Complexity must somehow be marshalled into patterns that are comprehensible to the webapp's users. Part of the answer will be the creation of consistent patterns, but the developer may then struggle to ensure that consistency is maintained within these patterns. This post is all about the arrangements that Sveltekit has provided to make a webapp developer's task easier.

  • Routes - making your webapp comprehensible

The simple "Inventory Maintenance" webapp created in the last section of this series has a silly design - a single address displays a list of current products and adds a button that launches a popup to add new ones. This is both confusing and restrictive. In the real world, a system like this would need to serve two entirely different groups of users - readers on the one hand and maintainers on the other. It would be much better if each of these two bits of functionality could be addressed separately as in, say:

In the webapp design world, addresses like these are referred to as routes (they represent the "route" which users follow to reach content). The content displayed by following a route is referred to as a page

Webapp users love routes. Once each functional component of webapp acquires an independent existence it becomes much easier to visualise the structure and purpose of the information system. Route designs dovetail nicely with the browser's "breadcrumb-trail" arrangements that enable you to navigate up and down the branches of a webapp's page hierarchy. Also, if a page is frequently accessed, users can save its route as a bookmark, enabling them to return to it easily by clicking the saved link. Designers, too, like this arrangement because it creates a clear structure wherein, for example, they can deliver security arrangements that leave some application areas publicly available whereas others are protected by login.

As you'll see in a moment, Svelte fixes this problem by baking the webapp route structure into the webapp project's file structure.

  • Layouts - delivering visual consistency

Users also find it helpful if families of related pages employ common elements such as a standard menu bar and page trailer. This gives the page family a consistent appearance and makes it easier for users to navigate the webapp. But in the past, developers have struggled to keep these common elements in agreement.

Svelte fixes this problem by arranging a mechanism to source common elements from central locations. In Svelte, these are called "layouts".

  • Components - delivering code consistency

Finally, developers grappling with the challenge of maintaining consistency and clarity within the codebase of a large project will feel happier if bits of it that need to operate identically are forced to do so because they use a shared component. Svelte is from top to bottom, a component-based architecture.

2. Routing in Svelte

SvelteKit uses your project's filing structure to define the required page structure. If, for example, you want a mywebapp/products-display page, you need to create a project folder called src/routes/products-display with a +page.svelte file inside it. Once this arrangement is in place, your webapp delivers a separate physical page for each route.

Here's a sample source folder structure for a SvelteKit project. To make things a little more realistic here I've added a "product-details" sub-folder to the "products-display" folder. The purpose of this is to deliver more comprehensive information on a product listed by the "products-display" route:

Sample source folder structure

Hang on though. Isn't this going to be a bit confusing? Lots of files all called +page.svelte? Yes - at least at first sight. But it's not so difficult to discipline yourself to check for the owner of a +page.svelte before you dive in and start editing. And once you've developed a SvelteKit project or two you'll begin to appreciate the value of the convention in declaring the purpose of a file. As you'll see later, there are quite a few variations, and these give the designer total control over the webapp's structure with consequent benefits for both its responsiveness and its maintainability.

Try out these folder and file-naming arrangements in your current svelte-dev project by creating new products-display and products-display/product-details folders containing the following +page.svelte files:

// products-display/+page.svelte - remove before running
<div style="text-align: center;">
    <a href="/products-display/product-details">View Product Details for Product 1</a>
</div>
Enter fullscreen mode Exit fullscreen mode
// products-display/product-details/+page.svelte  - remove before running
<div style="text-align: center;">
    <span>Here are the Product Details for Product 1</span>
</div>
Enter fullscreen mode Exit fullscreen mode

Now run your dev server, start the webapp and change the root address to localhost:5173/products-display (making allowance for any variation in port number, of course). It should look like this:

Products page screen

Now, what happens when you click the link? Unsurprisingly, the display changes to this:

Product-details page screen

Note that the page address has also changed. The link has transferred you to a new address - localhost:5173/products-display/product-details. Try using the browser's navigation buttons to return to the original page. Yes, everything works exactly as you would expect. Each of these +page.svelte files has created a navigation route based on its position in the development project hierarchy. You can also bookmark these. All you've had to do to achieve this is to submit yourself to Svelte's file naming conventions. I'd say that's a price well worth paying!

In the real world, the "Products display -> Product details" arrangement would need to be dynamic. Currently, there's nothing in the product-details/+page.svelte file to enable it to respond to a request for details on a particular product. In practice, the <a> links on the products page would carry information to enable them to specify which particular product they were interested in, and the product-details page would react accordingly. I'll describe how Svelte would handle this when I cover "components" later in this post.

3. Layouts in Svelte

The "home page" on a company's website is usually an elaborate showcase featuring some sort of header banner alongside a logo and contact details. Customers would probably find it useful to see some of this information on subordinate pages too.

Although it would be perfectly possible to duplicate these details, this would be very undesirable from a maintenance point of view.

Svelte's "layout" facility provides a neat solution to the problem.

Create a +layout.svelte file in the products-display folder containing the following code:

// products-display/+layout.svelte - remove before running
<header>
    <h3 style="display:flex; justify-content:space-between">
        <a href="/about">About</a>
        <span>Magical Products Company</span>
        <a href="/inventory_search">Search</a>
    </h3>
</header>

<slot></slot>

<trailer>
    <div style="text-align: center; margin: 3rem; font-weight: bold; ">
        <span>© 2024 Magical Products Company</span>
    </div>
</trailer>
Enter fullscreen mode Exit fullscreen mode

Now check out the products-display and products-display/product-details pages in your browser. Magic! You'll find that the header and trailer layouts have now been wrapped automatically around both pages. The localhost:5173/products-display page, for example, should now look like:

Layout page screen

The <slot></slot> element within the +layout.svelte file defines where the content of subject pages will be inserted. See +layout in Svelte docs for more information about the arrangement. When a layout is positioned at the top of a tree of pages, it is applied to all subordinate pages.

Svelte layouts are especially handy for building a "nav" (navigation) bar. This is the array of links or buttons used in many applications to provide a standard, centralized menu for accessing site pages. It is usually positioned at the head of the display, but you could just as easily style the nav bar to appear at the side.

4. Components in Svelte

While the standard "header/footer" layout arrangement is useful, there are many other situations where you might want to introduce a standard piece of HTML into the body of a page. Repeating yourself is never a good idea in coding and the "DRY" (don't repeat yourself) principle applies just as much to HTML as to Javascript.

A Svelte component is a self-contained unit of code that combines HTML, CSS, and JavaScript to create a piece of the user interface (UI). Typically, you would store this in a src/lib/MyComponent.svelte file. Here "MyComponent" would be some appropriate meaningful expression of the component's purpose.

Note the capitalisation of the first character of the component's filename and the absence of hyphenation. This isn't mandatory, but is a useful naming convention that makes them stand out as programmatic elements when referenced in a parent page's template section.

Just as with a Javascript function, a Svelte component will usually need to be provided with parameters to enable it to achieve its purpose so, for example, a UserProfile.svelte might be referenced in the HTML body section of a +page.svelte file with a statement like:

<UserProfile userId={userId} />
Enter fullscreen mode Exit fullscreen mode

Here, the userId parameter will have been declared by the +page.svelte file with a let userId statement and seeded with a value. The component will also have been imported into +page.svelte, in a <script> section that looks like this:

<script>
    import { UserProfile} from "$lib/UserProfile.svelte";
    let userId;
<script/>
Enter fullscreen mode Exit fullscreen mode

Note the "$" shortcut used in the import declaration. Svelte works out the actual route automatically, saving you thejb of working out all the conventional "./" and "//" relative location designators.

Meanwhile, in the UserProfile.svelte file, the parameter will have been declared as a prop (ie a parameter) with an export statement

<script>
    export userId;
<script/>
Enter fullscreen mode Exit fullscreen mode

The UserProfile.svelte file, (which in all other respects looks just like any other +page.svelte file), is now free to reference the userId parameter both in its <script> and template sections.

You'll recall this is exactly the same way a +page.svelte file gets its data from an associated +page.server.js file. This is because all +page.svelte files are themselves components.

And now that you know this, you'll maybe guess that component structures open a way to write products-display pages that link programmatically to product-details pages.

First, SvelteKit replaces the src/routes/products-display/product-detail route with a new, dynamic "parameterised" src/routes/products-display/[productNumber]/ route. This enables it to reference individual +page.svelte product-details pages from "anchor" links such as <a href="/products-display/1">View Product 1</a>. When the SvelteKit router is asked to parse this address it passes it to the following modified version of the original product-details +page.svelte file:

//  src/routes/products-display/[productNumber]/+page.svelte - remove before running
<script>
  export let data;
</script>

<div style="text-align: center;">
  <span>Here's the Product Details for Product { data.props.productNumber}</span>
</div>
Enter fullscreen mode Exit fullscreen mode

As you'll see, this is looking for a load() function to supply the productNumber parameter. You've seen this before (see Post 2.3, so you'll know that this needs to be supplied by a +page.server.js file. Here it is:

// src/routes/products-display/[productNumber]/+page.server.js - remove before running
export async function load(event) {
    const productNumber = event.params.productNumber;
    return {
      props: {
        productNumber
      }
    };
  }
Enter fullscreen mode Exit fullscreen mode

This digs the productNumber parameter out of the SvelteKit event object (the complex of properties and methods that follows a transaction throughout its lifecycle) and returns it to the +page.svelte file.

That was fun! Just to tie things up neatly, here is a version of products-display/+page.svelte that lists all products in the collection as anchor tags that link them dynamically to product-details pages:

// src/routes/products-display/+page.svelte - remove before running
<script>
    export let data;
</script>

<div style="text-align: center">
    <h3>Current Product Numbers</h3>

    {#each data.products as product}
        <!-- display each anchor on a separate line-->
         <div  style = "margin-top: .35rem;">
        <a href="/products-display/{product.productNumber}"
            >View Detail for Product {product.productNumber}</a
        >
    </div>
    {/each}
</div>
Enter fullscreen mode Exit fullscreen mode

I won't go into full details for the remaining files. The src/routes/products-display/+page.server.js file that provides the load() function for the above is perfectly standard. But the load() function for the dynamic route's src/routes/products-display/[productNumber]/+page.svelte file now has to use the productNumber value from the event.params object to get the associated productDetails field from the database. Here are the essentials of the code for this:

// src/routes/products-display/[productNumber]/+page.svelte - fragment
export async function load(event) {
  const productNumber = parseInt(event.params.productNumber, 10);

  // Now that you have the product number, you can fetch the product details from the database
  const productsCollRef = collection(db, "products");
  const productsQuery = query(productsCollRef, where("productNumber", "==", productNumber));
  const productsSnapshot = await getDocs(productsQuery);
  const productDetails = productsSnapshot.docs[0].data().productDetails;
  return {
    productNumber: productNumber, productDetails: productDetails
  };
}
Enter fullscreen mode Exit fullscreen mode

5. Beneath the hood - Svelte "rendering" and "pre-rendering" internals

The Svelte +page.svelte and +page.server.js file types described above deliver a "default" design in which pages are built on the server and initially displayed to the client as pure HTML. Here, in a process Svelte calls "hydration", javascript is added to make the page interactive. This, for example, would be the point at which a button's "on:click" capabilities are added. Finally, "state" data used on the server to generate the initial HTML is replayed to ensure that everything is still consistent.

Overall this arrangement:

  • generates a near-instant initial response when a page is requested by the browser
  • ensures that sensitive application logic, vulnerable form validation logic and valuable security keys can be hidden inside +page.server.js files
  • makes the most of the (generally) superior processing capability of the server
  • encourages better "indexing" by search engines. While search engines have got better in recent years at indexing content that was rendered with client-side JavaScript, server-side rendered content is indexed more frequently and reliably.

That said, there will always be cases where improvements might be made to the default arrangement, and Svelte is very helpful in providing ways of enabling this.

For example, if the data accessed by a +page.server.js isn't changing very much (perhaps not even changing at all) you can instrument Svelte to pre-render selected pages when the webapp is prepared for "deployment" (see Post 4.3)

Additionally, there may be cases when client-side processing would be a more practical way of loading the data for a +page.svelte file. Perhaps it doesn’t matter that the data fetch happens asynchronously after the initial page load or if the data can be fetched via APIs that don’t require server-side security. In this case, Svelte offers a +page.js file to export a load function. Unlike a +page.server.js file, a +page.js file can be inspected in the browser

For more information about this and other specialist arrangements, please refer to the SvelteKit Page Options docs

Top comments (0)