Enda Lee 2022
fork
the start site from this repository.sample.env
to .env
and configure for your database.npm install
npm run dev
In this tutorial you will add new products the Database via an HTTP POST
request to the API. The body of the POST request will include the values for a new product which need to be validated and inserted.
The web API (Supabase) will use SQL commands to accomplish the above but that will be hidden from users of the API who will only see the HTTP interface.
When http://localhost:5173 loads first, the empty default page looks like the following.
This page shows the current list of products (and categories). This lab will enable new products to be added via a form.
Make sure that your Supabase database instance is setup and running
The button will be displayed under the products list:
Open the home page, src/routes/routes/+page.svelte
, add a button link:
<a id="AddProductButton" class="btn btn-primary" href="/addproduct" role="button">New Product</a>
You can see that the button links to /addproduct
. Add this route to the routes
folder:
addproduct
.+page.svelte
+page.server.js
+page.svelte
)This is a Bootstrap 5 styled form, which will look like this when displayed:
<!-- Main Content - Products etc. -->
<div class="row">
<!-- Page Header -->
<h2 class="mt-5">Add Product</h2>
</div>
<div class="row">
<!-- Product Form -->
<form
<!-- POST method supports sending form data in request body -->
method="POST"
<!-- This is where data will be sent when the form is submitted -->
action="?/addproduct"
>
<div class="row mb-3">
<label for="category_id" class="form-label">Category:</label>
<div class="col-sm-8">
<select id="category_id" class="form-select" name="category_id">
<option value="0">Choose a category</option>
{#each $categories as cat}
<option value={cat.id}>{cat.category_name}</option>
{/each}
</select>
</div>
</div>
<div class="row mb-3">
<label for="product_name" class="form-label">Name:</label>
<div class="col-sm-8">
<input
id="product_name"
type="text"
class="form-control"
name="product_name"
value=""
/>
</div>
</div>
<div class="row mb-3">
<label for="product_description" class="form-label">Description:</label>
<div class="col-sm-8">
<input
id="product_description"
type="text"
class="form-control"
name="product_description"
value=""
/>
</div>
</div>
<div class="row mb-3">
<label for="product_stock" class="form-label">Stock:</label>
<div class="col-sm-8">
<input
id="product_stock"
type="number"
class="form-control"
name="product_stock"
value=""
/>
</div>
</div>
<div class="row mb-3">
<label for="product_price" class="form-label">Price:</label>
<div class="col-sm-8">
<input
id="product_price"
type="number"
min="0.00"
max="10000.00"
step="0.01"
class="form-control"
name="product_price"
value=""
/>
</div>
</div>
<!-- productId is a hidden field value is not required but set = 0-->
<input id="id" type="hidden" value="0" />
<div class="mb-3">
<button type="submit" class="btn btn-primary"> Add Product </button>
<a href="/" class="btn btn-secondary"> Cancel </a>
</div>
</form>
<!-- End form-->
</div>
</div>
The category select box must be filled so that they can be selected. Add the follow script block at the top to import categories
from the product store
<script>
import { categories, getAllCategories } from '../../stores/productsStore.js';
// Update list of categories (used in form)
getAllCategories();
</script>
The {#each}{/each}
block to add an option for each category is already included in the HTML:
The Add Product form has two attributes which define what happens after the **Add Product ** button is clicked.
method="POST"
will result in a POST
request to the server with the form data contained in the body of the request.action="?/addproduct"
defines the form action
- the script/ function which will handle the data sent.addproduct
action handler
This is defined in the server-side scriptaddproduct/+page.server.js
This script doe the following:
addProduct
form action
. This code executes when the form is submittedaddProduct
doe the following:
formData
- sent in the request body (as it an HTTP POST)addNewProduct(product)
passing the valid product. Afterwards return the successful resultRead the comments for details.
// Import sveltekit dependencies
// @ts-ignore
import { invalid, redirect } from "@sveltejs/kit"
// Import addNewProduct function from the product store
import {addNewProduct} from '../../stores/productsStore';
// The form action handler(s)
export const actions = {
// This is where the form sends its data
// @ts-ignore
addproduct: async ({request }) => {
// @ts-ignore
let success = false;
// get data from the POST request
const form_data = await request.formData();
// read each value
const product = {
category_id: Number(form_data.get('category_id')),
product_name: form_data.get('product_name'),
product_description: form_data.get('product_description'),
product_stock: Number(form_data.get('product_stock')),
product_price: Number(form_data.get('product_price'))
}
// Basic validation check
if (product.category_id > 0 &&
product.product_name != '' &&
product.product_description != '' &&
product.product_stock > 0 &&
product.product_price > 0
) {
// Add the new product to Supabase
const result = await addNewProduct(product);
console.log('add product result: ', result)
// If validation passed - return the result
// This is a JS object containing the success state, a message, and a copy of the newly added product (from Supabase)
return {
success: true,
// The following annotation is to ignore TypeScript Syntax errors detected by ESLint and the Svelte VS Code extensions
// @ts-ignore
message: `New product added with id: ${result[0].id}`,
// @ts-ignore
product: result[0]
};
// If va;idation failed
// Return a response with Status 400
// set error state, a message, and return product (a copy of the form data)
} else {
return invalid(400, {
error: true,
message: 'validation failed',
product: product
})
}
}
};
addNewProduct()
function from stc/store/productsStore.js
// Function to call Supabase and insert a row
// @ts-ignore
export const addNewProduct = async (new_product) => {
const { data, error } = await supabase
.from('product')
.insert([
{ category_id: Number(new_product?.category_id),
product_name: new_product?.product_name,
product_description: new_product?.product_description,
product_stock: Number(new_product?.product_stock),
product_price: Number(new_product?.product_price)
},
])
// Select the newly inserted product (so that it can be returned)
.select();
if(error) {
return console.error(error);
}
// return inserted product
return data;
}
addform
pageThe form page is reloaded after the product is inserted so the result can be displayed. Note the <script>
block changes from 2.2 above and also the {#if} {:else} {/if}
block to control whether the result or form is displayed.
<script>
// Sveltekit form enhancements
import { enhance, applyAction } from '$app/forms';
// Import the store etc.
import { categories, getAllCategories } from '../../stores/productsStore';
// Access data returned from +page.server.js
// @ts-ignore
export let data;
// Access data returned from +page.server.js
// @ts-ignore
export let form;
// Update list of categories (used in form)
getAllCategories();
</script>
<!-- Main Content - Products etc. -->
<div class="container">
<!-- If the insert was sucessfull display the new product details-->
{#if form?.success}
<div class="row">
<!-- Page Header -->
<h2 class="mt-5">{form?.message}</h2>
</div>
<div class="row">
<div class="row mb-3">
<h6>Product ID: {form?.product.id}</h6>
</div>
<div class="row mb-3">
<h6>Category: {form?.product.category_id}</h6>
</div>
<div class="row mb-3">
<h6>Name: {form?.product.product_name}</h6>
</div>
<div class="row mb-3">
<h6>Description: {form?.product.product_description}</h6>
</div>
<div class="row mb-3">
<h6>Stock: {form?.product.product_stock}</h6>
</div>
<div class="row mb-3">
<h6>Price: {form?.product.product_price}</h6>
</div>
</div>
<!-- else show the form again (very simple error handling - should also show validation errors)-->
{:else}
<div class="row">
<!-- Page Header -->
<h2 class="mt-5">Add Product</h2>
</div>
<div class="row">
<!-- Product Form - note the form enhancements -->
<!-- Note that field values will be empty first time but will contain values entered if resubmission required (after failed validation) -->
<form
method="POST"
action="?/addproduct"
use:enhance={({ form }) => {
// Before form submission to server
return async ({ result, update }) => {
// After form submission to server
if (result.type === 'success') {
await applyAction(result);
}
if (result.type === 'invalid') {
await applyAction(result);
}
update();
};
}}
>
<div class="row mb-3">
<label for="category_id" class="form-label">Category:</label>
<div class="col-sm-8">
<select id="category_id" class="form-select" name="category_id">
<option value="0">Choose a category</option>
{#each $categories as cat}
<option value={cat.id}>{cat.category_name}</option>
{/each}
</select>
</div>
</div>
<div class="row mb-3">
<label for="product_name" class="form-label">Name:</label>
<div class="col-sm-8">
<!-- If form data exists display it (on error), display it. Otherwise blank -->
<input
id="product_name"
type="text"
class="form-control"
name="product_name"
value="{form?.product.product_name || ''}"
/>
</div>
</div>
<div class="row mb-3">
<label for="product_description" class="form-label">Description:</label>
<div class="col-sm-8">
<input
id="product_description"
type="text"
class="form-control"
name="product_description"
value="{form?.product.product_description || ''}"
/>
</div>
</div>
<div class="row mb-3">
<label for="product_stock" class="form-label">Stock:</label>
<div class="col-sm-8">
<input
id="product_stock"
type="number"
class="form-control"
name="product_stock"
value="{form?.product.product_stock || ''}"
/>
</div>
</div>
<div class="row mb-3">
<label for="product_price" class="form-label">Price:</label>
<div class="col-sm-8">
<input
id="product_price"
type="number"
min="0.00"
max="10000.00"
step="0.01"
class="form-control"
name="product_price"
value="{form?.product.product_price || ''}"
/>
</div>
</div>
<!-- productId is a hidden field value is not required but set = 0-->
<input id="id" type="hidden" value="0" />
<div class="mb-3">
<button type="submit" class="btn btn-primary"> Add Product </button>
<a href="/" class="btn btn-secondary"> Cancel </a>
</div>
<!-- Show if a validation error recorded in the result -->
{#if form?.error}
<div class="alert alert-danger col-sm-8" role="alert">validation failed: {form?.message}</div>
{/if}
</form>
<!-- End form-->
</div>
{/if} <!-- End the if block-->
</div>
Here is an example of the message displayed after a product is added successfully
This is an example of the form reloaded with errors:
Enda Lee 2022