How to create custom events in Svelte

Kinanee Samson - Aug 16 '23 - - Dev Community

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.

// 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>
Enter fullscreen mode Exit fullscreen mode

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.

// 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>
Enter fullscreen mode Exit fullscreen mode

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

// 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>
Enter fullscreen mode Exit fullscreen mode

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.

// 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>
Enter fullscreen mode Exit fullscreen mode

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.

// 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}
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .