DEV Community

CorvusEtiam
CorvusEtiam

Posted on

How to build Budget Poll App in Svelte3

Introduction

I would like to start by explaining myself, why I use yet-another-almost-unknown-framework, where most people just stop at using Angular, React, Vue and maybe one or two more. Well, best explanation would be something about combination of both small learning curve which is comparable with raw HTML5 and old-school tech, speed, small size and low amount of boilerplate.

Here I would like to preset for you, how to build small, usable, money polling app. It is not much and it wont win any beauty pageants for websites.

It would be good idea, if you have good grasp of HTML5, Javascript.

Money Polling?

Let me show you, some simple example. You are going with your friends to grab a pizza together, but there is a problem. The best pizzeria in town doesn't accept card payment, ATM is far away and not everyone has enough cash on hand to fully split a bill. What do we do? It is pretty simple. We take receipt and split it equally between everyone. Everyone pay as much as they could, and later on we will repay bills between each other or just sent each other money using our smathphones. Easy? Yes.

How normal person do it?

  1. Take calculator
  2. Divide recipe by number of people
  3. Average price per person - money placed in a poll => How much rest of people owe to this guy/gal or how much they are in debt.

How dev do it?

  1. Open Excel
  2. Go to: algorithm for normal person

How webdev do it?

  1. Built website for it.
  2. Sit alone at restaurant because rest of your friends made a deal, few hours ago and happily go home. Restaurant owner looks weird at you now.

Again why Svelte and not React?

DISCLAIMER: I am planning to do the same in React.

This is how, classic input component for basic data data within out app look like in both:

class BasicInfo extends React.Component {
    render() {
         return (
      <fieldset>
        <legend>Poll</legend>
        <p>Provide name and amount of money for your poll</p>
        <p>
          <label htmlFor="pollName">Name: </label>
          <input
            type="text"
            id="pollName"
            name="pollName"
            onChange={ev => this.props.onChangeName(ev.target.value) }
          />
        </p>
        <p>
          <label htmlFor="pollAmount">Amount: </label>
          <input
            type="number"
            id="pollAmount"
            name="pollAmount"
            onChange={ev =>
              this.props.onChangeAmount(parseInt(ev.target.value, 10))
            }
          />
        </p>
        <button type="button" className="active"
          onClick={() => {
            this.props.onSave();
          }}
        >
          Save
        </button>
      </fieldset>
    );
  }
}
}

Yuck, and it is not all of it. Seriously, mine code took almost twice as much, because of the additional type annotations from react. It is still missing large chunk of code for constructor and default props and state.

Now for svelte.

<script>

    const CURRENCY = {
        "PLN" : { name: "złoty" },
        "USD" : { name: "dollar" }
    }

    let name = "";
    let amount = 0;
    let currency;

    function save() {
        /// here goes save function. Cut for brewity
    }

    $: is_filled = ( name !== "" ) && ( amount > 0 ) && (currency !== undefined);
