Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save chriscorwin/a40dc819e8d0dcb52194e4bc5b9697fa to your computer and use it in GitHub Desktop.
Save chriscorwin/a40dc819e8d0dcb52194e4bc5b9697fa to your computer and use it in GitHub Desktop.
SvelteKit Coffee Brewing App Tutorial

In-Depth SvelteKit Coffee Brewing App Tutorial for Beginners

Welcome to the comprehensive tutorial on how to create a single-page SvelteKit application from scratch. We're going to build a practical, fully functional mockup for an iOS app designed to calculate the ideal coffee-to-water brewing ratio. You'll learn to construct various components, understand their interdependencies, and get a sense of the bigger picture of app development.

Prerequisites

Before we dive into the building process, let's make sure you have all the tools you need. We'll be using the following:

  • Homebrew: The package manager for macOS (or Linux).
  • Git: A distributed version control system.
  • Node.js and npm: JavaScript runtime and package manager.
  • Visual Studio Code: A source code editor. You can use your preferred one, but we'll use VS Code in our examples.

If you don't have these installed, no worries. You can find installation instructions in the resources listed at the end of this tutorial. Once you have all these in place, you'll be ready to start creating your SvelteKit project.

Setting Up the Project

Step 1: Creating a new SvelteKit Project

First, let's create a new SvelteKit project. Open your terminal (also known as the command line) and navigate to the directory where you want to create your new project. You can do this using the cd (change directory) command. Once you're in the desired directory, run the following command:

npm init svelte@next coffee-calc

This command initiates a new SvelteKit project. coffee-calc is the name of our project. Feel free to replace this with a name of your choosing. Once the command is executed, a new directory named coffee-calc will be created in your current directory. This new directory will contain all the files for your SvelteKit project.

Step 2: Installing Project Dependencies

Next, we need to install the project dependencies. These are libraries and tools that our project will use. To do this, first navigate into your new project directory using the cd command, like so:

cd coffee-calc

Then, run the following command to install the dependencies:

npm install

This command tells npm (the package manager we installed earlier) to download and install the packages that our project needs.

Step 3: Starting the Development Server

Now that the project setup is complete, we can start the development server. This is a small program that runs on your computer and serves your project files to your web browser. It also watches your files for changes and automatically refreshes your browser when you save changes. To start the development server, run:

npm run dev

After running this command, if you open your web browser and navigate to http://localhost:5000, you should see a "Hello world!" message. This indicates that your SvelteKit project setup is correct and you're ready to start building.

Building the Components

Our coffee brewing calculator will consist of several components. In Svelte, a component is a reusable piece of UI. It can contain its own state (data), markup (HTML), and behavior (JavaScript). Our application will have the following components:

  • RatioInput.svelte: Allows users to input the desired ratio of coffee to water.
  • MassInput.svelte: Lets users input the mass of coffee and water they plan to use.
  • UnitToggle.svelte: Provides users with the ability to switch between using grams and ounces.
  • FeedbackDisplay.svelte: Displays the selected mass of coffee and water in the currently chosen unit.
  • ResetButton.svelte: Allows users to reset each input individually.
  • ResetAllButton.svelte: Provides users with the option to reset the entire form at once.
  • VisualFeedback.svelte: Offers a visual cue to users when a value changes.
  • KeyboardInput.svelte: Lets the application respond to keyboard inputs.

Let's walk through creating each of these components one by one.

Building the RatioInput.svelte Component

The RatioInput.svelte component allows users to input the desired ratio of coffee to water. In the src/routes directory, create a new file named RatioInput.svelte. In this file, add the following code:

<script>
  // We're creating an object named 'ratio' to store the coffee-to-water ratio. We initialize it with a default ratio of 1:15.
  export let ratio = { coffee: 1, water: 15 };

  // This is a function to handle changes in the input fields.
  function handleInputChange(event) {
    // We're using the name of the input field (either 'coffee' or 'water') to determine which property of the 'ratio' object to update.
    // The 'Number()' function is used to convert the input field value from a string to a number.
    ratio[event.target.name] = Number(event.target.value);
  }
