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 -
- Toolbar for the app and for views/components
- Navigation bars
- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
<!-- 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<!-- 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 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<!-- ./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 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
<!-- ./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.
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 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
<!-- ./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 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// .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 -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// ./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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
<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 -
- Keep things in control - see related components in one place
- 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.