KINTO Tech Blog
Development

Developing reusable components using props and slots in Vue

Cover Image for Developing reusable components using props and slots in Vue

Hello, I'm Chris, a frontend developer in the Global Development Division at KINTO Technologies.
When developing frontend components, you’ve probably heard about using props to pass necessary information. Popular frameworks such as Angular, React, Vue, and Svelte each have their own ways of implementing this functionally for passing data between components. Discussing all of these would make this article very lengthy, so I will focus on Vue, which is commonly used in our Global Development Division.
When considering component reusability, relying solely on props may not be sufficient. This is where slots come in. In this article, I will explain both props and slots, comparing their usage through practical examples.

Passing information with props

For example, let’s say you need to implement a reusable component for a table with a title. By passing props for the title, headers, and data, you can easily achieve this.

DataTable.vue
#Component
<template>
  <div>
    <h5>{{ title }}</h5>
    <table>
      <tr>
        <th v-for="header in headers" :key="header">{{ header }}</th>
      </tr>
      <tr v-for="(row, i) in data" :key="i">
        <td v-for="(column, j) in row" :key="`${i}-${j}`">{{ column }}</td>
      </tr>
    </table>
  </div>
</template>
<script>
export default {
  name: 'DataTable',
  props: {
    title,
    headers,
    Data
  },
}
</script>
Index.vue
#The parent that calls the component
<template>
  <DataTable :title="title" :headers="headers" :data="data"/>
</template>
<script>
// Omit the import statement for the component
export default {
  data() {
    return {
      title: 'Title',
      headers: ['C1', 'C2', 'C3', 'C4'],
      data: [
        {
          c1: `R1-C1`,
          c2: `R1-C2`,
          c3: `R1-C3`,
          c4: `R1-C4`,
        },
        {
          c1: `R2-C1`,
          c2: `R2-C2`,
          c3: `R2-C3`,
          c4: `R2-C4`,
        },
      ]
    }
  },
}
</script>

