Lab 5: Svelte II (Loading Data & Reactivity)
In this lab, we will learn:
- How to leverage Svelte’s reactivity to build interactive UIs
- How to load data from an API and display it in a Svelte app
Table of contents
- Lab 5: Svelte II (Loading Data & Reactivity)
- Check-off
- Lab 5 Rubric
- Slides
- Step 1: Creating a layout for UI shared across pages
- Step 1.1: Creating a layout component
- Step 1.2: Adding a navigation bar to the layout
- Step 1.3: Adding a class to the current page link
- Step 1.4: Adding
target="_blank"
to external links - Step 1.5: Moving CSS specific to the Navigation bar to the layout component
- Step 1.6: Importing global CSS via the layout component (Optional)
- Step 2: Port the theme switcher to Svelte
- Step 2.1: Porting the theme switcher HTML and CSS to our layout
- Step 2.2: Bind color scheme to a variable
- Step 2.3: Apply the color scheme to the
<html>
element - Step 2.4: Reading the color scheme from local storage
- Step 2.5: Saving the color scheme to local storage
- Step 2.6: Preventing FOUC (Optional)
- Step 3: Loading data from an API
- Step 4: Update your project data
- Resources
Check-off
You need to come to TA Office Hours to get checked off for this lab (any of them, no appointment needed), OR submit your work asynchronously by filling out this form.
If you choose to submit your work asynchronously and have an incorrect or incomplete part of the lab, you will not receive any credit for the lab (we do not offer partial credit on labs). You may not resubmit this form nor ask for a synchronous check off for the same lab.
Lab 5 Rubric
To successfully complete this lab check-off, ensure your work meets all of the following requirements:
- Nav bar highlights the current page
- Theme switcher is implemented with Svelte
- GitHub stats are displayed on the home page with styling
Slides
Step 1: Creating a layout for UI shared across pages
In lab 4, for our nav bar, we were still using our JS code from Lab 3 to display the navigation bar. Not only would it be easier to manage this code in Svelte, but it would also fix a pretty annoying bug: when you navigate to a different page, the navigation bar does not update to reflect the current page. This is because behind the scenes, Svelte is actually using JS to update the content when you navigate to another page, so the code that updates the current page link is not being run.
There are two ways to do this We could create a Navbar
component and import it and use it in every page. However, that would be quite tedious.
There is actually a concept we have not yet covered called a layout component or “layout” for short. A layout is a component that wraps every page with the same content, so it’s very useful for things like headers, navigation bars, and footers. Svelte distinguishes layouts from other components by their file name, which is +layout.svelte
.
In fact, a larger website typically has multiple different layouts, many of which build on others. E.g. you could have a main layout for the whole website, and then a layout for blog posts.
But don’t we already have app.html
for that? Note that app.html
is not a component, so it does not provide any of the conveniences of components (expressions, scoping, etc.).
Step 1.1: Creating a layout component
Create a new file called +layout.svelte
in the src/routes
directory. Put YOLO <slot />
as its only content and save.
<slot />
is a special element that is replaced with the contents of a component. In the case of a layout component, the contents are the page contents.
Visit your website. Do you see the text “YOLO” at the top of every page? If so, great, the layout works! Now let’s do something more useful with it. Delete the “YOLO”, so that just <slot />
remains at the end of the file.
Step 1.2: Adding a navigation bar to the layout
Now that we’ve made sure we hooked everything up correctly, let’s start porting our navigation menu. In your +layout.svelte
file, add a <script>
element and define a variable with your pages. We’ll use the same array of objects:
let pages = [
{ url: "./", title: "Home" },
{ url: "./projects", title: "Projects" },
{ url: "./contact", title: "Contact" },
// add the rest of your pages here
];
Then, in the HTML portion of +layout.svelte
, add a <nav>
element with an {#each}
block inside it to iterate over the pages and create links for each:
Just like how order dictates position in HTML -> webpage, the same applies to the HTML portion of Svelte files. In this case, because <slot />
renders the page contents, we want this to be ordered AFTER our nav bar and any elements we are adding to the top of our page. Keep this in mind as you build out pages to be conscious of where elements (including <slot />
) should reside on your page.
<nav>
{#each pages as p}
<a href="{p.url}">{p.title}</a>
{/each}
</nav>
Comment out the code related to the old nav bar in global.js
so that we don’t see double nav bars. We won’t need our previously defined nav bar now that we are using Layouts.
Save and preview. You should now see your navigation bar in all its past glory.
Step 1.3: Adding a class to the current page link
While our navigation menu looks the same at first, there is no different styling for the current page (déjà vu?).
Let’s add that!
First, we need to import the $page
Svelte data store, which contains information about the current page. To access it, add this to the top of +layout.svelte
in the <script>
element:
import { page } from "$app/stores";
Note that even though we use $page
in expressions, we import it as page
(without the $
). $
usually indicates reactivity in Svelte, i.e. a value that when it changes, everything that references it updates to reflect the change.
Try it now: Add an expression anywhere in your +layout.svelte
component to print out info about the current page as a JSON object:
{
JSON.stringify($page)
}
It should look like this (make sure to remove it after you’re done):
Notice that we can compare $page.route.id
with the url
of each page to see if it’s the current page.
In the HTML, we can replace the current line defining a link to use the class:class-name
syntax to add a class to an element if a condition is true:
<a href={p.url} class:current={"." + $page.route.id === p.url}>
{p.title}
</a>
This will add the class current
to the link iff the condition is true.
We could have simply used the
class
attribute, but the code would have been a little awkward:<a href={p.url} class={"." + $page.route.id === p.url ? "current" : ""}> {p.title} </a>
Step 1.4: Adding target="_blank"
to external links
Another thing that is missing in our new navigation is that external links don’t open in a new tab. We can fix that by adding a target="_blank"
attribute to links that start with http
or https
.
To remove the attribute if the link is not external, we simply assign null
to it:
<a
href={p.url}
class:current={$page.route.id === p.url}
target={p.url.startsWith("http") ? "_blank" : null}
>
{p.title}
</a>
Step 1.5: Moving CSS specific to the Navigation bar to the layout component
Just like with the <Project>
component in Step 5.5, let’s move the CSS that only applies to the nav bar to be within our +layout.svelte
component by moving it from styles.css
to the bottom of +layout.svelte
in a <style>
element.
Step 1.6: Importing global CSS via the layout component (Optional)
Another benefit that the layout gives us is that we now import our global CSS via the layout to apply to every page, which means that you benefit from Svelte’s hot reloading, i.e. the feature that updates your website when you save changes without you having to refresh anything. This replaces the need to link every stylesheet in app.html
.
To make use of that, we need to move our style.css
from static
to src
. Then, in src/app.html
, we remove the <link>
element referencing styles.css
, since we’ll be importing it a different way.
Now, in routes/+layout.svelte
, in the <script>
element, add:
import "../style.css";
View your website and make sure everything works the same!
Step 2: Port the theme switcher to Svelte
Step 2.1: Porting the theme switcher HTML and CSS to our layout
Open the Dev Tools Console on your portfolio site, and copy the HTML associated the theme switcher. This is the HTML code that was generated from the global.js
code we wrote in Step 4.2 of Lab 3.
Paste this code into your +layout.svelte
component, and then delete or comment out (Cmd + /) the JS code that was generating that HTML in global.js
.
As a refresher, the code in
global.js
generating the HTML code is:document.body.insertAdjacentHTML( "afterbegin", ` <label class="color-scheme"> Theme: <select> <!-- TODO add <option> elements here --> </select> </label>` );
Add a <style>
element to your layout component if it doesn’t already have one. Then remove the CSS that styles the theme switcher from your style.css
file and paste it there.
Some styles you may need to import include for .color-scheme
(or the equivalent class name you created), label
, and select
styles.
Refresh and make sure everything still works.
Step 2.2: Bind color scheme to a variable
Add a <script>
element to your layout component if it doesn’t already have one, since we’ll be writing some JS in this step. Namely, we’ll add a variable to hold the color scheme. Let’s call it colorScheme
:
let colorScheme = "light dark";
The next step is to bind the colorScheme
variable to the value of the <select>
element.
In this context, bind means that the value of the variable will be automatically updated when the value of the <select>
changes, and vice versa.
To do this, we use a bind:value
directive on the <select>
element and set it to {colorScheme}
. We want to replace our current <select>
element in our theme switcher HTML to the Svelte syntax for directives, which is very similar to attributes:
<select bind:value={ colorScheme }>
This binds the value
property of the <select>
element to our colorScheme
variable. In fact, it does what is called double binding: the value of the variable will be updated when the value of the <select>
changes, and the value of the <select>
will be updated when the value of the variable changes.
Ensure that it works by printing a {colorScheme}
expression above the <select>
so you can see the value of colorScheme in plain text. It should change each time you select “light”, “dark”, or “auto” with your selector. Once you have ensured the value changes properly, you can remove that variable so the page doesn’t print it anymore.
Step 2.3: Apply the color scheme to the <html>
element
Now delete or comment out all of the theme switcher code in your global.js
. It’s time to move all of that logic to Svelte!
We have already seen how to use curly bracket expressions (i.e. { ... }
) to set attributes or element contents based on variables or expressions. But here, we need to set a CSS property on the root element (<html>
), which is not part of the Svelte component tree. If you recall, the <html>
element is part of the skeleton in src/app.html
, which is not a Svelte component and thus, cannot take expressions. What to do? Is it even possible?!
Fear not, of course it is! In fact, there are multiple ways to do this. You will find the one that seemed most straightforward and closest to our original code below, but feel free to ask about the others!
If you recall, in our original code we were doing this:
document.documentElement.style.setProperty("color-scheme", colorScheme);
Can’t we just copy this line of code wholesale? Unfortunately not. There is a bit of a wart here: Our Svelte code is first ran in Node.js to generate the static parts of our HTML, and the more dynamic parts make it to the browser. However, Node.js
has no document
object, let alone a document.documentElement
object. Try it for yourself: add this to your <script>
element:
console.log(document);
You will likely see something like this in your terminal:
And an error in your browser:
Does this mean we cannot access all the objects we normally have access to in the browser? Of course not; it just means we need to be a bit more careful about how we access them.
All of these predefined objects are actually properties of the global object. There are many ways to access this object explicitly:
- In the browser:
window
,self
,globalThis
- In Node.js:
global
,globalThis
The only name that works in every context is globalThis
, so we will use that reference.
In JS, accessing undefined variables produces an error, as we just saw. However, accessing undefined object properties on an object that exists does not produce an error; it just returns >
undefined
. Accessing properties onundefined
ornull
will also produce an error. To sum up, ifobj
is an empty object ({}
), and we have defined nothing else:
foo
produces an error (ReferenceError: foo is not defined
)obj.foo
does not produce an error, it just returnsundefined
obj.foo.bar
produces an error (TypeError: Cannot read property 'bar' of undefined
) When accessing properties of objects of …questionable existence, we can use the optional chaining operator?.
instead of the dot operator to avoid errors. To continue the example above,obj.foo?.bar
will not produce an error, it will just returnundefined
.
Therefore, we can have a variable to hold the <html>
element, by doing this in our <script>
element of +layout.svelte
:
let root = globalThis?.document?.documentElement;
Now root
will be undefined
when Svelte runs in Node.js, but it will contain the object that corresponds to the <html>
element when it runs in the browser. This means that we need to use the optional chaining operator ?
when accessing properties of root
to avoid errors.
Therefore, to set the color-scheme
CSS property, we need something like this:
root?.style.setProperty("color-scheme", colorScheme);
Try it out: does it work? You’ll notice that now changing the theme in the dropdown no longer changes the color scheme of the page. Why is that?
There is one last bit to make this work. The way we’ve written this, it will only be executed once, just like regular JS. To tell Svelte to re-run this every time any of its dependencies change, we need to use a reactive statement, i.e. we need to prefix that line of code with $:
.
$: root?.style.setProperty("color-scheme", colorScheme);
If you try it again, the theme switcher should work!
Step 2.4: Reading the color scheme from local storage
Notice that when you reload the page, the theme is reset to the default. This is because we have not yet added any code to save the color scheme to local storage.
First, we’d need to read from localStorage
to get the saved color scheme, if any.
In a browser, any of the following implementations would work, with decreasing levels of verbosity.
if
statement:
let colorScheme = "light dark";
if (localStorage.colorScheme) {
colorScheme = localStorage.colorScheme;
}
let colorScheme = localStorage.colorScheme
? localStorage.colorScheme
: "light dark";
let colorScheme = localStorage.colorScheme ?? "light dark";
However, if you try them in Svelte, you will get an error. This is because, just like document
, localStorage
is a browser-specific variable that is not defined in Node.js.
We could use the same method as above, and access localStorage
through the global object. However, that would get quite messy, so we’ll use a different method. We’ll specify a local variable that is set to localStorage
if it exists, and to an empty object if it doesn’t. We can even call that local variable localStorage
!
let localStorage = globalThis.localStorage ?? {};
As long as we place this before any attempt to access localStorage
, we can now use localStorage
as normal.
Step 2.5: Saving the color scheme to local storage
Reading from localStorage
is only half the battle. We also need to save the color scheme to localStorage
every time it changes.
Thankfully, that is pretty simple too. Our first attempt may look something like this:
localStorage.colorScheme = colorScheme;
However, just like Step 1.3, this will only be executed once. To tell Svelte to make this a reactive statement, we need to prefix that line of code with $:
, just like we did in Step 1.3.
Step 2.6: Preventing FOUC (Optional)
You may have noticed that when you refresh the page, the theme changes after the page has loaded. This is because the theme switcher is only rendered after the page has loaded, and the theme is only set after the theme switcher has been rendered. This is called a Flash of Unstyled Content (FOUC).
To prevent this, we can set the theme before the page has loaded. We can do this by adding a <script>
element to the <head>
of src/app.html
and setting the theme there. This script will be executed before the rest of the page is loaded.
<script>
let root = document.documentElement;
let colorScheme = localStorage.colorScheme ?? "light dark";
root.style.setProperty("color-scheme", colorScheme);
</script>
We could also add this code to your global.js
— it will be executed a little later, but still before the rest of the page is loaded.
Step 3: Loading data from an API
So far we have been loading data from a static JSON file in our own repository. But what fun is that?
Let’s load data from another website and display it in our app! We will use GitHub’s API to read stats about our GitHub profile and display them in our homepage.
Step 3.0: Follow some of your classmates!
If you’re new to GitHub, you may not have followers yet. Since we will be printing out your number of followers from the GitHub API in this step, it will be more rewarding the more followers you have. Plus, you get to explore how quickly the API updates with new data!
Ask the people next to you, behind you, and in front of you for their GitHub usernames, and follow them. Then ask them to follow you back. When you leave the lab, you should all have at least three followers and three following.
Step 3.1: Viewing the data in our browser
GitHub is one of the few APIs left that provides public data without requiring us to authenticate. We can use the /users/username
API endpoint to get public data about a user. Visit https://api.github.com/users/your-username
in your browser, replacing your-username
with your GitHub username. For example, here is mine: https://api.github.com/users/leaverou
.
You should see something like this in your browser:
Step 3.2: Fetching the data in Svelte
In this step, we will be working in src/routes/+page.svelte
.
To make an arbitrary HTTP request in JS, we can use the fetch()
function. For example,
let profileData = fetch("https://api.github.com/users/your-username");
fetch()
is an example of an asynchronous function. This means that it does not return the data directly, but rather a Promise that will eventually resolve to the data after successful retrieval. In fact,fetch()
returns aPromise
that resolves to aResponse
object, which is a representation of the response to the request. To get meaningful data from aResponse
object, we need to call one of its methods, such asjson()
, which returns aPromise
that resolves to the JSON representation of the response body.You do not need to understand promises deeply for the purposes of this lab, but if you want to learn more, you can read MDN’s guide to promises.
Svelte has a special syntax for working with promises in the template: the {#await}
block. It allows us to show different content based on the state of the promise.
The syntax is as follows:
{#await promise} Loading... {:then data} The data is {data} {:catch error}
Something went wrong: {error.message} {/await}
To read data from fetch()
we actually need two nested {#await}
blocks: one for the response, and one for the data. It looks like this:
{#await fetch("https://api.github.com/users/leaverou") }
<p>Loading...</p>
{:then response} {#await response.json()}
<p>Decoding...</p>
{:then data}
<p>The data is { JSON.stringify(data) }</p>
{:catch error}
<p class="error">Something went wrong: {error.message}</p>
{/await} {:catch error}
<p class="error">Something went wrong: {error.message}</p>
{/await}
Try pasting this in your src/routes/+page.svelte
component (replacing leaverou
with your username) and see what happens.
It would look something like this:
You may get a warning about not calling fetch()
eagerly during server-side rendering. The “proper way” to load data for your pages is via a +page.js
file. You may want to experiment with this if you have time, but for now, you can ignore the warning.
Step 3.3: Displaying the data in a more useful way
Now that we’ve made sure we can fetch the data, let’s display it in a more meaningful way. In the same file (+layout.svelte
), in the HTML portion, create a <section>
with an <h2>
for that part of your page.
Decide which stats you want to display, e.g. number of public repos (public_repos
key), number of followers (followers
key), etc. and wrap them in a <dl>
list. Feel free to include any attributes (stats) of your choice that you can see in the API response. Here’s an example of what that code may look like:
{#await fetch("https://api.github.com/users/zhengsophia")}
<p>Loading...</p>
{:then response}
{#await response.json()}
<p>Decoding...</p>
{:then data}
<section>
<h2>My GitHub Stats</h2>
<dl>
<dt>Followers:</dt>
<dd>{data.followers}</dd>
<dt>Following:</dt>
<dd>{data.following}</dd>
<dt>Public Repositories:</dt>
<dd>{data.public_repos}</dd>
</dl>
</section>
{:catch error}
<p class="error">Something went wrong: {error.message}</p>
{/await}
{:catch error}
<p class="error">Something went wrong: {error.message}</p>
{/await}
It should look like this before any styling is applied:
Add some of your personal styling as you see fit!
Because Svelte’s local server will re-run the
fetch()
call every time you save (yes, even if you loaded it via a+page.js
file), it’s easy to hit the rate limit as you iterate on CSS. To avoid that, you can comment out thefetch()
call and use this instead while you’re experimenting with CSS:let profileData = { ok: true, json: async () => ({ followers: 100, following: 100, public_repos: 100, public_gists: 100, }), };
This is what I did:
In case you want a similar style, the gist of it is:
- I applied a grid on the
<dl>
with four equal-sized columns (1fr
each) - I used
grid-row
to override the automatic grid placement and specify that every<dt>
should be placed on the first row of the grid, and every<dd>
on the second row
Step 4: Update your project data
This is in preparation for the next lab. Please update your project data (src/lib/projects.json
) with your assignments from the class and any other projects you can think of. Make sure you have at least 12 projects, even if you need to leave some placeholder data in. Also add a "year"
field to each project with a number for the year you worked on it. Example:
{
"title": "Lorem ipsum dolor sit.",
"year": "2024",
"image": "https://vis-society.github.io/labs/2/images/empty.svg",
"description": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Magnam dolor quos, quod assumenda explicabo odio, nobis ipsa laudantium quas eum veritatis ullam sint porro minima modi molestias doloribus cumque odit."
},
Make sure not all your projects have the same year, since in the next lab we’ll be drawing visualizations based on it, and it would be a pretty boring visualization if they all had the same one!