Layouts in Vuetify vs. Quasar

How do you create typical layouts in the the most popular Material Design styling libraries for Vue?

Creating layouts for your application

Layouts help us standardize UI across the application.

For e.g., you have layouts to take care of -

  1. Toolbar for the app and for views/components
  2. Navigation bars
  3. Standard controls (buttons/titles etc.) for list or detail views .. and so on.

Vuetify and Quasar allow you to do your own thing, but I find the below way of creating layouts easiest.

Vuetify

Vuetify is an amazing library and we choose Vuetify for client projects most of the times if material design standards are agreeable.

I typically do not end up using layouts in Vue router when using Vuetify. - I should, but found it harder to reuse. Instead I use layouts like a beginner would.

Create layouts

Create a new folder - components/layouts in your Vue project.

Create a baseline panel that comprises of -

  • a toolbar
  • placeholder to show ‘alerts’
  • slots for toolbar items and main content
<!-- Panel.vue -->

<template>
  <div>
    <v-toolbar
      flat
      dense
      v-if="title"
      fill-height
      class="align-center ma-0 pa-0"
    >
      <v-toolbar-title class="title primary--text">
        <v-icon v-if="icon" color="primary" class="mr-2">{{ icon }}</v-icon>
        <span color="primary" class="font-weight-bold">{{ title }}</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>

      <slot name="toolbar-items"></slot>

      <template v-slot:extension v-if="extn">
        <slot name="toolbar-extn"></slot>
      </template>

      <v-progress-linear
        :active="loading"
        :loading="loading"
        :indeterminate="true"
        bottom
        absolute
        color="#2196F3"
      />
    </v-toolbar>

    <Alert />
    <!-- alert component -->
    <v-row>
      <v-col cols="12">
        <slot name="content">No content</slot>
      </v-col>
    </v-row>
  </div>
</template>

<script>
  import { mapState } from "vuex";
  export default {
    name: "panel",

    props: {
      title: String,
      icon: String,
      extn: Boolean,
    },
    data() {
      return {};
    },
    computed: {
      ...mapState(["loading"]),
    },
    components: {
      Alert: () => import("../util/Alert"),
    },
  };
</script>

Panel.vue will be used by your Vue views.

Optionally, create additional “panels” for showing specialised components like a ‘list’.

For e.g. have a PanelListMain.vue that includes -

  • a mini toolbar with title and icon
  • slots
  • action buttons
<!-- PanelList.vue -->

<template>
  <v-card class="ml-3 mr-3">
    <v-toolbar dense flat class="elevation-1">
      <slot name="toolbar-items"></slot>
    </v-toolbar>

    <v-container grid-list-xs fluid>
      <slot name="content">No content</slot>
    </v-container>
  </v-card>
</template>

<script>
  export default {
    name: "PanelListMain",

    props: {
      title: String,
    },
  };
</script>

The above layout is often used by components.

Using Layouts

This is the easy part.

In your main view -

<!-- ./views/ServiceReq.vue -->
<template>
  <Panel icon="mdi-book-plus" title="Service Requests">
    <template slot="toolbar-items"></template>
    <template slot="content">
      <ServiceReqList />
    </template>
  </Panel>
</template>

<script>
  import Panel from "../components/layouts/Panel";
  import ServiceReqList from "../components/ServiceReqList";

  export default {
    components: {
      Panel,
      ServiceReqList,
    },
  };
</script>

I could use the list layout in the ServiceReqList component -

<!-- ./components/ServiceReqList.vue -->

<template>
  <PanelListMain>
    <template slot="toolbar-items">
      <span class="subtitle-2">Service List</span>
      <v-spacer></v-spacer>
      <v-btn @click="newRecord" small outlined>
        New
      </v-btn>
    </template>
    <template slot="content">
      <v-row dense>
        <v-col cols="12">
          <v-card flat color="transparent" height="100%" style="overflow:auto">
            <v-card-title>
              <v-spacer></v-spacer>

              <v-text-field
                v-model="srchSrNum"
                prepend-icon="mdi-magnify"
                label="Search SR Number"
                single-line
              ></v-text-field>
            </v-card-title>
            <v-data-table
              :headers="headers"
              :items="serviceReqs.data"
              :server-items-length="Number(serviceReqs.total)"
              hide-default-footer
            >
              <template v-slot:item="props">
                <tr @click="activeServiceReq = props.item">
                  <td>{{ props.item.sr_number }}</td>
                  <td>{{ props.item.type_cd }}</td>
                  <td>
                    <v-icon color="success" @click="editRecord(props.item)"
                      >mdi-pencil</v-icon
                    >
                  </td>
                </tr>
              </template>
            </v-data-table>
          </v-card>
        </v-col>
        <v-col cols="12" class="text-md-right pt-2">
          <v-pagination
            v-model="serviceReqs.page"
            :total-visible="7"
            :length="serviceReqs.lastPage"
            @input="changePage"
            justify="end"
          ></v-pagination>
        </v-col>
      </v-row>
      <ServiceReqEdit v-model="detailDialog" />
    </template>
  </PanelListMain>
