loading...

Build a Random Quote Machine with Svelte and Parcel

ringmaster profile image Owen Winkler ・10 min read

Alt Text

Here's what we're going to build.

This tutorial is about how to build a simple Random Quote generator using Svelte and to be able to share the quote on Twitter.

The goal of this tutorial is to show how to use Svelte, Bulma, and Parcel to build a simple web app similar to the post written by Tunde Oyewo to do the same thing in React.

Getting Set Up

There are a handful of good resources for getting set up with Svelte development, including one on the Svelte website which refers to a post here at dev.to. Since we're looking to focus primarily on the Svelte functionality, let's get through the setup as quickly as possible.

Create a new project directory, and in it we'll set up a basic app structure by running these commands:

# Create a package.json file with the dev dependencies
npm install --save-dev svelte parcel-plugin-svelte parcel-plugin-static-files-copy parcel-bundler bulma @fortawesome/fontawesome-free

# Make a source directory to build from and a static asset dir
mkdir -p src/static

# Create your blank base files
touch src/index.html src/main.js src/App.svelte src/Quote.svelte src/base.scss src/static/quotes.json

Your app's package.json

You can get pretty far in your app development without using the package.json file, but unfortunately there are some settings in this application that will require setting up some values there. In addition to any npm packages that are installed, you'll want to add these settings in your package.json file:

{
  "browserslist": [
    "since 2017-06"
  ],
  "staticFiles": {
    "staticPath": "src/static"
  }
}

The browserslist option specifies a setting that lets us compile for more recent browsers without having to dive deep into some Babel configuration.

The staticFiles option specifies a directory from which some static files will be copied into the dist directory when the application is built. We'll use this to package up a data file of quotes that isn't built directly into our application. Keep reading to learn more about that.

The HTML Boilerplate

There is likely an npx command to get the above and a bit of html and javascript boilerplate, but it's easy enough to get things rolling with some basic knowledge.

Edit the index.html to create a basic html page that refers to our javascript file:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Svelte Quote Generator</title>
</head>
<body>
</body>
<script src="./main.js"></script>
</html>

At this point, you should be able to start up Parcel to listen for your changes and serve them through a development web server. To do so, simply run:

parcel src/index.html

You will see Parcel compile your assets (in dev mode) and serve them via a local webserver, likely http://localhost:1234 If you open that page, you should see the blank page from above.

Getting Bulma working

Bulma is a great little CSS package that does a lot of what Bootstrap does without a lot of the headache. Even if you don't want to use Bulma, you could use these instructions to get a similar SASS/SCSS framework set up for your project.

In the src/base.scss file, make a simple update to set the charset and include bulma from node_modules:

@charset "utf-8";
@import "../node_modules/bulma/bulma.sass";

Later, you could define variables and global page styles in this file above the line where Bulma is imported, and when Parcel compiles your CSS, it'll apply those to the Bulma source. This is neat because it lets you easily change the "theme" of the Bulma colors without having to redefine the classes yourself. Check out this customization page if you want to see what variables are available.

To get your customizations and Bulma itself to compile into your output, as well as make Font-Awesome available, import your base.scss file in the src/main.js file, so that Parcel packages it for you:

import "./base.scss"
import "@fortawesome/fontawesome-free/css/all.css"

With this in place, Parcel will automatically package your CSS from the base.scss file into your output, and make all of the Font-Awesome classes available for use. You do not need to alter your html file for this to happen or add references to CDNs; Parcel will add the line to your output that references the required CSS file from the local web server that it starts.

If you save your files in this state, everything should compile successfully in Parcel. If there is an error compiling your base.scss file due to the Bulma files not being present, try reinstalling the Bulma npm package by itself using npm install --save-dev bulma then restarting Parcel with the command above.

Adding the Svelte app

We'll initially need to tell Svelte the html node to attach to so that the application can start. We will also need to import the main Svelte application file.

In the src/main.js file, update to include these commands to import and attach the Svelte application to the body element:

import "./base.scss"
import "@fortawesome/fontawesome-free/css/all.css"
import App from "./App.svelte"