Using the code above, you can create the following table. (I've added some simple CSS styling, but that's not relevant to this article so I won't go into details here.)
Regular Table
To elaborate a bit on the use of props, Vue.js allows for simple type checking and validation of the data received from the parent component, even without using TypeScript. The following is an example, but for more details, please check the Vue.js official documentation. (Adding such settings to all sample code in this article would make it too long, so it’s omitted.)

DataTable.vue
<script>
export default {
  props: {
    title: {
      // A prop with a String type. When a prop can have multiple types, you can specify them using an array, such as [String, Number], etc.
      type: String,
      // This prop must be provided by the parent component.
      required: true,
      // Prop validation check. Returns a Boolean to determine the result.
      validator(value) {
        return value.startsWith('Title')
      }
    },
  },
}
</script>

Issues with using only Props

While props are indeed convenient for specifying types and validating values, they can sometimes feel inadequate depending on what you want to achieve. For example, have you ever encountered requirements like these?

  • Make the value displayed in table cell bold, italic, or change the text color based on conditions.
  • Display one or more action buttons in each row of the table, which can be disabled based on conditions.
    These requirements make sense, but implementing them only using props can lead to complex code.
    To change the style of a cell, you might need to pass the logic for determining the style as a prop, or add markers to the data objects indicating which values need style changes. Similarly, to add buttons to each data row, you would need to pass the button information as props to the component.
    For example, if you extend the initial sample code, it would look like this:
DataTable.vue
<template>
  <div>
    <h5>{{ title }}</h5>
    <table>
      <tr>
        <th v-for="header in headers" :key="header">{{ header }}</th>
      </tr>
      <tr v-for="(row, i) in data" :key="i">
        <!-- The class information is retrieved using a function that evaluates the received styles -->
        <td
          v-for="(value, j) in row"
          :class="cellStyle(value)"
          :key="`${i}-${j}`"
        >
          {{ value }}
        </td>
        <!-- If there are buttons, prepare a separate column for them -->
        <td v-if="buttons.length > 0">
          <button
            v-for="button in buttons"
            :class="button.class"
            :disabled="button.disabled(row)"
            @click="button.onClick(row)"
            :key="`${button}-${i}`"
          >
            {{ button.text }}
          </button>
        </td>
      </tr>
    </table>
  </div>
</template>
<script>
export default {
  props: {
    title,
    headers,
    data,
    // To receive the cell style logic as a prop
    cellStyle,
    // To receive button information as a prop
    buttons,
  },
}
</script>
Index.vue
<template>
  <!-- Passing the function that returns class information and button-related information as props -->
  <DataTable :title="title" :headers="headers" :data="data" :cell-style="cellStyle()" :buttons="buttons" />
</template>
<script>
export default {
  data() {
    return {
      // Other data information is omitted.
      Buttons: [
        {
          text: 'edit',
          class: 'btn-primary',
          disabled: (rowData) => {
            // Logic to determine whether to disable the button
          },
          onClick: (rowData) => {
            // Logic after button press
          },
        },
        {
          text: 'delete',
          class: 'btn-danger',
          disabled: (rowData) => {
            // Logic to determine whether to disable the button
          },
          onClick: (rowData) => {
            // Logic after button press
          },
        },
      ],
    }
  },
  methods: {
    cellStyle() {
      return (val) => {
        // The logic that returns the necessary style class information
      }
    }
  }
}
</script>

Here is a screenshot showing the result of applying styles to cell text based on conditions and disabling buttons as needed.
Table with style and buttons
However, if you want to further control the HTML structure within the cell (e.g., adding <p>, <span>, or <li> tags, etc.), you will need to pass the HTML code as a string prop to the child component and use v-html to render it. While v-html is a convenient method to render HTML, it can make the code harder to read when you include a lot of dynamic elements because you are constructing the HTML code as a string.
In summary, when using only props, you need to carefully consider how the child component will receive the data.

Using slots to complement the limitations of props

This is where the slot feature comes in. As explained in the Official Documentation, you can create a slot in a component and pass HTML information from the calling template into that slot, enabling you to implement the desired content within the specified slot area.
Props illustration
The illustration above is a conceptual image. The box on the left represents a component using props, while the box on the right represents a component using slots.
In the case of props, each entry point is narrow and the type is fixed, meaning that the developer can only pass information defined by the component. In contrast, slots provide a much wider entry point, giving the developer more control over what is passed to the component.
For example, let's try implementing a data table using slots as described above.

DataTable.vue
<template>
  <div>
    <!-- default slot -->
    <slot />
    <!-- table slot -->
    <slot name="table" />
  </div>
</template>
Index.vue
<template>
  <DataTable>
    <!-- When you write HTML code inside the component, it is automatically placed into the slot declared on the component side -->
    <!-- If not wrapped in a template, it is placed into the default slot -->
    <h5>Title</h5>
    <!-- Placed into the slot named table -->
    <template #table>
      <table>
        <tr>
          <th v-for="header in headers" :keys="header">{{ header }}</th>
        </tr>
        <tr v-for="(row, i) in data" :keys="`row-${i}`">
          <td
            v-for="(column, j) in row"
            :class="{
              'font-italic': italicFont(column),
              'font-weight-bold': boldFont(column)
            }"
            :key="`row-${i}-col-${j}`"
          >
            {{ column }}
          </td>
          <td>
            <button :disabled="editDisabled(column.c1)" @click="edit(column.c1)">Edit</button>
            <button :disabled="destroyDisabled(column.c1)" @click="click(column.c1)">Delete</button>
          </td>
        </tr>
      </table>
    </template>
  </DataTable>
</template>
<script>
export default {
  // Omit data information
  methods: {
    edit(id) {
      // Logic to edit the data for that row
    },
    destroy(id) {
      // Logic to delete the data for that row
    },
    italicFont(val) {
      // Logic to determine whether the font should be Italic
    },
    boldFont(val) {
      // Logic to determine whether the font should be bold
    },
    editDisabled(id) {
      // Logic to determine whether the edit button should be disabled
    },
    destroyDisabled(id) {
      // Logic to determine whether the delete button should be disabled
    }
  },
}
</script>

In this example, no props are passed to the component, making the code look quite clean. However, there's one issue: it allows developers to implement anything they want. For instance, when using the component mentioned above, it is expected that developers use the specified tags (such as <h5> for the title and <table> for the table, etc.) and apply the appropriate styles. Yet, due to a lack of communication or insufficient implementation knowledge, developers might use different tags. This can lead to differences in appearance, and you'll need to double-check during testing to make sure it fits on each screen size.