</script>

<div class="ratio-input">
  <!-- The 'for' attribute of the label should match the 'id' of the corresponding input field. -->
  <!-- The 'bind:value' attribute creates a two-way data binding between the input field value and the corresponding property of the 'ratio' object. -->
  <!-- The 'on:input' attribute is an event listener that triggers the 'handleInputChange' function every time the user changes the input field value. -->
  <label for="coffee">Coffee Ratio:</label>
  <input id="coffee" name="coffee" bind:value={ratio.coffee} type="number" min="0" step="0.1" on:input={handleInputChange} />

  <label for="water">Water Ratio:</label>
  <input id="water" name="water" bind:value={ratio.water} type="number" min="0" step="0.1" on:input={handleInputChange} />
</div>

Building the MassInput.svelte Component

The MassInput.svelte component allows users to input the mass of coffee and water they plan to use. This is similar to the RatioInput.svelte component. Create a new file in the src/routes directory named MassInput.svelte, and add the following code:

<script>
  // We're creating an object named 'mass' to store the mass of coffee and water. We initialize it with a default value of 0 for both.
  export let mass = { coffee: 0, water: 0 };

  // This is a function to handle changes in the input fields.
  function handleInputChange(event) {
    // We're using the name of the input field (either 'coffee' or 'water') to determine which property of the 'mass' object to update.
    // The 'Number()' function is used to convert the input field value from a string to a number.
    mass[event.target.name] = Number(event.target.value);
  }
</script>

<div class="mass-input">
  <!-- The 'for' attribute of the label should match the 'id' of the corresponding input field. -->
  <!-- The 'bind:value' attribute creates a two-way data binding between the input field value and the corresponding property of the 'mass' object. -->
  <!--

 The 'on:input' attribute is an event listener that triggers the 'handleInputChange' function every time the user changes the input field value. -->
  <label for="coffee">Coffee Mass:</label>
  <input id="coffee" name="coffee" bind:value={mass.coffee} type="number" min="0" step="0.1" on:input={handleInputChange} />

  <label for="water">Water Mass:</label>
  <input id="water" name="water" bind:value={mass.water} type="number" min="0" step="0.1" on:input={handleInputChange} />
</div>

Building the UnitToggle.svelte Component

The UnitToggle.svelte component provides users with the ability to switch between using grams and ounces. Create a new file in the src/routes directory named UnitToggle.svelte and add the following code:

<script>
  import { createEventDispatcher } from 'svelte';

  // We're initializing a variable named 'units' with a default value of 'grams'.
  // We're also creating an event dispatcher. This will allow us to dispatch custom events.
  let units = 'grams';
  const dispatch = createEventDispatcher();

  // This is a function to toggle the units and dispatch a 'change' event.
  function toggleUnits() {
    // The ternary operator (?) is used to toggle the 'units' variable between 'grams' and 'ounces'.
    units = units === 'grams' ? 'ounces' : 'grams';
    // We're dispatching a 'change' event with the new units as the payload.
    dispatch('change', units);
  }
</script>

<div class="unit-toggle">
  <!-- The 'on:click' attribute is an event listener that triggers the 'toggleUnits' function when the user clicks the button. -->
  <button on:click={toggleUnits}>
    <!-- The ternary operator (?) is used to display the unit that the user can switch to. -->
    Switch to {units === 'grams' ? 'ounces' : 'grams'}
  </button>
</div>

Building the FeedbackDisplay.svelte Component

The FeedbackDisplay.svelte component displays the selected mass of coffee and water in the currently chosen unit. Create a new file in the src/routes directory named FeedbackDisplay.svelte and add the following code:

<script>
  // We're exporting variables named 'mass' and 'units'. These will receive values from the parent component.
  export let mass;
  export let units;
