Categories
Vuetify

Vuetify — Tabs

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Tabs

We can add tabs to our Vuetify app with the v-tabs component.

For example, we can write:

<template>
  <v-tabs fixed-tabs background-color="indigo" dark>
    <v-tab>One</v-tab>
    <v-tab>Two</v-tab>
    <v-tab>Three</v-tab>
  </v-tabs>
</template>
<script>
export default {
  name: "HelloWorld",
};
</script>

to show tabs.

background-color has the background color.

fixed-tabs makes its position fixed.

dark makes the text of the non-active tabs gray.

The active tab is always centered.

Tab Items

We can add the v-tab-items component to let us customize the content per tab.

For example, we can write:

<template>
  <v-card>
    <v-tabs v-model="tab" background-color="primary" dark>
      <v-tab v-for="item in items" :key="item.tab">{{ item.tab }}</v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item v-for="item in items" :key="item.tab">
        <v-card flat>
          <v-card-text>{{ item.content }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      tab: null,
      items: [
        { tab: "One", content: "Tab 1 Content" },
        { tab: "Two", content: "Tab 2 Content" },
        { tab: "Three", content: "Tab 3 Content" },
        { tab: "Four", content: "Tab 4 Content" },
        { tab: "Five", content: "Tab 5 Content" },
      ],
    };
  },
};
</script>

to add tabs and populate the headings and content.

v-tab has the tab headings and v-tab-items has the tab items.

The tab state has the index of the active tab.

v-model lets us control which tab is active programmatically.

We just render all the tab content with the v-tab-item component and the right one will be displayed automatically.

Grow Tabs

We can add the grow prop to make the tag items take up all available space up to a max-width of 300px:

<template>
  <v-card>
    <v-tabs v-model="tab" background-color="primary" grow dark>
      <v-tab v-for="item in items" :key="item.tab">{{ item.tab }}</v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item v-for="item in items" :key="item.tab">
        <v-card flat>
          <v-card-text>{{ item.content }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      tab: null,
      items: [
        { tab: "One", content: "Tab 1 Content" },
        { tab: "Two", content: "Tab 2 Content" },
        { tab: "Three", content: "Tab 3 Content" },
        { tab: "Four", content: "Tab 4 Content" },
        { tab: "Five", content: "Tab 5 Content" },
      ],
    };
  },
};
</script>

We have the grow prop to make it grow to fix the screen.

Pagination

The show-arrows prop will make the arrow display so that we can scroll through the tabs:

<template>
  <v-card>
    <v-tabs v-model="tab" background-color="primary" show-arrows dark>
      <v-tab v-for="item in items" :key="item.tab">{{ item.tab }}</v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item v-for="item in items" :key="item.tab">
        <v-card flat>
          <v-card-text>{{ item.content }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      tab: null,
      items: [
        { tab: "One", content: "Tab 1 Content" },
        { tab: "Two", content: "Tab 2 Content" },
        { tab: "Three", content: "Tab 3 Content" },
        { tab: "Four", content: "Tab 4 Content" },
        { tab: "Five", content: "Tab 5 Content" },
      ],
    };
  },
};
</script>

The arrows will show when the tabs overflow the page.

Conclusion

We can add tabs with the v-tabs and tab content with the v-tab-item component.

Categories
Vuetify

Vuetify — Table Sorting and Pagination

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

External Sorting

We can control sorting externally with various props.

For instance, we can write:

<template>
  <div>
    <v-data-table
      :headers="headers"
      :items="desserts"
      :sort-by.sync="sortBy"
      :sort-desc.sync="sortDesc"
      class="elevation-1"
    ></v-data-table>
    <div class="text-center pt-2">
      <v-btn color="primary" class="mr-2" @click="toggleOrder">Toggle sort order</v-btn>
      <v-btn color="primary" @click="nextSort">Sort next column</v-btn>
    </div>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    sortBy: "fat",
    sortDesc: false,
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
  methods: {
    toggleOrder() {
      this.sortDesc = !this.sortDesc;
    },
    nextSort() {
      let index = this.headers.findIndex((h) => h.value === this.sortBy);
      index = (index + 1) % this.headers.length;
      this.sortBy = this.headers[index].value;
    },
  },
};
</script>