Index.vue
<template>
  <DataTable>
    <!-- Use h1 instead of h5 -->
    <h1>Title</h1>
    <template #table>
      <!-- Use <div> instead of <table>, <tr>, and <th> -->
      <div>
        <div>
          <div v-for="header in headers" :keys="header">{{ header }}</div>
          <div></div>
        </div>
        <div v-for="(row, i) in data" :keys="`row-${i}`">
          <div v-for="(column, j) in row" :key="`row-${i}-col-${j}`">
            {{ column }}
          </div>
          <div>
            <button :disabled="editDisabled(column.c1)" @click="edit(column.c1)">Edit</button>
            <button :disabled="destroyDisabled(column.c1)" @click="click(column.c1)">Delete</button>
          </div>
        </div>
      </div>
    </template>
  </DataTable>
</template>

Using <div> tags for everything in a table may seem like an extreme example, but it‘s safer not to give more freedom than necessary. While interpretations may vary by company or team, my ideal approach is to consult with designers and allow flexibility only where necessary. Before deciding whether to use props or slots, it’s important to determine which parts of the implementation can be flexible and which must follow the specific guidelines.

DataTable.vue
<template>
  <div>
    <!-- Use props to ensure the text is always placed in <h5> -->
    <h5>{{ title }}</h5>
    <!-- Ensure the use of <table> tag -->
    <table>
      <tr>
        <!—Use props to pass header information to enforce the use of <th> -->
        <th v-for="header in headers" :key="header">{{ header }}</th>
      </tr>
      <!-- Dynamically generate slots based on the number of rows in the passed data -->
      <!-- Use v-bind to pass data to the parent template -->
      <slot name="table-item" v-for="row in data" v-bind="row" />
    </table>
  </div>
</template>
<script>
export default {
  data() {
    return {
      title,
      headers,
      data
    }
  }
}
</script>
Index.vue
<template>
  <!—Pass the title and headers as props -->
  <DataTable title="Title" :headers="headers" :data="data">
    <!-- Receive the v-bind data on the component side -->
    <template #table-item="row">
      <!—Accept a row of data and define how it should be displayed -->
      <tr>
        <td v-for="(column, i) in row" :key="`col-${i}`">
          {{ column }}
        </td>
        <td>
          <button :disabled="editDisabled(column.c1)" @click="edit(column.c1)">Edit</button>
          <button :disabled="destroyDisabled(column.c1)" @click="click(column.c1)">Delete</button>
        </td>
      </tr>
    </template>
  </DataTable>
</template>

By the way, when using slots, you can utilize this.$scopedSlots on the component side to check which slots are being used by the parent and how they are being utilized. There are various use cases; for instance, you can determine what tags are being used within a slot. This provides mild form of validation against the issue of excessive flexibility with slots that was mentioned earlier.

Index.vue
<template>
  <DataTable title="Title" :headers="headers" :data="data">
    <template #table-item="row">
      <!-- Notify the developer in some way if it is not <tr> -->
      <div>
        <td v-for="(column, i) in row" :key="`col-${i}`">
          {{ column }}
        </td>
      </div>
    </template>
  </DataTable>
</template>

Summary

To sum up, although using props is the easiest way to develop reusable components with Vue, it lacks flexibility, as shown in the examples in this article. On the other hand, using slots gives developers more freedom in their implementation, but for various reasons, this freedom can lead to unexpected methods being used, making it difficult to ensure quality.
Therefore, it is important to involve stakeholders in the component’s development to decide in advance how much flexibility each part of the component should have. Based on these decisions, you can use props and slots appropriately to balance control and flexibility. Furthermore, providing thorough documentation will help ensure that users of the component understand the specifications and the intended usage.

Facebook

関連記事 | Related Posts

We are hiring!

フロントエンドエンジニア(レコメンドシステム)/DX開発G/東京

DX開発グループについてKINTO ONE売上拡大を目的とした、新しいクルマの買い方を提案するプロジェクトチームによるレコメンド&パーソナライズシステム開発、全国約3,600店舗の販売店に対するテクノロジーを用いたソリューション提案のためのシステム開発と、『 KINTOマガジン...

【フロントエンドエンジニア(コンテンツ開発)】新車サブスク開発G/東京

新車サブスク開発グループについてTOYOTAのクルマのサブスクリプションサービスである『 KINTO ONE 』のWebサイトの開発、運用をしています。​業務内容トヨタグループの金融、モビリティサービスの内製開発組織である同社にて、自社サービスである、クルマのサブスクリプションサービス『KINTO ONE』のWebサイトコンテンツの開発・運用業務を担っていただきます。