</script>

<div class="feedback-display">
  <!-- We're using curly braces ({}) to insert JavaScript expressions into the HTML. -->
  <!-- The 'mass.coffee' and 'mass.water' expressions will be replaced with the values of the 'coffee' and 'water' properties of the 'mass' object. -->
  <!-- The 'units' expression will be replaced with the value of the 'units' variable. -->
  <p>Coffee Mass: {mass.coffee} {units}</p>
  <p>Water Mass: {mass.water} {units}</p>
</div>

Building the ResetButton.svelte Component

The ResetButton.svelte component allows users to reset each input individually. Create a new file in the src/routes directory named ResetButton.svelte and add the following code:

<script>
  // We're exporting a variable named 'resetValue'. This will receive a function from the parent component.
  export let resetValue;
</script>

<div class="reset-button">
  <!-- The 'on:click' attribute is an event listener that triggers the 'resetValue' function when the user clicks the button. -->
  <!-- We're using an arrow function (=>) to call the 'resetValue' function with an argument of 0. -->
  <button on:click={() => resetValue(0)}>Reset</button>
</div>

Building the ResetAllButton.svelte Component

The ResetAllButton.svelte component allows users to reset the entire form at once. Create a new file in the src/routes directory named ResetAllButton.svelte and add the following code:

<script>
  import { createEventDispatcher } from 'svelte';

  // We're creating an event dispatcher. This will allow us to dispatch custom events.
  const dispatch = createEventDispatcher();

  // This is a function to dispatch a 'reset' event.
  function resetAll() {
    // We're dispatching a 'reset' event. This event doesn't need a payload.
    dispatch('reset');
  }
</script>

<div class="reset-all-button">
  <!-- The 'on:click' attribute is an event listener that triggers the 'resetAll' function when the user clicks the button. -->
  <button on:click={resetAll}>Reset All</button>
</div>

Building the VisualFeedback.svelte Component

The VisualFeedback.svelte component offers a visual cue to users when a value changes. Create a new file in the src/routes directory named VisualFeedback.svelte and add the following code:

<script>
  import { fade } from 'svelte/transition';
  import { onMount } from 'svelte';

  // We're initializing a variable named 'show' with a default value of false.
  let show = false;

  // The 'onMount' function is a lifecycle hook that runs after the component first renders.
  // We're using it to set 'show' to true after a delay of 2 seconds.
  onMount(() => {
    setTimeout(() => {
      show = true;
    }, 2000);
  });
</script>

