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
- Check-off
- Questions Doc
- Slides
- Step 0: Creating a layout for UI shared across pages
- Step 1: Port the theme switcher to Svelte
- Step 1.1: Porting the theme switcher HTML and CSS to our layout
- Step 1.2: Bind color scheme to a variable
- Step 1.3: Apply the color scheme to the
<html>
element - Step 1.4: Reading the color scheme from local storage
- Step 1.5: Saving the color scheme to local storage
- Step 1.6: Preventing FOUC (Optional)
- Step 2: Loading data from an API
- Step 3: 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). Please fill in the check-off form at labs/5/checkoff
before your check-off. Ideally you should fill in the form right before your check-off, but it’s ok if you fill it out in advance.
Filling out the form is a necessary but not sufficient condition to get checked-off. You still need to come to office hours in person for your check-off to be processed.
You could even fill it out before you finish the lab, since we won’t look at it until your check-off, but the closer to the end of the lab you fill it out, the more meaningful your feedback will be.
Questions Doc
Add questions to the questions doc throughout the lecture and lab! After lab, come to office hours or ask on Discourse for futher questions!
Slides
Make sure to read the notes on each slide as well!
Step 0: Creating a layout for UI shared across pages
This was optional in Lab 4, but we will need it for this lab. If you haven’t done it yet, create a layout component that will be shared across all pages.
You can ignore steps 6.2 to 6.4 or do them at home, all we need for this lab is the layout component, i.e. up to step 6.1.
Step 1: Port the theme switcher to Svelte
Step 1.1: Porting the theme switcher HTML and CSS to our layout
Copy the HTML that your JS was generating for the theme switcher and paste it in your +layout.svelte
component. Delete or comment out (Cmd + /) the JS code that was generating that HTML.
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.
Refresh and make sure everything still works.
Step 1.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.
“Bind” in this context 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}
. The syntax for directives 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 adding a {colorScheme}
expression above the <select>
(and remove it after).
Step 1.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 expressions in { ... }
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
Note that the only name that works in every context is globalThis
, so let’s use that.
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:
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 1.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 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 1.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 1.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 2: 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 2.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 2.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 2.2: Fetching the data in Svelte
To make an arbitrary HTTP request in JS, we can use the fetch()
function.
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. 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 2.3: Displaying the data in a more useful way
Ok, now that we’ve made sure we can fetch the data, let’s display it in a more meaningful way. 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 display them in a <dl>
list.
It should look like this before any styling is applied:
Feel free to style it 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 3: 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!