Skip to content

How to use svelte-form-libs, sveltestrap and firestore to save data.

Code

https://github.com/phptuts/firebase-sveltekit-recipe-site/tree/svelte-form-yt

Video

Libraries

Instructions

1. Install Svelte Form Libary

1
npm i svelte-forms-lib yup

2. Clean up the index.svelte page

3. Add Bootstrap app.html

1
2
3
4
5
6
7
8
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" />
<link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
      integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    />

4. Create the starter form in our add-recipe.svelte pageg.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import * as yup from "yup";
import { createForm } from "svelte-forms-lib";
import { Row, Col, Button, FormGroup, Input, Label } from "sveltestrap/src";

const schema = yup.object().shape({
  title: yup.string().required().min(4).max(50),
  description: yup.string().required().min(10).max(1000),
});

const { form, errors, handleChange, handleSubmit } = createForm({
  initialValues: {
    title: "",
    description: "",
  },
  validationSchema: schema,
  onSubmit: (values) => {
    alert(JSON.stringify(values));
  },
});

5. Create the HTML

 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
<Row>
  <Col>
    <FormGroup>
      <Label for="title">Title</Label>
      <Input
        on:change={handleChange}
        bind:value={$form.title}
        invalid={$errors.title.length > 0}
        type="text"
        name="title"
        id="title"
        placeholder="Recipe Title"
      />
      {#if $errors.title}
        <div class="invalid-feedback">{$errors.title}</div>
      {/if}
    </FormGroup>
  </Col>
</Row>

<Row>
  <Col>
    <FormGroup>
      <Label for="title">Description</Label>
      <Input
        on:change={handleChange}
        bind:value={$form.description}
        invalid={$errors.description.length > 0}
        type="textarea"
        name="description"
        id="description"
        placeholder="Recipe Description"
      />
      {#if $errors.description}
        <div class="invalid-feedback">{$errors.description}</div>
      {/if}
    </FormGroup>
  </Col>
</Row>
<Row>
  <Col>
    <Button on:click={handleSubmit} class="w-100" color="success">Submit</Button
    >
  </Col>
</Row>

6. Add the list of ingredients to our schema.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const schema = yup.object().shape({
  title: yup.string().required().min(4).max(50),
  description: yup.string().required().min(10).max(1000),
  ingredients: yup
    .array()
    .min(1)
    .max(10)
    .of(
      yup.object().shape({
        name: yup.string().required().min(2).max(10),
        unit: yup.mixed().oneOf(["n/a", "ounces", "cups", "pounds"]),
        amount: yup.number().min(1).max(30000),
      })
    ),
});

7. Add the default for the create form.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const { form, errors, handleChange, handleSubmit } = createForm({
  initialValues: {
    title: "",
    description: "",
    ingredients: [
      {
        name: "",
        units: "none",
        amount: 1,
      },
    ],
  },
  validationSchema: schema,
  onSubmit: (values) => {
    alert(JSON.stringify(values));
  },
});

7. Add the HTML For Ingredients and the button.

1
2
3
4
5
6
7
8
<Row class="mb-4 mt-4">
  <Col>
    <h2>Ingredients</h2>
  </Col>
  <Col>
    <Button class="float-end" color="primary">Add Ingredient</Button>
  </Col>
</Row>

8. Add the loop HTML For the ingredients

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
{#each $form.ingredients as ingredient, i}
  <Row>
    <Col sm={5}>
      <FormGroup>
        <Label for={`ingredients_${i}_name`}>Name</Label>
        <Input
          on:change={handleChange}
          bind:value={$form.ingredients[i]["name"]}
          invalid={$errors.ingredients[i]["name"] &&
            $errors.ingredients[i]["name"].length > 0}
          type="text"
          name={`ingredients[${i}].name`}
          id={`ingredients_${i}_name`}
          placeholder="Name"
        />
        {#if $errors.ingredients[i]["name"]}
          <div class="invalid-feedback">{$errors.ingredients[i]["name"]}</div>
        {/if}
      </FormGroup>
    </Col>
    <Col sm={4}>
      <FormGroup>
        <Label for={`ingredients_${i}_units`}>Units</Label>
        <Input
          on:change={handleChange}
          bind:value={$form.ingredients[i]["units"]}
          type="select"
          name={`ingredients[${i}].units`}
          id={`ingredients_${i}_units`}
        >
          <option value="none">None</option>
          <option value="pounds">Pounds</option>
          <option value="ounces">Ounces</option>
          <option value="cups">Cups</option>
        </Input>
      </FormGroup>
    </Col>

    <Col sm={2}>
      <FormGroup>
        <Label for={`ingredients_${i}_amount`}>Amount</Label>
        <Input
          on:change={handleChange}
          bind:value={$form.ingredients[i]["amount"]}
          invalid={$errors.ingredients[i]["amount"] &&
            $errors.ingredients[i]["amount"].length > 0}
          type="number"
          min="1"
          max="300000"
          name={`ingredients[${i}]amount`}
          id={`ingredients_${i}_amount`}
        />
        {#if $errors.ingredients[i]["amount"]}
          <div class="invalid-feedback">{$errors.ingredients[i]["amount"]}</div>
        {/if}
      </FormGroup>
    </Col>
    <Col sm={1}>
      <i class="fas fa-trash" on:click={() => removeIngredient(i)} />
    </Col>
  </Row>
{/each}

<style>
.fa-trash {
    margin-top: 35px;
    color: rgb(254, 131, 131);
    cursor: pointer;
    font-size: 25px;
  }
</style>

9. Add an add function to add ingredients and attach it to the button.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const addIngredient = () => {
  $form.ingredients = $form.ingredients.concat({
    name: "",
    units: "none",
    amount: 1,
  });

  $errors.ingredients = $errors.ingredients.concat({
    name: "",
    units: "",
    amount: "",
  });
};
1
2
3
<button class="float-end" on:click="{addIngredient}" color="primary">
  Add Ingredient
</button>

10. Add a remove column button.

1
2
3
4
const removeIngredient = (index: number) => {
  $form.ingredients = $form.ingredients.filter((i, j) => j !== index);
  $errors.ingredients = $errors.ingredients.filter((i, j) => j !== index);
};
1
<i class="fas fa-trash" on:click={() => removeIngredient(i)} />

11. Add the error message for when no ingredients are present.

1
2
3
{#if typeof $errors.ingredients === "string" && !$errors.ingredients.includes("[object")}
  <Alert color="danger">{$errors.ingredients}</Alert>
{/if}

12. Create a file called db. Here we'll add types and save db function.

 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
import firebase from "firebase/app";
import { firestore } from "./firestore";

export type Ingredient = {
  name: string;
  units: string;
  amount: number;
};

export type Recipe = {
  title: string;
  description: string;
  userId: string;
  createdAt: firebase.firestore.Timestamp | firebase.firestore.FieldValue;
  updatedAt: firebase.firestore.Timestamp | firebase.firestore.FieldValue;
  ingredients: Ingredient[];
};

export type RecipeForm = {
  title: string;
  description: string;
  ingredients: Ingredient[];
};

export const createRecipe = async (recipeForm: RecipeForm, userId: string) => {
  const recipe: Recipe = {
    ...recipeForm,
    userId,
    createdAt: firebase.firestore.FieldValue.serverTimestamp(),
    updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
  };
  const db = await firestore();
  await db.collection("recipes").add(recipe);
};

13. Edit the submit function to save.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  import { createRecipe } from "../db";

...
onSubmit: async (values) => {
      try {
        await createRecipe(values, $authStore.user.uid);
        alert("Saved Recipe");
      } catch (e) {
        alert("error saving");
        console.log(e);
      }
    },

14. Firestore Rules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /recipes/{document=**} {
      allow read: if true;
      allow create: if request.auth != null;
    }

    match /private/{document=**} {
      allow read, write: if request.auth != null;
    }

    match /public/{document=**} {
      allow read: if true;
    }
  }
}