<!-- The '{#if show} ... {/if}' syntax is an if block. It only renders the enclosed HTML if 'show' is true. -->
<!-- The 'transition:fade' attribute applies a fade transition to the div. This will cause the div to fade in when it is rendered. -->
{#if show}
  <div transition:fade>
    Value changed
  </div>
{/if}

Building the KeyboardInput.svelte Component

The KeyboardInput.svelte component lets the application respond to keyboard inputs. Create a new file in the src/routes directory named KeyboardInput.svelte and add the following code:

<script>
  import { onMount } from 'svelte';

  // The 'onMount' function is a lifecycle hook that runs after the component first renders.
  // We're using it to add a 'keydown' event listener to the window.
  // We're also returning a function that removes the event listener. This function will run when the component is destroyed.
  onMount(() => {
    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  });

  // This is a function to handle the 'keydown' event.
  function handleKeyDown(event) {
    switch (event.key) {
      case 'ArrowUp':
        // This case will run when the user presses the 'ArrowUp' key.
        // You can put code here to increase a value.
        break;
      case 'ArrowDown':
       

 // This case will run when the user presses the 'ArrowDown' key.
        // You can put code here to decrease a value.
        break;
    }
  }
</script>

Part 5: Bringing It All Together

Now that we've created all the components, we need to bring them together in the main application component, App.svelte. This is where we'll manage the application's state and pass data and functions down to the child components.

Replace the content of src/App.svelte with the following code:

<script>
  // We're importing all the components we've just created.
  import RatioInput from './RatioInput.svelte';
  import MassInput from './MassInput.svelte';
  import UnitToggle from './UnitToggle.svelte';
  import FeedbackDisplay from './FeedbackDisplay.svelte';
  import ResetButton from './ResetButton.svelte';
  import ResetAllButton from './ResetAllButton.svelte';
  import VisualFeedback from './VisualFeedback.svelte';
  import KeyboardInput from './KeyboardInput.svelte';

  // We're initializing some variables with default values.
  // These variables represent the state of our application.
  let coffeeMass = 17;
  let waterMass = 255;
  let coffeeRatio = 1;

  // We're calculating the water ratio based on the coffee mass and water mass.
  let waterRatio = waterMass / coffeeMass;

  // We're initializing some objects with the values of our variables.
  // These objects will be passed to child components as props.
  let ratio = { coffee: coffeeRatio, water: waterRatio };
  let mass = { coffee: coffeeMass, water: waterMass };

  // We're initializing a variable for the units.
  let units = 'grams';

  // We're creating a reactive statement using the '$:' syntax.
  // This statement will run whenever one of its dependencies (mass.coffee, mass.water, coffeeRatio) changes.
  $: {
    coffeeMass = mass.coffee;
    waterMass = mass.water;
    waterRatio = parseFloat((waterMass / coffeeMass).toFixed(1));
    ratio = { coffee: coffeeRatio, water: waterRatio };
  }

  // This is a function to handle the 'change' event from the UnitToggle component.
  function handleChange(event) {
    // We're updating the 'units' variable with the new units from the event's 'detail' property.
    units = event.detail;
  }

  // This is a function to reset a value.
  function resetValue(value) {
    // We're returning 0, which will reset the value.
    return 0;
  }

  // This is a function to reset all values.
  function resetAll() {
    // We're resetting the 'coffee', 'water', and 'ratio' properties of the 'mass' and 'ratio' objects to their initial values.
    mass.coffee = resetValue(coffeeMass);
    mass.water = resetValue(waterMass);
    ratio.coffee = resetValue(coffeeRatio);
    ratio.water = resetValue(waterRatio);
  }
</script>

<svelte:head>
  <title>Coffee Calc</title>
  <!-- We're adding a description meta tag to improve SEO. -->
  <meta name="description" content="A coffee brewing calculator built with Svelte." />
</svelte:head>

<section>
  <!-- We're adding a heading for our application. -->
  <h1>Coffee Calc</h1>

  <!-- We're rendering all of our components inside a section element. -->
  <!-- We're passing props to the components using the '{propName}' syntax. -->
  <!-- We're also passing event handlers to the components using the 'on:eventName' syntax. -->
  <RatioInput bind:ratio={ratio} />
  <MassInput bind:mass={mass} />
  <UnitToggle on:change={handleChange} />
  <FeedbackDisplay {mass} {units} />
  <ResetButton {resetValue} />
  <ResetAllButton on:reset={resetAll} />
  <VisualFeedback />
  <KeyboardInput />
</section>

<style>
  /* We're adding some CSS styles to center the content on the page. */
  section {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    flex: 0.6;
  }

  /* We're adding a style to make the heading full-width. */
  h1 {
    width: 100%;
  }
</style>

Part 6: Testing Your Application

Now that you've built your application, it's time to test it! Run the following command in your terminal to start your development server:

npm run dev

Then, open your browser and navigate to http://localhost:5000. You should see your new Svelte application, "Coffee Calc"! Try entering some values and see how the application responds. If everything works as expected, congratulations! You've built a fully functional Svelte application.

Part 7: Resources for Further Learning

This tutorial only scratches the surface of what you can do with Svelte. If you want to learn more, here are some resources to check out:

I hope you enjoyed this tutorial! Happy coding!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment