I designed both the Add Transaction and Add Category pages to be similar. Both pages utilize the form tag. Inside the form element, I created two columns to separate the label and the input fields.

Svelte Directory Structure

In Svelte, the directory represents the URL. For example, if a website has a page /foo/bar, we need to create a new directory in Svelte src/routes/foo/bar and create a +page.svelte and any other files needed.

In this project, the URL to add transaction and add category is /categories/add and /transactions/add. So, we need to create new directories categories/add and transactions/add.

Directory Structure

After that, we need to create a +page.svelte in each directory to define the page layout. +page.server.js and categories.scss are not mandatory for every page, but we use it to enhance the experience. +page.server.js is a file that contains code that will be executed by the frontend server, while categories.scss is a Sassy CSS file.

Add Transaction Page

Add Transaction Page

This is what the Add Transaction page looks like. And I will show the content of +page.svelte of Add Transaction page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script>
    export let data;
    const { categories } = data;
</script>
<style src="./transaction.scss"></style>

<main class="admin__main">
    <h2>Add Transaction</h2>

    <form method= "POST" class="theForm" action="?/addTransaction">
        <label class="theForm__label" for="amount">Amount</label>
        <input type="text" class ="theForm__input" placeholder="Insert amount" name="amount" id="amount" required>

        <label class="theForm__label" for="category_id">Category</label>
        <select class ="theForm__input" name = "category_id" id ="category_id" required>
            {#each categories as category}
                <option value="{category.category_id}">{category.category_name}</option>
            {/each}
        </select>

        <label class="theForm__label" for="transaction_date">Date</label>
        <input type="date" class ="theForm__input" id="transaction_date" name="transaction_date" required>

        <label class="theForm__label" for="note">Notes</label>
        <textarea placeholder="Insert notes" id="note" class ="theForm__input"  name="note" rows="5" cols="30"></textarea>

        <button class ="theForm__input theForm__button">Submit</button>
    </form>
</main>

Let’s break the code down.

1
2
3
4
<script>
    export let data;
    const { categories } = data;
</script>

This is the code to store the category list into categories variable. Where to get the category list data ? I will get back to it later.

The rest of the code is a code to import SCSS file and the HTML layout of the page. The form action in the code above redirects to ?/addTransaction. It means the form will call a Svelte’s form action named addTransaction. Form action is a feature in Svelte to intercept the default handler with its own handler. It is implemented in +page.server.js.

1
2
3
4
5
<select class ="theForm__input" name = "category_id" id ="category_id" required>
    {#each categories as category}
        <option value="{category.category_id}">{category.category_name}</option>
    {/each}
</select>

This code will iterate the categories variable obtained from the script above and transform each of them into a dropdown option.

Now I will show the content of +page.server.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import 'dotenv-expand/config'

export const load = async ({fetch}) => {
    const fetchCategories = async() => {
        const categoryRes = await fetch(`${process.env.HOST_URL}/users/${process.env.USER_ID}/categories/list`)
        
        const categoryData = await categoryRes.json()
        return categoryData.category_list      
    }

    return {
        categories: fetchCategories()
    }
}

export const actions = {
    addTransaction: async ({request}) => {
        const formData = await request.formData()

        const amount = formData.get('amount')
        const categoryId = formData.get('category_id')
        const transactionDate = formData.get('transaction_date')
        const notes = formData.get('note')

        const toSend = {
            amount: amount,
            category_id: categoryId,
            transaction_date: transactionDate,
            note: notes ? notes: null
        }

        const addRes = await fetch(`${process.env.HOST_URL}/users/${process.env.USER_ID}/transactions/add`, {
            method: 'POST',
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(toSend)
        })
        const add = await addRes.json()
        
        console.log (JSON.stringify({
            toSend
        }), add)
    }
}

Let’s breakdown the code

1
import 'dotenv-expand/config'

I use dotenv-expand library in this project. It enables the system to read configurations from .env file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export const load = async ({fetch}) => {
    const fetchCategories = async() => {
        const categoryRes = await fetch(`${process.env.HOST_URL}/users/${process.env.USER_ID}/categories/list`)
        
        const categoryData = await categoryRes.json()
        return categoryData.category_list      
    }

    return {
        categories: fetchCategories()
    }
}

load is a function that is executed by Svelte before the page loads. In this code snippet, it will get a list of categories from the backend server and return the result to +page.svelte. Remember this code snippet ?

const { categories } = data;

The result of load function is exported to the code above.

What about the ${process.env.HOST_URL} and ${process.env.USER_ID} values ? process.env is a syntax to retrieve configuration data from .env file, where I store the HOST_URL and USER_ID variables. In short, I use values stored in .env file to construct a URL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export const actions = {
    addTransaction: async ({request}) => {
        const formData = await request.formData()

        const amount = formData.get('amount')
        const categoryId = formData.get('category_id')
        const transactionDate = formData.get('transaction_date')
        const notes = formData.get('note')

        const toSend = {
            amount: amount,
            category_id: categoryId,
            transaction_date: transactionDate,
            note: notes ? notes: null
        }

        const addRes = await fetch(`${process.env.HOST_URL}/users/${process.env.USER_ID}/transactions/add`, {
            method: 'POST',
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(toSend)
        })
        const add = await addRes.json()
        
        console.log (JSON.stringify({
            toSend
        }), add)
    }
}

This is the code to export actions. The addTransaction function defines /?addTransaction action in Add Transaction form. addTransaction function sends an HTTP request to the backend server to add a new transaction and log both the request and the result.

Add Category Page

Add Category Page

The layout of Add Category page is similar to Add Transaction page. It has an input for name, dropdown for type of category, and a color picker for category color.

Let’s take a look at the +page.svelte content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<style src="./categories.scss"></style>

<main class="admin__main">
    <h2>Add Category</h2>
    <form method= "POST" class="theForm" action="?/addCategory">
        <label class="theForm__label" for="name">Name</label>
        <input type="text" class ="theForm__input" placeholder="Insert name" name="name" id="name" required>

        <label class="theForm__label" for="transaction_type">Type</label>
        <select class ="theForm__input" name = "transaction_type" id ="transaction_type" required>
            <option value="EARNING">EARNING</option>
            <option value="EXPENSE">EXPENSE</option>
        </select>

        <label class="theForm__label" for="color">Color</label>
        <input type="color" class ="theForm__input" id="color" name="color" required>

        <button class ="theForm__input theForm__button">Submit</button>
    </form>
</main>

There is no import script here because there is nothing to get before the page loads.

Now, let’s take a look of the content of +page.server.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'dotenv-expand/config'

export const actions = {
    addCategory: async ({request}) => {
        const formData = await request.formData()
        
        const name = formData.get('name')
        const transactionType = formData.get('transaction_type')
        const color = formData.get('color')

        const toSend = {
            category_name: name,
            category_color: color,
            transaction_type: transactionType
        }

        const res = await fetch(`${process.env.HOST_URL}/users/${process.env.USER_ID}/categories/add`, {
            method: 'POST',
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(toSend)
        })

        const response = await res.json()
        return;
    }
}

Similar to Add Transaction page, there is a form action that handles a form action, which is addCategory. It creates an HTTP request to add a new category to the backend server. Unlike Add Transaction page, there is no load function implemented.

Resources

I put several links to get more understanding about the concept mentioned here:

  • Svelte’s form actions: link
  • Page load using Svelte: link
  • Dotenv-expand library: link
  • Sass: link
  • Tabunganku Frontend repository: link