const app = new App({
  target: document.body
})

Let's make our Svelte App file simple to start, just to show that Svelte is working properly. In src/App.svelte:

<section class="section">
  <div class="container has-text-centered">
    <h1 class="title">Random Quote</h1>
  </div>
  <div class="container">
    <div class="box">Test</div>
  </div>
</section>

This should compile cleanly with Parcel, and display a very basic page that shows "Test" in a box where the random quote and attribution will appear.

Loading the quotes async

Here's a little bit of a bonus. You could just export the quotes from a js file and then import them into the app, but in most cases, you're likely to be pulling this data as json from some server somewhere, probably even your own. So let's try loading the quote data asynchronously in src/App.svelte:

<script>
let gettingQuotes = getQuotes()

async function getQuotes() {
  const response = await fetch("./quotes.json")
  const data = await response.json()

  if(response.ok) {
    return data
  }
  else {
    throw new Error(data)
  }
}
</script>

<section class="section">
  <div class="container has-text-centered">
    <h1 class="title">Random Quote</h1>
  </div>
  <div class="container">
    {#await gettingQuotes}
    <p>Loading quotes...</p>
    {:then quotes}
    {quotes}
    {:catch error}
    <p>{error.message}</p>
    {/await}
  </div>
</section>

From the top, in the <script> section, we set a variable gettingQuotes to the value returned by the async function getQuotes(), which retrieves the quotes from a json file stored on the server. Because getQuotes() is async, it will return a Promise. This is important because when our application starts, we will not have the quote values loaded.

Below, in the html, we have some mustache tags. The {#await gettingQuotes} is what waits for our Promise to resolve. Until it does, it displays the loading message. After the {:then quotes} is shown after the Promise resolves successfully. Note that quotes is the value returned when the promise resolves; the actual return value of the completed call to getQuotes(). The {:catch error} section is displayed if there is an error, and the value of error is the thrown error result. We close the section with the {/await}.

We'll end up replacing the {quotes} in the above with a <Quotes {quotes}/> so that it uses the component to display one of the random quotes. But before we do that, just for now to make this work we'll need to put some basic json quotes into the src/static/quotes.json file:

[
  {
      "quote": "Be who you are and say what you feel, because those who mind don't matter, and those who matter don't mind.",
      "author": "Bernard M. Baruch"
  },
  {
      "quote": "The fool doth think he is wise, but the wise man knows himself to be a fool.",
      "author": "William Shakespeare, As You Like It"
  },
  {
      "quote": "Truth is singular. Its 'versions' are mistruths.",
      "author": "David Mitchell, Cloud Atlas"
  },
  {
      "quote": "It's only after we've lost everything that we're free to do anything.",
      "author": "Chuck Palahniuk, Fight Club"
  }
]

Totally use your own favorite quotes; it's fun.

After you save this file and Parcel compiles it, the application should load the data and display text output indicating that it loaded the individual quote objects. Now we just need to feed those objects into the component for selection.

Adding the quote component

To build the quote component, we'll build a div that will fit into the right space in the original App.svelte file, and fill it with quote markup. In the src/Quote.svelte file:

<script>
export let quotes=[]

let quote = getRandomQuote(quotes)

function getRandomQuote(quotes){
  return quotes[Math.floor(Math.random() * quotes.length)]
}

function updateQuote() {
  quote = getRandomQuote(quotes)
}

function postToTwitter() {
  window.open('https://twitter.com/intent/tweet/?text=' + encodeURIComponent(quote.quote + '--' + quote.author))
}
</script>

<style>
footer {
  font-weight: bold;
  margin-left: 3rem;
}
footer::before {
  content: "\2014 ";
}
blockquote {
  margin-bottom: 2rem;
}
</style>

<div class="box">
  <blockquote>
    <p class="quote content">{quote.quote}</p>
    <footer>{quote.author}</footer>
  </blockquote>

  <div class="buttons">
    <button
      class="button"
      on:click={updateQuote}
      type="submit">
      <span class="icon"><i class="fas fa-redo"></i></span>
      <span>Generate Quote</span>
    </button>
    <button
    on:click={postToTwitter}
    class="button">
      <span class="icon"><i class="fab fa-twitter"></i></span>
      <span>Share Quote</span>
    </button>
  </div>
</div>

There's a lot to unpack here. Starting from the top in the script section, we export an empty array of quotes. This will be set to the value of the quotes parameter of the <Quote> tag that we'll ultimately add to the App.svelte file.

We assign a random quote object from that array to the quote variable. The function getRandomQuote() returns one quote object from the array that we pass. The updateQuote() function will update the value of quote to a new quote object so that we can execute that when we click our button. Likewise, the postToTwitter() function sends the currently selected quote to Twitter, and can be called when clicking that button in our UI.

The <style> section of our component contains styles that are local only to this component. As such, it's safe to define new styles for footer and blockquote since they will only affect markup that is produced in this component. This is a pretty slick feature of Svelte.

Our html markup is fairly straighforward except potentially for where we've included mustache code to output our data and connect our events. Inside the <blockquote> element, we're using {quote.quote} and {quote.author} to output the quote text and author name, respectively, from our selected quote object.

For each of the buttons, an on:click handler function is set to handle that button's event. The functions perform as described above. Of note is that the function name must be wrapped in braces, and the value that is inside the braces for an event must evaluate to a function. That is, using {updateQuote()} as the handler will not work, because this would assign the result of calling updateQuote() as the event hander, which is not a function. This is a mistake I still make too frequently.

There are definitely different ways to accomplish these event bindings and assignments than what I've done, but I provided this example because this gave me a clear route to explain what was going on. For example, it's possible to simplify (for some meanings of "simplify") the event handlers by defining them inline, like on:click="{()=>quote=getRandomQuote()}", but I found this less readable when trying to explain how to use it for the purpose of this post.

Using our component

Using the component is pretty simple in the end. We just need to include the file that defines the component, and then include the component in the output.

In the src/App.svelte file, add this line to the top of the <script> section to include the component:

import Quote from "./Quote.svelte"

Then, to use the component, replace the line that says {quotes} with this:

<Quote {quotes}/>

When you use an unnamed parameter like this, the name of the variable is used as the parameter name. So this is functionally equivalent to:

<Quote quotes="{quotes}"/>

It's often handy to use this technique, particularly when you can use the spread operator on an object.

The final App.svelte file should look like this:

<script>
import Quote from "./Quote.svelte"
let gettingQuotes = getQuotes()

async function getQuotes() {
  const response = await fetch("./quotes.json")
  const data = await response.json()

  if(response.ok) {
    return data
  }
  else {
    throw new Error(data)
  }
}
</script>


<section class="section">
  <div class="container has-text-centered">
    <h1 class="title">Random Quote</h1>
  </div>
  <div class="container">
    {#await gettingQuotes}
    <p>Loading quotes...</p>
    {:then quotes}
    <Quote {quotes}/>
    {:catch error}
    <p>{error.message}</p>
    {/await}

  </div>
</section>

If you're still running the Parcel web server, it should build this file automatically, and serve it from the named host. If not, run the server again now, and you should see the positive results.

Making a build

Running from the dev server is fine for debugging, but it builds in a lot of extra functionality so that you can debug, and doesn't make the output as small as it can be, which is one of Svelte's advantages.

To make a production build, stop the Parcel dev server (press Ctrl+c) and then run the build command:

parcel build --public-url '.' src/index.html

When this command completes, the files in a new dist directory are the result. You should be able to open the index.html file there in a browser, but because the files are on your local filesystem and not loaded via a server, your browser will prevent the script from loading your quotes file. If you upload the contents of your dist directory to a public server, everything should load as it did on your dev server.

Perhaps the easiest way to accomplish this is to use Surge. This is well beyond this post's intent, though, and the Surge documentation is pretty thorough. You should be able to easily deploy your test app to a surge.sh domain and share it with others.

Enjoy!

Posted on by:

ringmaster profile

Owen Winkler

@ringmaster

Still learning this poop after 20 years.

Discussion

pic
Editor guide