DEV Community

Nuno Alexandre
Nuno Alexandre

Posted on • Updated on

Svelte is Great, How I build forms on it with a store for state management πŸ”₯

About a week ago I went out to the wild discovering another frontend framework to try and learn, and I end up founding Svelte, actually, I already have tried but not that in dept. Have been using it to build a side project, made me learn a lot about it. This blog post I will share with you a bit about and how I am using svelte to create forms.

svelte-logo

What is svelte?

Svelte is a framework/compiler with an abstraction logic that updates the DOM making it fast and simple. Let me show you an example, of how simple it is:

<script>
  let name = 'Nuno';
</script>

<p>My name is {name}</p>

(App.svelte)

What you see in this example seams like HTML right ? That is true the svelte syntax is very similar to HTML but with some cool reactivity effects.

In this example, we can see a script tag that holds the name variable and on the <p> block we are referencing the name, so the result will be:

My name is Nuno

So as you can see svelte files holds javascript and HTML at the same time, like VUE, but with a different syntax.

Another cool thing is that you can even declare styling on the svelte file also, like so:

<style>
  p { color: red; }
</style>

Basic stuff right ?

There is a lot of the syntax to learn but if you want to learn them would be better to check out the svelte documentation, here. Although in the following sections, I will show you how to use svelte with forms, in this way you will learn more in dept about svelte and at the same time how to create reusable form and components.

Lets create a Form component

Creation page

<script>
  import { saving, save, errors, success } from "../stores/product.js";
  import Form from './ProductForm.svelte';

  let form = {
    name: '',
    price: 0
  };

  const handleSubmit = () => {
    save(form);
  };
</script>

<Form bind:form={form} on:submit={handleSubmit} errors={$errors} loading={$saving} />

(create-product.svelte)

This code holds the product creation page, and as you can see we have a script tag importing some values and methods from the product store (I will get on that in a minute), as well a Product Form component (that can be reused on the creation of the product as well as on the product editing page).

In the HTML part, you can see that we are passing the form object into the Form component with bind:form={form}, this will bind the form object, meaning that when the form object updates inside the Form component, it will be reflected on the creation page as well.

The on:submit={handleSubmit} is an event listener, the will trigger whenever the Form fires/dispatchs submit event.

The errors={$errors} loading={$saving} props are values that we receive from the store and are passed to our Form component (the $ is just a shortcut to subscribe to the store, I will explain more in dept bellow).

Form component

<script>
  import { createEventDispatcher } from 'svelte';
  import Field from "../components/Field.svelte";

  const dispatch = createEventDispatcher();

  export let form; // prop
  export let errors; // prop
  export let loading; // prop
</script>
<form on:submit|preventDefault={() => dispatch('submit')}>
  <Field
    name="name"
    label="Name"
    bind:value={form.name}
    errors={errors} />
  <Field 
    name="price" 
    label="Price" 
    bind:value={form.price} 
    errors={errors} />
    <div class="mt-4 flex justify-between">
      <button
        type="submit"
        class={loading ? "button loading" : "button success"}
        disabled={loading}>
        Save
      </button>
    </div>
</form>

(ProductForm.svelte)

The Product Form looks fairly simple, in the script tag, we are importing the Field component, to use it as our inputs but already abstracted with the markup and necessary logic to work. Then we are importing createEventDispatcher that will provide us with a dispatch letting us communicate with a parent component. To receive the props from a parent component, we are exporting some variables (form, errors, loading), using export is required, by using it we are tailing the parent and svelte that we want to receive that as props.

In the HTML part, we are defining a form with an on:submit event and passing a preventDefault modifier (allows us to prevent the default browser behavior when the event fires), in fact, we can add as many modifiers as we want, see more here, as a value, we are passing a function that will dispatch the submit event. If you remember on the Product creation form we are using on:submit={handleSubmit} to listen to for the submit event from the child. So by dispatching events from the children to the parent, we are able to communicate between them.

We are also declaring some Field components that will receive the value that will be bound, and also the errors prop, that will be used to display errors on the input itself.

To finish the Form component we have a submit button which has the class props, so when the user [prop]={ ...expression }, the value of the prop will be always the result of the expression.

Field Component

<script>
  export let value;
  export let label;
  export let name;
  export let errors = {};

  $: hasErrors = errors && errors[name];
  $: classes = hasErrors ? "input with-error" : "input";

  const handleInput = e => {
    value = e.target.value;
  };
</script>

<div class="mb-4">
  <input
    type="text"
    placeholder={label}
    {name}
    class={classes}
    bind:value
    on:input={handleInput} />
    {#if hasErrors}
      <p class="text-red-500 text-xs italic mt-3">{errors[name].message}</p>
    {/if}
</div>

In the Field component we have some props as expected, two computed variables, when we have $: [varName] = {expression} will create a variable that will recompute when the component updates (like a computed method in vue). Then we are creating a method to handle the input change, this means that on:input={handleInput} will listen to any change on the input and when it happens it will trigger the handleInput method, that therefore will update the value, since the value is being bound with the parent value will be updated as well.

For the first time we can see an if statement, as there are others (see here)

The Store

import { writable } from 'svelte/store';
import * as api from 'api.js';

let errors = writable(null);
let saving = writable(false);
let success = writable(null);

const save = async (form) => {
  saving.set(true);
  errors.set(null);

  const response = await api.post('product', form);

  if (response.status >= 400) {
    saving.set(false);
    errors.set(response.data.errors);
    return;
  }

  success.set(response.data.message);
  order.set(response.data.data);

  saving.set(false);
};

export default { errors, saving, success, save}

(product.js)

To abstract logic from components we often need to create a store, this can also be useful when we need to share information across multiple components. Svelte has a couple of functions that help us create stores, like writable, readable, derived. When accessing store variables on a svelte component we normally use $ prefix, which will set the appropriate store subscription with that component, making us access it without any more logic.

The writable returns a object that has additional set and update methods, more about here.

Our store has multiple writable(s) like errors, success, saving that will be used later on our components with $ prefix, to be subscribed. We are also declaring a save method that is responsible to send our product to the API and set the errors or success accordingly.

Conclusion

There is a lot more about Svelte than what I have shown you, although I think this will be a good start. If you have any suggestion feel free to reach out to me.

easy

You can see also the blog post on my blog here.

Disclaimer: Writing blog posts is a big challenge for me if you think that something is wrong, feel free to reach out to me.

Top comments (0)