We change the sortBy and sortDesc prop values to let us change the column to sort by and whether we sort ascending or descending respectively.

The sync modifier is required for updating the table properly when the buttons are clicked to change the prop values.

Paginate and Sort Server-Side

We can do the pagination and sorting on the server-side.

For example, we can write:

<template>
  <div>
    <v-data-table
      :headers="headers"
      :items="desserts"
      :options.sync="options"
      :server-items-length="totalDesserts"
      :loading="loading"
      class="elevation-1"
    ></v-data-table>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      totalDesserts: 0,
      desserts: [],
      loading: true,
      options: {},
      headers: [
        {
          text: "Dessert (100g serving)",
          align: "start",
          sortable: false,
          value: "name",
        },
        { text: "Calories", value: "calories" },
        { text: "Fat (g)", value: "fat" },
      ],
    };
  },
  watch: {
    options: {
      async handler() {
        const { items, total } = await this.getDataFromApi();
        this.desserts = items;
        this.totalDesserts = total;
      },
      deep: true,
    },
  },
  async mounted() {
    const { items, total } = await this.getDataFromApi();
    this.desserts = items;
    this.totalDesserts = total;
  },
  methods: {
    getDataFromApi() {
      this.loading = true;
      return new Promise((resolve, reject) => {
        const { sortBy, sortDesc, page, itemsPerPage } = this.options;

        let items = this.getDesserts();
        const total = items.length;

        if (sortBy.length === 1 && sortDesc.length === 1) {
          items = items.sort((a, b) => {
            const sortA = a[sortBy[0]];
            const sortB = b[sortBy[0]];

            if (sortDesc[0]) {
              if (sortA < sortB) return 1;
              if (sortA > sortB) return -1;
              return 0;
            } else {
              if (sortA < sortB) return -1;
              if (sortA > sortB) return 1;
              return 0;
            }
          });
        }

        if (itemsPerPage > 0) {
          items = items.slice((page - 1) * itemsPerPage, page * itemsPerPage);
        }

        setTimeout(() => {
          this.loading = false;
          resolve({
            items,
            total,
          });
        }, 1000);
      });
    },
    getDesserts() {
      return [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
        },
      ];
    },
  },
};
</script>

The getDataFromApi method returns a promise that resolves to the data that we want to populate in the table.

The sorting is done with the sort method in the promise.

In the mounted hook, we get the data and set it.

this.desserts has the items.

totalDesserts is a number with the total number of desserts.

We set totalDessert as the value of server-items-length .

And items has the desserts array as its value.

Conclusion

We can sort and paginate data from the client or server-side with Vuetify.

Categories
Vuetify

Vuetify — Table Slots and Pagination

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Customizing Default Header

We can customize the default header with the header.<name> slot.

<name> is the name of the value property in the header item sent to headers .

For example, we can write:

<template>
  <v-data-table :headers="headers" :items="desserts" class="elevation-1">
    <template v-slot:header.name="{ header }">{{ header.text.toUpperCase() }}</template>
  </v-data-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
};
</script>

to add a table with our own headings.

We populate the header.name slot to customize the display of the name column heading.

We display it as upper case.

Customizing Columns

We can customize some columns with the item.<name> slot.

<name> is the property name of the column we want to change.

For example, we can write:

<template>
  <v-data-table :headers="headers" :items="desserts" class="elevation-1">
    <template v-slot:item.calories="{ item }">
      <v-chip :color="getColor(item.calories)" dark>{{ item.calories }}</v-chip>
    </template>
  </v-data-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
  methods: {
    getColor(calories) {
      if (calories > 400) return "red";
      else if (calories > 200) return "orange";
      else return "green";
    },
  },
};
</script>

We add a chip to the item.calories slot to change how the calories column is displayed.

External Pagination

We can add external pagination with the options prop.

For instance, we can write:

<template>
  <div>
    <v-data-table
      :headers="headers"
      :items="desserts"
      :page.sync="page"
      :items-per-page="itemsPerPage"
      hide-default-footer
      class="elevation-1"
      [@page](http://twitter.com/page "Twitter profile for @page")-count="pageCount = $event"
    ></v-data-table>
    <div class="text-center pt-2">
      <v-pagination v-model="page" :length="pageCount"></v-pagination>
      <v-text-field
        :value="itemsPerPage"
        label="Items per page"
        type="number"
        min="-1"
        max="15"
        [@input](http://twitter.com/input "Twitter profile for @input")="itemsPerPage = parseInt($event, 10)"
      ></v-text-field>
    </div>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    page: 1,
    pageCount: 0,
    itemsPerPage: 10,
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 159,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 262,
        fat: 16.0,
      },
    ],
  }),
  methods: {
    getColor(calories) {
      if (calories > 400) return "red";
      else if (calories > 200) return "orange";
      else return "green";
    },
  },
};
</script>

to add a text field to our page to let users change the items per page.

Also, we added the v-pagination component to add the pagination buttons.

Conclusion

We can change how columns and pagination buttons are displayed with various components and slots.

Categories
Vuetify

Vuetify — Tables

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Tables

We can add a table with the v-simple-table component.

For example, we can add it by writing:

<template>
  <v-simple-table height="300px">
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      }
    ],
  }),
};
</script>

We have the v-simple-table component with the default slot populated with the table rows.

We just use v-for to render the array entries into rows.

Fixed Header

The fixed-header with the height prop together let us fix the header to the top of the table.

For example, we can write:

<template>
  <v-simple-table height="150px" fixed-header>
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      },
    ],
  }),
};
</script>

to make the table scrollable and keep the header always on top.

Dense Table

To add a dense version of the table, we can use the dense prop.

For example, we can write:

<template>
  <v-simple-table dense>
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      },
    ],
  }),
};
</script>

The rows will be shorter than the default version.

Dark Theme

The dark prop lets us switch the table to the dark theme:

<template>
  <v-simple-table dark>
    <template v-slot:default>
      <thead>
        <tr>
          <th class="text-left">Name</th>
          <th class="text-left">Calories</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in desserts" :key="item.name">
          <td>{{ item.name }}</td>
          <td>{{ item.calories }}</td>
        </tr>
      </tbody>
    </template>
  </v-simple-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    desserts: [
      {
        name: "Yogurt",
        calories: 159,
      },
      {
        name: "Ice cream sandwich",
        calories: 237,
      },
      {
        name: "Eclair",
        calories: 262,
      },
    ],
  }),
};
</script>

Now the table will have a black background.

Conclusion

We can add a simple table with Vuetify’s v-simple-table component.

Categories
Vuetify

Vuetify — Table Footer and Slots

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Footer Props

We can change the v-data-table ‘s footer-props prop to change the footer.

For example, we can write:

<template>
  <v-data-table
    :headers="headers"
    :items="desserts"
    :items-per-page="5"
    item-key="name"
    class="elevation-1"
    :footer-props="{
      showFirstLastPage: true,
      firstIcon: 'mdi-arrow-collapse-left',
      lastIcon: 'mdi-arrow-collapse-right',
      prevIcon: 'mdi-minus',
      nextIcon: 'mdi-plus'
    }"
  ></v-data-table>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    search: "",
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        sortable: false,
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 200,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 200,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 300,
        fat: 16.0,
      },
    ],
  }),
};
</script>

We have an object with the firstIcon , lastIcon , prevIcon , and nextIcon properties to change the navigation buttons for the table.

showFirstLastPage lets us show the buttons to go to the first and last page.

Filterable Columns

We can set some columns to be filterable with the filterable property.

For example, we can write:

<template>
  <v-card>
    <v-card-title>
      <v-text-field v-model="search" append-icon="search" label="Search" single-line hide-details></v-text-field>
    </v-card-title>
    <v-data-table :headers="headers" :items="desserts" :search="search"></v-data-table>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    search: "",
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        filterable: false,
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    desserts: [
      {
        name: "Frozen Yogurt",
        calories: 200,
        fat: 6.0,
      },
      {
        name: "Ice cream sandwich",
        calories: 200,
        fat: 9.0,
      },
      {
        name: "Eclair",
        calories: 300,
        fat: 16.0,
      },
    ],
  }),
};
</script>

to disable filtering on the first column.

We added the filterable property into the object in the headers array.

Slots

We can populate various slots with our content.

For example, we can write:

<template>
  <div>
    <v-select v-model="enabled" :items="slots" label="Slot" clearable></v-select>
    <v-data-table
      :headers="headers"
      :items="items"
      :search="search"
      :hide-default-header="hideHeaders"
      :show-select="showSelect"
      :loading="isLoading"
      hide-default-footer
      item-key="name"
      class="elevation-1"
    >
      <template v-if="isEnabled('top')" v-slot:top>
        <div>This is content above the actual table</div>
      </template>

      <template
        v-show="isEnabled('header.data-table-select')"
        v-slot:header.data-table-select="{ on, props }"
      >
        <v-simple-checkbox color="purple" v-bind="props" v-on="on"></v-simple-checkbox>
      </template>

<template v-if="isEnabled('header')" v-slot:header="{ props: { headers } }">
        <thead>
          <tr>
            <th :colspan="headers.length">This is a header</th>
          </tr>
        </thead>
      </template>

<template v-show="isEnabled('progress')" v-slot:progress>
        <v-progress-linear color="purple" :height="10" indeterminate></v-progress-linear>
      </template>

      <template
        v-show="isEnabled('item.data-table-select')"
        v-slot:item.data-table-select="{ isSelected, select }"
      >
        <v-simple-checkbox color="green" :value="isSelected" @input="select($event)"></v-simple-checkbox>
      </template>

      <template
        v-show="isEnabled('item.<name>')"
        v-slot:item.name="{ item }"
      >{{ item.name.toUpperCase() }}</template>

<template v-show="isEnabled('body.prepend')" v-slot:body.prepend="{ headers }">
        <tr>
          <td :colspan="headers.length">This is a prepended row</td>
        </tr>
      </template>

      <template v-show="isEnabled('body')" v-slot:body="{ items }">
        <tbody>
          <tr v-for="item in items" :key="item.name">
            <td>{{ item.name }}</td>
          </tr>
        </tbody>
      </template>

      <template v-show="isEnabled('no-data')" v-slot:no-data>NO DATA HERE!</template>

      <template v-show="isEnabled('no-results')" v-slot:no-results>NO RESULTS HERE!</template>

      <template v-show="isEnabled('body.append')" v-slot:body.append="{ headers }">
        <tr>
          <td :colspan="headers.length">This is an appended row</td>
        </tr>
      </template>

      <template v-show="isEnabled('footer')" v-slot:footer>
        <div>This is a footer</div>
      </template>
    </v-data-table>
  </div>
</template>
<script>
const desserts = [
  {
    name: "Frozen Yogurt",
    calories: 200,
    fat: 6.0,
  },
  {
    name: "Ice cream sandwich",
    calories: 200,
    fat: 9.0,
  },
  {
    name: "Eclair",
    calories: 300,
    fat: 16.0,
  },
];

export default {
  name: "HelloWorld",
  data: () => ({
    enabled: null,
    search: null,
    slots: [
      "body",
      "body.append",
      "body.prepend",
      "footer",
      "header.data-table-select",
      "header",
      "progress",
      "item.data-table-select",
      "item.<name>",
      "no-data",
      "no-results",
      "top",
    ],
    headers: [
      {
        text: "Dessert (100g serving)",
        align: "start",
        sortable: false,
        value: "name",
      },
      { text: "Calories", value: "calories" },
      { text: "Fat (g)", value: "fat" },
    ],
    items: desserts,
  }),
  computed: {
    showSelect() {
      return (
        this.isEnabled("header.data-table-select") ||
        this.isEnabled("item.data-table-select")
      );
    },
    hideHeaders() {
      return !this.showSelect;
    },
    isLoading() {
      return this.isEnabled("progress");
    },
  },

  watch: {
    enabled(slot) {
      if (slot === "no-data") {
        this.items = [];
      } else if (slot === "no-results") {
        this.search = "...";
      } else {
        this.search = null;
        this.items = desserts;
      }
    },
  },

  methods: {
    isEnabled(slot) {
      return this.enabled === slot;
    },
  },
};
</script>

We add the template elements with the slots.

There are many slots for the body, showing data when there’s no data or no results, a footer, and more.

The dropdown sets which one is enabled and we use v-show to show the enabled ones.

Conclusion

We can populate various slots with our own content.

Footer icons can also be changed.