Data Binding and Event Forwarding

In this module, we will learn how to pass data from a child component to the parent using data bindings and event handling.
Table of Contents
Data Binding and Event ForwardingView the code for this module.
Course Version History
  • Nov. 21, 2022 - Updated to SvelteKit v1.0.0-next.549. Changed index.svelte and __layout.svelte to +page.svelte and +layout.svelte respectively.

In the last video, we went over passing data from a parent component to the child component using props, but what if we want to pass data upstream from child to parent? In this video, we will learn how to do just that.

The first topic we will go over is component bindings. Just as you can bind to properties of DOM elements, you can also bind properties to component props. Let's take a look at a scenario where we would want to do this.

In our layout file, we have a variable called cartItems which is an array of objects representing each item in our cart. Each object contains a name, src, price, and quantity. We are displaying the total number of items in our cart in the header, and we have this banner which gives us the option to add a new item into our cart. Clicking the ‘Add to Cart’ button calls the addItemToCart method which adds the newItem object to our cartItems array.

In our layout we are also importing a component called ShoppingCart. This component is wrapped in an if block and will conditionally show based on the value of showCart, which is false until we click our cart button.

<script>
  import ShoppingCart from '$lib/ShoppingCart.svelte';
 
  let showCart = false;
  let cartItems = [
    {
      name: 'Cup',
      src: 'https://cdn.shopify.com/s/files/1/0434/0285/4564/products/Cup-front-black.png?v=1623159405',
      price: '$10.00',
      quantity: 1,
    },
    {
      name: 'Quarter Zip',
      src: 'https://cdn.shopify.com/s/files/1/0434/0285/4564/products/QZSide-Model.png?v=1623256247',
      price: '$30.00',
      quantity: 1,
    },
  ];
  let newItem = {
    name: 'T-Shirt',
    src: 'https://cdn.shopify.com/s/files/1/0434/0285/4564/products/Front-NoModel_ec3be051-d579-4c03-b55b-64449d0b0445.png?v=1623255893',
    price: '$80.00',
    quantity: 1,
  };
  function addItemToCart() {
    cartItems = [...cartItems, newItem];
  }
</script>
 
