How to create custom events in Svelte
Tags: svelte, javascript, webdev
Published: 2023-08-16 11:07:43

Quite recently, I was working on a svelte project. I was had a form component and I observed that I was repeating much code with each new input I added to the form. I realized that I can't keep up with this because in the future this is going to be a nightmare. I was also using TailwindCSS, throw in a bunch of classes and you already have a mess. Okay, so I had an idea about how to face the situation. What if I created a generic Input element that can be reused inside the form? For simplicity's sake, the form and the input will be very basic. Here's a sample of our form before we split it.

```jsx
// form.svelte

<form>
  <div>
  <label 
    class="font-semibold text-sm text-gray-600 pb-1 block"
  >Email</label>
  <input
    type="email"
    class="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"
  />
  </div>
  <div>
  <label 
    class="font-semibold text-sm text-gray-600 pb-1 block"
  >Password</label>
  <input
    type="password"
    class="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"
  />
  </div> 
</form>
```

Do you see what I'm talking about here? The snippet above is really looking awkward and if we add let's say two more fields for `firstName` and `lastName` it's going to be unbearable. That's a lot of dead code that will need to be updated. So let's split this code.

```jsx
// input.svelte

<script>
   export let label, type;
</script>

 <div>
  <label 
    class="font-semibold text-sm text-gray-600
    pb-1 block"
  >{label}</label>
  <input
    {type}
    class="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"
  />
  </div>
```

And our form will now look a bit better, but we are far from done.

```jsx
// form.svelte

<script>
  import Input from './Input.svelte';
  const inputs = ['email', 'password'];
</script>

<form>
  {#each inputs as input}
     <Input
       type={input}
       label={input.toUpperCase()}
    />
  {/each}
</form>
```
We now need to bind the data the user enters into the form to a value we can work with in Javascript, however a slight challenge presents itself because we can't use the built-in `bind:value` directive from Svelte. The state of our data will not be updated, so what do we do?

We need to create a custom event on the Input element. The custom event will be emitted whenever there's an input event on the element. Svelte provides the `on:input` event which is triggered when the state of the input's value changes. We can attach a listener to this event and the listener will dispatch a custom event every time it's called. Let's see the snippet for that.

```jsx
// input.svelte

<script>
export let label, type;
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher()
</script>

<div>
  <label 
    for={label.toLowerCase()} 
    class="font-semibold text-sm text-gray-600 pb-1 block"
  >{label}</label>
  <input
    {type}
    on:input={(e) => {dispatch('input', e.target.value)}}
    id={label.toLowerCase()} 
    class="border rounded-lg px-3 py-2 mt-1 mb-5 text-sm w-full"
  />      
</div>
```

We import `createEventDispatcher` from svelte, this will allow us to create a dispatcher for this component. We create a variable `dispatch` which is equal to the result of calling `createEventDispatcher`. Then we attach a listener for the input event on the component. So anytime the user enters a value into the input field we dispatch our own custom `input` event with the `dispatch` function. This function accepts two arguments, the first is a string that represents the event we want to dispatch. The second argument is any data we want to forward to any listeners attached to the event. Now we need to handle this event from the parent component.

```jsx
// forms.svelte

<script>
  import Input from './Input.svelte';
  const inputs = ['email', 'password'];
  $:user = {
    email: '',
    password: ''
  }
</script>

<form 
   class="bg-white shadow w-full rounded-lg divide-y divide-gray-200"
>
  {#each inputs as input}
   <div class="px-5 py-7">
    <Input 
      type={input}
      label={input.toUpperCase()}
      on:input={(e) => user[input] = e.detail}
    />
  </div>
  {/each}
```
On the parent component which is the form where we imported the `Input` component into. We now listen for an `input` event, remember that the `Input` component dispatches an `input` event anytime the user types into the form field with whatever value inside the form field as data. This data can be captured from the event object, it resides in the `detail` property on the event object, you can then use it how you see fit.

I just encountered this problem and thought of sharing this solution I came up with. I hope you find this useful, please leave your thoughts about our approach to this problem and also tell us if you handled this situation differently.