HB's Thoughts

I try to think really hard.

3/18/2024

Vue emits with parameters

Passing events in Vue from a component back to the one that calls it is done with emits. The emits can also be done with Pinia or another library for state management, but this will be for another article.

What is a page and what is a component?

I am often asked: 'Should I extract this code into components or leave it on the page?'. I have built myself a basic rule - if I have the question, whether this code should become a component, it should become a component. I also group the components by certain characteristics. I often give the @layer directive of TailwindCSS as an example when someone wonders how to group their components.

I divide the components into 2 types:

  1. Speculators: Those that do not perform any business logic, but only draw and/or transfer data;
  2. Workers: Those that perform secondary processing of the incoming parameters and add business logic, which they return to the calling component.

Workers can often call other workers or speculators, while Speculators usually work independently.

How are parameters passed?

Let's make an example. We have a component (Speculator) that shows on the screen how many more product pages are left before the last page is reached. We call it LoadMore.vue. The input parameters of the component are 2: 'which number is the current page' and 'how many are the total number of pages'. The component does not perform any business logic. It calculates how many more pages are left and draws them.

// components/utilities/LoadMore.vue

<script lang="ts" setup>
defineProps<{
  page: number; // current page
  totalPages: number; // total number of pages
}>();
</script>

<template>
  <p>{{ `There are ${(totalPages - page)} more page${(totalPages - page) > 1 ? 's' : ''}` }}</p>
</template>

Direct event passing

Let's rework our component a bit so that when it is clicked, it passes the event to the calling component, which fires a function.

// components/utilities/LoadMore.vue

<script lang="ts" setup>
  defineProps<{
    page: number;
    totalPages: number;
  }>();
</script>

<template>
  <div class="col-12 text-center q-mt-md q-mb-xl">
    <button @click="$emit('loadMore', page)"
            :label="`There are ${(totalPages - page)} more page${(totalPages - page) > 1 ? 's' : ''}`"
            class="btn btn-action btn-action--micro" />
  </div>
</template>

This is a direct way to pass the event. When the button is clicked, the loadMore event and the page parameter are passed to the calling component.

// pages/Products.vue

<script lang="ts" setup>
  import LoadMore from '@/components/utilities/LoadMore.vue';
  import { ref } from 'vue';

  const page = ref(1);
  const totalPages = ref(10);

  const loadMore = (page: number) => {
    console.log(`Load more products from page ${page}`);
  };
</script>

<template>
  <div>
    <LoadMore :page="page" :totalPages="totalPages" @load-more="loadMore" />
  </div>
</template>

I always use kebab-case for emitted events.

Define emit

When you want to make some changes to the parameters you are passing before emitting the event, you will need to define the emit.

// components/utilities/LoadMore.vue

<script lang="ts" setup>
  defineProps<{
    page: number;
    totalPages: number;
  }>();

  const emit = defineEmits(['loadMore']);

  /**
  * Change the page number and emit "loadMore" with the updated number.
   * @param {number} page - The current page number
   */
  const changePage = (page: number) => {
    const nextPage = page + 1;

    emit('loadMore', nextPage);
  };
</script>

<template>
  <div class="col-12 text-center q-mt-md q-mb-xl">
    <button @click="changePage(page)"
            :label="`There are ${(totalPages - page)} more page${(totalPages - page) > 1 ? 's' : ''}`"
            class="btn btn-action btn-action--micro" />
  </div>
</template>

This way we now have a lot of control over the output parameters.

// pages/Products.vue

<script lang="ts" setup>
  import LoadMore from '@/components/utilities/LoadMore.vue';
  import { ref } from 'vue';

  const page = ref(1);
  const totalPages = ref(10);

  const loadMore = (page: number) => {
    console.log(`Load more products from page ${page}`);
  };
</script>

<template>
  <div>
    <LoadMore :page="page" :totalPages="totalPages" @load-more="loadMore" />
  </div>
</template>

This example is just illustrative. If a similar change to the page number is being made, it should be logic in the calling component. But for our example, it's a great way to see how you can take control of the returned data.


If you have an opinion or questions about the article, don't hesitate to share them.

Join the open discussion on GitHub.


Competence: Elementary
Tags: Vue