</script>
<fieldset>
    <legend>Poll Start</legend>
    <p>Please provide basic data about poll</p>
    <label for="pollName">Poll Name: </label>
    <input type="text" id="pollName" bind:value={name} required>
    <label for="pollAmount">Poll Name: </label>
    <input type="number" id="pollAmount" bind:value={amount} required>
    <select bind:value={currency}>
        <option value="default" disabled selected>Select Currency</option>
        {#each Object.entries(CURRENCY) as entry }
        <option value={entry[0]}>{ entry[1].name }</option>
        {/each} 
    </select>
    {#if is_filled }
    <button type="button" on:click={save}>Save</button>
    {/if}
</fieldset>

Don't worry if you are not understanding all of it.
Most important part here is that, Svelte code in original project took around 32 lines of code to implement all of this.

I lied... Sorry.

Seriously, Svelte is not a framework. If you visit their site SvelteDev, you will find only some weird text namely: CYBERNETICALLY ENHANCED WEB APPS.
Which sounds like hip-words said to bunch of managers, to sell yet-another-useless-project to them. Don't worry, it is not that bad. What they probably meant to say with it is that: Svelte is not a framework. Not at all.

You should think about as something like Typescript, Vue Single-File-Templates or Babel. It is the tool that generate boilerplate for you. Compiler of sorts, just not be scared. There is not many dragons inside...

Time to start

First we should setup our environment. There are two possible paths for you now.

  1. Use codesandbox.io and login with Github credentials. Then click on "Create Sandbox" button and frow "Client Sandbox" tab pick Svelte Sandbox.

  2. Setup svelte locally with editor. I use VSCode, but something like Notepad++ will work good enough. You just need editor with HTML5 support.

I will show you second path.

Installation

You should have working nodejs and npm on your system. Also we will use git for good practice. If not, grab an https://nodejs.org/en/download/.
Secondly open shell or command line and type in

npx degit sveltejs/template budget-poll

Npx is tool which wraps npm. It download and install tool provided as second argument and run this tool with rest of arguments provided.
Here we use degit. Tool written by Rich Harris, author of Svelte, to setup project by cloning template from git repo and placing everything in folder named budget-poll.
He loves to reinvent a wheel, but he does some wonderful stuff with it.
Now standard git setup and project install:

cd budget-poll
git init
git add *
git commit -am "Initial setup of budget app"
npm install 

After a while, and downloading solid chunk of internet we have working environemt. Now just type in npm run dev and open browser with adress showing up in your terminal. For me, it was localhost:5000.

General components layout

First things first, we should think about how our components will be laid out.
We need few things. I would love to split budget input into few parts and add some way to go between those pages.
That means we need both MultiPanelForm and FormPanel components.
Let me write it inside out App.svelte.

<script>
    // App.svelte content
</script>
<MultiPanelForm>
    <FormPanel>
        <!-- First panel -->
    </FormPanel>
    <FormPanel>
        <!-- Second panel -->
    </FormPanel>
    <FormPanel>
        <!-- Third panel -->
    </FormPanel>
</MultiPanelForm>

Ok, looks simple. If you ever saw how to use JSX it is similar. Or frankly, just as you would write it in HTML5.

Those uppercased non-standard tags are our components. To use those we will need to import them from elsewhere.
To do that, add with App.svelte script tags those ES6 style imports. Remember to use relative paths and add extension to your file names. Svelte will not be happy, because they do not exist yet. Get over it big boy, we will do it in a moment. Also remove property props: { ... } from main.js. You are not going to use it.

import MultiPanelForm from "./MultiPanelForm.svelte";
import FormPanel from "./FormPanel.svelte";

Now, you think this is easy. Now there will be madness of JSX, styles in js and whatnot, just like in React land.
Well, let's find out.

MultiFormPanel

Create file named MultiFormPanel.svelte. This is out first, reusable component. It contains free parts enclosed in tags.
Add those to the file.

<script>
    /// mostly JS and some svelte-specific extensions
</script>
<style>
    /* css styling for your component */
</style>
<form>
    <!-- html part of component and svelte templating -->
</form>

What are does svelte-specific extensions and templating? Ok, give me a second.
Create another file, named FormPanel.svelte and copy this layout to the newly created file. Just replace form tag with a div.

When you open browser, svelte should render everything correctly. I mean, you wont see nothing except empty form, but it works. Now, time to deal with those custom components withing components.
We have to provide target, a place of some sorts to place those components within.
A slot! How to do that?

Time to edit MultiFormPanel.svelte and add some html.

<form>
    <div class="container">
        <slot></slot>
    </div>
    <div class="controller"></div>
</form>

Ok, I jumped forward a little bit. If you know any react JSX than, you will notice, we are not using any className here, or any weirdly named tags. Just old school html plus custom (or not so custom), slot tag.
Slot is a place were children of this component will go automatically. To better understand this, open svelte.dev page and read up in pretty great, interactive tutorial.

We will also edit: FormPanel and enter this html.

<div class="multiform-panel">
    <slot></slot>
</div>

Ok. With some generic HTML, now we need to add functionality. First buttons except that, they should appear only
when certain criteria are met.
But first we need a way to store on which page we are exactly.
Create globals.js file in the same ./src dir and type in there, these text.

/* writable is a way to keep global state in svelte, just like context api, 
just simpler and easier to split in smaller parts
*/
import { writable } from "svelte/store";

/* here we define and export controllerState store. We can now subscribe in other files to this store and update its content */
export const controllerState = writable({
    current: 0
})

Now time for some cybernetical enhanced js... or just normal js with a few extension to make less boilerplate possible.

Open MultiFormPanel.svelte and first add two control buttons. Those buttons should appear when certain criteria are met.
To archive this we will use templates similar to those known from Mustache template.

<div class="controller">
    {#if prev_active}
    <button type="button">Previous</button>
    {/if}
    {#if next_active}
    <button type="button">Next</button>
    {/if}
</div>

Both prev_active and next_active are boolean variables. Let us define them now.

<script>
    import { controllerState } from "./globals.js";

    export let last_page;

    let prev_active = $controllerState.current > 0;
    let next_active = $controllerState.current < last_page;
</script>

We get few new things here. First export let last_page. This is how svelte implements properties.
Just export let <prop_name> = <default_value> and you are ready to go.

Now you can, edit <MultiFormPanel> in App.svelte to <MultiFormPanel last_page={2}> to pass properties withing.

What are those dollar sing for? I didn't import that one, do I?

Yes... From a certain point of view...

Those are syntactic sugar over.

let prev_active;

controllerState.subscribe(val => {
    prev_active = (val.current > 0);
})

Thanks to them, you can access store as any ordinary variable now.

Problem is... it didn't work. We need a bit more code here.
First let me make only panel which index prop is equal to $controllerState.current visible.

In FormPanel.svelte:

<script>
    import { controllerState } from "./globals.js";

    export let index;
</script>
{#if index == $controllerState.current }
<div class="multiform-panel">
    <slot></slot>
</div>
{/if}

And add index prop inside App.svelte.

This is how, it should look like.

<MultiPanelForm last_page={2}>
    <FormPanel index={0}>
        <!-- First panel -->
    </FormPanel>
    <FormPanel index={1}>
        <!-- Second panel -->
    </FormPanel>
    <FormPanel index={2}>
        <!-- Third panel -->
    </FormPanel>
</MultiPanelForm>

To make switching working open MultiFormController.svelte and in <script> block add few things.

function next_panel() {
    $controllerState.current = $controllerState.current + 1;
}

function prev_panel() {
    $controllerState.current = $controllerState.current + 1;
}

And add to coresponding buttons those events. Just like this:

<button type="button" on:click={prev_panel}>Previous</button>

And similarly to the Next button.
Something is not right. Buttons do not change as they should. How to deal with the updating variables "prev_active" and "next_active".

They were assigned once, and we did not change them. Will they recalculate automatically?

Nope. Nothing can be that easy! We will need to sweat a little to do that.
Inside MultiFormPanel.svelte, in <script> block. Time for some black-magic.

Ready!

Set! Go!

    let prev_active = $controllerState.current > 0;
    let next_active = $controllerState.current < last_page;

You have this. Now to make everything reactive we will need to change things. How?

Here comes, one of the best concepts in Svelte.
Just swap let with $: and you are ready to go.

$: prev_active = $controllerState.current > 0;

Wait!? What? How?

Remember, what I told you about Svelte being compiler and not framework. It gives them ability to alter language itself.
It is most often pretty much not problematic. Here we get almost for free, full-blown reactive variable.
Anytime $controllerState.current will change, it will update prev_active and next_active.

Final stuff

git add src\*
git commit

And write some useful git commit. Now press <ESC> and type in :wq! and press <Enter>. If you need more help, read up on editing with Vim or change your default vim editor to nano or something...

Thanks for reading. We will be back for more!

Some stats.

React:

  • took me: 120 lines of code to archieve almost the same.
  • compiles on my bad laptop in: 70 seconds or so, when used with Parcel
  • Involve merging children and enhancing them with props. If not Typescript and Parcel I would be very angry

Svelte:

  • 40 lines of code in components.
  • Easy CSS with built-in support for modules
  • Resulting code generated is much smaller.
  • Easy to use, easy to read version.
  • It is possible to use typescript in Svelte too!

Top comments (0)