<div>
  <div>
    <nav class="flex items-center border-b border-zinc-700 p-4 lg:px-6">
      <button on:click="{()" ="">
        { showCart = true; }} class="uppercase text-white flex items-center" >
        <div>Cart</div>
        <div
          class="ml-2 flex h-5 w-5 items-center justify-center rounded-full bg-white text-xs font-bold text-black"
        >
          {cartCount}
        </div>
      </button>
    </nav>
    <div class="flex items-center justify-between bg-zinc-900 px-6 py-1">
      <div class="relative flex items-center">
        <img src="{newItem.src}" class="mr-2 h-10 bg-white" alt="cup" />
        <p class="font-medium uppercase">{newItem.name}</p>
        <div
          class="absolute left-0 top-0 -ml-4 flex items-center justify-center rounded-full bg-white px-1 py-px font-medium text-black"
          style="font-size: 10px;"
        >
          NEW!
        </div>
      </div>
      <button
        on:click="{addItemToCart}"
        class="bg-white/90 p-1 text-xs font-medium uppercase text-black"
      >
        Add to Cart
      </button>
    </div>
  </div>
  <div class="relative">
    {#if showCart}
    <ShoppingCart {cartItems} />
    {/if}
    <slot />
  </div>
</div>

Now, we see our cart is displaying each item in our cartItems array, which is being passed into the component as a prop. For each item in our cart we have two buttons to either increase or decrease the quantity of that item.

If you remember from the last module, props pass data in only one direction, from the parent component to the child. This means that if we add an item to our cart, since this logic is happening in the parent component, it will update the child component with the new value of cartItems. In the last video we also talked about how logic is scoped to the component. This means that our parent component is completely unaware of the changes we’re making to the data in the child component. To demonstrate this, we can increase the quantity of one of our items by clicking the "Plus" button in our cart. We will see that the cart component is reflecting this change, but the parent is completely unaware, so our total cart quantity is still four. What we need to do is bind these values so that cartItems in the parent component and cartItems in the ShoppingCart component are always in-sync.

Luckily, Svelte makes binding components extremely intuitive. All we have to do is go into our parent component where the ShoppingCart component is being used and add the bind directive, bind:, wherever we are passing in our prop.

<div class="relative">
  {#if showCart}
  <ShoppingCart bind:cartItems />
  {/if}
  <slot />
</div>

Now, if we increment the quantity of our first item, we see our total cart quantity is now reflecting this change. This is because the value of cartItems is bound between the parent and child, so anytime it changes in the parent, the child will be updated, and vice versa.

Currently in our example, the cart is being conditionally shown based on the value of showCart, which is false until we click our cart button. The issue is, right now we have no way to close our cart without refreshing the page. Let’s update our cart to close when we click the ‘close’ button. There are a couple different ways we can do this. We could bind the value of showCart like we did with or cartItems. this way if we change the value of showCart to false in our cart component, it will also update in our parent component. Or we can forward the click event from our cart to the layout using component event forwarding. By doing this, anytime the close button is clicked it will forward the event, and a click event will be emitted wherever the component is being used. The way we do this is by registering the click event, but not actually handling it in the component. Let’s move into our shopping cart component, and at our close button, let’s listen for the click event using the on directive, but not set it equal to anything. When we don't handle this click event, it will forward the click event to where we are using this component in our layout. Now we can listen to that on:click event in the parent component and toggle the value of showCart to false.

ShoppingCard.svelte
<button on:click class="text-sm uppercase opacity-80 hover:opacity-100">
  close
</button>
+layout.svelte
<ShoppingCart
  bind:cartItems
  on:click={() => {
    showCart = false;
  }}
/>

Now our cart component will emit that click event whenever the button is clicked, and when that happens we will change the value of showCart in the parent component to false, thus closing the cart.

Now, this example is extremely basic. Oftentimes, we need to communicate more than just a click event to the parent component. For instance, we may need to pass data from the child component to the parent component along with the click event.

Currently, we have a banner in our parent component which shows a highlighted product that we can add to our cart. Anytime this button is clicked, it calls our addToCart function and updates the value of cartItems to include the new item. But what if we want to move this banner out of the parent component and into the cart component, while keeping the logic in the parent? In this scenario we would not only need to forward the click event, but we would also have to pass the product data along with it so our parent component can add it to our cartItems array.

In SvelteKit, we can send data from the child component to the parent by dispatching a custom event from our component using createEventDispatcher(). To do this we have to import and call createEventDispatcher() when the component is initiated. First, we need to import createEventDispatcher from svelte, and set dispatch equal to createEventDispatcher(), which links dispatch to the component instance. Now, we can use dispatch to emit the name of the event as well as any data we want to send with it.

In our example, let's dispatch an event when the ‘Add to Cart’ button is clicked. We can do this by calling a function addItemToCart on the click event, which will create an event called addItemToCart with an object containing a single key, item, which will be our newItem variable.

ShoppingCart.svelte
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();
  let newItem = {
    name: 'T-Shirt',
    src: 'https://cdn.shopify.com/s/files/1/0434/0285/4564/products/Front-NoModel_ec3be051-d579-4c03-b55b-64449d0b0445.png?v=1623255893',
    price: '$80.00',
    quantity: 1,
  };
  function addItemToCart() {
    dispatch('addItemToCart', {
      item: newItem,
    });
  }
</script>

Now, anytime the button is clicked, this event will be dispatched to the parent component, but the app will not react to it yet. We need to listen for this event in the parent component using the on:addItemToCart directive. This directive is an attribute prefixed with on: followed by the event name that we are dispatching, in this case, addItemToCart.

Next let's listen for this custom event in our parent component by adding on:addItemToCart, and call our function addItemToCart whenever the event is dispatched. Since this function is being called with our custom event, we can pass event in as a parameter which allows us to access our product data, which exists under the key name detail. In our addToCart method, we can push event.detail.item into our cartItems array. Now our new item will be successfully added to our cart!

+layout.svelte
<script>
  function addItemToCart(event) {
    cartItems = [...cartItems, event.detail.item];
  }
</script>
 
<ShoppingCart bind:cartItems on:click="{()" ="">
  { showCart = false; }} on:addItemToCart={addItemToCart} /></ShoppingCart
>

In this example we have a single key, item, We can dispatch our data with as many keys as we'd like, allowing you to be extremely flexible in what data you dispatch with your event.

We have now learned how to send data from parent to child, how to bind data between components, and how to send data upstream from child to parent. In the next module, we will learn how to send data between nested components.