</template>

<!-- removed additional code to keep this simple-->

The resulting sample UI.

vuetify-layout-sample

Examples

A more complete example for the layout creation as described here can be found on Vuetify booster starter template.

Quasar

Quasar framework is often compared against Vuetify since both of them implement Material Design guidelines. Quasar provides a more controlled ecosystem that can make you productive on web/desktop/mobile UI development and also provide “more useful” components. [not starting a war here - this is my opinion and I am entitled to have a stupid opinion].

Quasar provides an out-of-the-box way of using layouts in router.

One of my main mottos in life happens to be “Do not fight frameworks” - so I just follow “the way”.

Create layouts

Creating layouts is similar to what we did in Vuetify, but we don’t use slots in the same way, and we have a different folder structure.

We create a MainLayout.vue which serves the layout -

<!-- ./src/layouts/MainLayout.vue -->

<template>
  <q-layout view="lHh Lpr lFf">
    <q-header elevated>
      <q-toolbar>
        <q-btn
          flat
          dense
          round
          icon="menu"
          aria-label="Menu"
          @click="leftDrawerOpen = !leftDrawerOpen"
        />

        <q-toolbar-title>Mostart</q-toolbar-title>
        <span class="self-right">
          <q-btn flat fab to="/settings">
            <q-icon name="settings" />
          </q-btn>
          <q-btn flat fab to="/help">
            <q-icon name="help" />
          </q-btn>
        </span>
      </q-toolbar>
    </q-header>

    <q-drawer
      v-model="leftDrawerOpen"
      show-if-above
      bordered
      content-class="bg-grey-1"
    >
      <q-list>
        <q-item-label header class="text-grey-8">Options</q-item-label>
        <!-- This is just a link -->
        <EssentialLink
          v-for="link in essentialLinks"
          :key="link.title"
          v-bind="link"
        />
      </q-list>
      <q-list>
        <!-- <EssentialLink v-bind="setupLink" /> -->
        <q-separator />
        <EssentialLink v-bind="helpLink" />
        <q-separator />
        <EssentialLink v-bind="exitLink" />
      </q-list>
    </q-drawer>

    <q-page-container>
      <router-view />
    </q-page-container>
  </q-layout>
</template>

<!-- Removed to keep this condensed -->

Using layouts

The router file will specify the layout -

// .src/router/routes.js
const routes = [
  {
    path: "/",
    component: () => import("layouts/MainLayout.vue"),
    children: [
      { path: "", component: () => import("pages/Index.vue") },
      { path: "/history", component: () => import("../pages/History.vue") },
      { path: "/settings", component: () => import("../pages/Settings.vue") },
    ],
  },
];

if (process.env.MODE !== "ssr") {
  console.log("process.env.MODE", process.env.MODE);
  routes.push({
    path: "*",
    component: () => import("pages/Error404.vue"),
  });
}

export default routes;

Use these routes in your app -

// ./src/router/index.js

import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";

Vue.use(VueRouter);

export default function () {
  const Router = new VueRouter({
    scrollBehavior: () => ({ x: 0, y: 0 }),
    routes,

    mode: process.env.VUE_ROUTER_MODE,
    base: process.env.VUE_ROUTER_BASE,
  });

  return Router;
}

All you need to do now is to create the actual page.

<template>
  <!-- ./src/pages/History.vue -->
  <q-page class="flex">
    <div class="q-pa-lg q-gutter-md">
      <div class="row full-height">
        <div class="col-12">
          <div class="title text-weight-bold text-subtitle text-grey mt-3 mb-5">
            View message history.
          </div>
          <div class="title text-weight-bold text-h5 mt-3 mb-5">History</div>
        </div>

        <div class="col-12 q-gutter-md">
          <q-card class="col col-12 col-sm-8">
            <q-card-section class="q-px-md" v-if="motor">
              <div class="row justify-around">
                <q-input
                  class="col col-12"
                  :value="motor['name']"
                  label="Name"
                  readonly
                />
                <q-input
                  class="col col-12 col-md-6"
                  :value="motor['phone']"
                  label="Motor Phone"
                  readonly
                />

                <q-input
                  class="col col-12 col-md-6"
                  :value="motor['location']"
                  label="Location"
                  readonly
                />
              </div>
            </q-card-section>
          </q-card>
        </div>

        <div class="col-12 overline text-weight-bold block text-grey-5 mt-3">
          <p class="infoText">
            You are viewing message history for your chosen device.
          </p>
        </div>
      </div>
    </div>
  </q-page>
</template>

As you can see we did not specify the layout within the page. Things are seamlessly used in router.

Examples

Find the complete example on Mostart: an app for SMS automation.

End Word

There is no “better option” amongst these IMO.

I personally like the Vuetify option better -

  1. Keep things in control - see related components in one place
  2. Multiple, dynamic layouts based on data is easy to implement and comprehend

Nothing in Quasar prevents us from having the same layout style as in Vuetify, but the framework does provide a clean abstraction for standard applications and UI structure.

comments powered by Disqus