Перед вами четвёртая часть серии материалов, которые посвящены разработке веб-приложения Budget Manager с использованием Node.js, Vue.js и MongoDB. В первой, второй и третьей частях речь шла о создании основных серверных и клиентских компонентов приложения. Сегодня мы продолжим развитие проекта, а именно — займёмся списками документов и клиентов. Кроме того, нельзя не заметить, что к настоящему моменту сделано уже немало, поэтому вполне можно критически взглянуть на то, что получилось, и поработать над повторным использованием кода.
Совершенствование компонентов
Начнём с изменения имени папки Budget
на List
. Кроме того, переименуем три компонента, которые находятся в этой папке. А именно, BudgetList
теперь будет называться List
, BudgetListHeader
получит название ListHeader
, а BudgetListBody — ListBody
. В итоге папка и файлы компонентов должны выглядеть так, как показано на рисунке ниже.
Откроем файл компонента List
и приведём его к такому виду:
<template>
<section class="l-list-container">
<slot name="list-header"></slot>
<slot name="list-body"></slot>
</section>
</template>
<script>
export default {}
</script>
Тут мы изменили имя класса таким образом, чтобы оно соответствовало имени компонента. Кроме того, мы поменяли имена слотов.
Откроем файл компонента ListHeader
и внесём в него следующие изменения:
<template>
<header class="l-list-header">
<div class="md-list-header white--text"
v-if="headers != null"
v-for="header in headers">
{{ header }}
</div>
</header>
</template>
<script>
export default {
props: ['headers']
}
</script>
<style lang="scss">
@import "./../../assets/styles";
.l-list-header {
display: none;
width: 100%;
@media (min-width: 601px) {
margin: 25px 0 0;
display: flex;
}
.md-list-header {
width: 100%;
background-color: $background-color;
border: 1px solid $border-color-input;
padding: 0 15px;
display: flex;
height: 45px;
align-items: center;
justify-content: center;
font-size: 22px;
@media (min-width: 601px) {
justify-content: flex-start;
}
}
}
</style>
Здесь, опять же, мы поменяли имена классов, а так же отредактировали шаблон, настроив его на вывод данных из свойств (props
). Так мы сможем повторно использовать этот компонент на других страницах.
Теперь пришёл черёд компонента ListBody
:
<template>
<section class="l-list-body">
<div class="md-list-item"
v-if="data != null"
v-for="item in data">
<div class="md-info white--text" v-for="info in item" v-if="info != item._id">
{{ info }}
</div>
<div class="l-actions">
<v-btn small flat color="light-blue lighten-1">
<v-icon small>visibility</v-icon>
</v-btn>
<v-btn small flat color="yellow accent-1">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
</section>
</template>
<script>
export default {
props: ['data']
}
</script>
<style lang="scss">
@import "./../../assets/styles";
.l-list-body {
display: flex;
flex-direction: column;
.md-list-item {
width: 100%;
display: flex;
flex-direction: column;
margin: 15px 0;
@media (min-width: 960px) {
flex-direction: row;
margin: 0;
}
.md-info {
flex-basis: 25%;
width: 100%;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
padding: 0 15px;
display: flex;
height: 35px;
align-items: center;
justify-content: center;
&:first-of-type, &:nth-of-type(2) {
text-transform: capitalize;
}
&:nth-of-type(3) {
text-transform: uppercase;
}
@media (min-width: 601px) {
justify-content: flex-start;
}
}
.l-actions {
flex-basis: 25%;
display: flex;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
align-items: center;
justify-content: center;
.btn {
min-width: 45px !important;
margin: 0 5px !important;
}
}
}
}
</style>
Этот компонент мы тоже подготовили к повторному использованию, задействовав вывод данных из свойств.
Теперь откроем файл компонента Home
и отредактируем его:
<template>
<main class="l-home-page">
<app-header></app-header>
<div class="l-home">
<h4 class="white--text text-xs-center my-0">
Focus Budget Manager
</h4>
<list>
<list-header slot="list-header" :headers="budgetHeaders"></list-header>
<list-body slot="list-body" :data="budgets"></list-body>
</list>
</div>
<v-snackbar :timeout="timeout"
bottom="bottom"
color="red lighten-1"
v-model="snackbar">
{{ message }}
</v-snackbar>
</main>
</template>
<script>
import Axios from 'axios'
import Authentication from '@/components/pages/Authentication'
import ListHeader from './../List/ListHeader'
import ListBody from './../List/ListBody'
const BudgetManagerAPI = `http://${window.location.hostname}:3001`
export default {
components: {
'list-header': ListHeader,
'list-body': ListBody
},
data () {
return {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
snackbar: false,
timeout: 6000,
message: ''
}
},
mounted () {
this.getAllBudgets()
},
methods: {
getAllBudgets () {
Axios.get(`${BudgetManagerAPI}/api/v1/budget`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
}
}
}
</script>
<style lang="scss" scoped>
@import "./../../assets/styles";
.l-home {
background-color: $background-color;
margin: 25px auto;
padding: 15px;
min-width: 272px;
}
</style>
Здесь мы поменяли команды импорта и теги, привели их в соответствие переименованным компонентам, добавили snackbar
, что даёт возможность показывать сообщения об ошибках в том случае, если нам не удастся получить данные. Кроме того, мы добавили сюда новый массив для данных компонента, budgetHeaders
, а также данные, необходимые для snackbar
.
Мы будем использовать budgetHearders
для показа заголовков списка. Кроме того, мы внесли некоторые изменения в метод getAllBudgets
:
getAllBudgets () {
Axios.get(`${BudgetManagerAPI}/api/v1/budget`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
Теперь, вместо того, чтобы передавать данные документов напрямую, мы используем новый метод:
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
}
Этот метод, в качестве первого аргумента, принимает массив и произвольное число аргументов, которые сформируют массив options
с использованием оператора расширения.
Метод будет брать каждый элемент из массива, в данном случае это — документы, и создавать новый объект parsedItem
.
Этот объект будет содержать данные массива options
, после завершения его подготовки он будет помещён в массив parsedData
, который мы возвращаем из этого метода.
И, наконец, мы перехватываем ошибки (если таковые возникнут), активируя snackbar
.
Теперь нужно отредактировать код маршрутизатора, для этого перейдём в папку router
и откроем index.js
:
import Vue from 'vue'
import Router from 'vue-router'
import * as Auth from '@/components/pages/Authentication'
// Pages
import Home from '@/components/pages/Home'
import Authentication from '@/components/pages/Authentication/Authentication'
// Global components
import Header from '@/components/Header'
import List from '@/components/List/List'
// Register components
Vue.component('app-header', Header)
Vue.component('list', List)
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
components: {
default: Home,
header: Header,
list: List
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
router.beforeEach((to, from, next) => {
if (to.path !== '/login') {
if (Auth.default.user.authenticated) {
next()
} else {
router.push('/login')
}
} else {
next()
}
})
export default router
Тут мы поправили команды импорта, имена компонентов, теги и пути, а также сделали некоторые улучшения в router.beforeEach
, так как мы собираемся защищать любой маршрут, отличающийся от login
, мы убираем meta
из маршрута страницы Home
.
Вывод информации о клиентах
Вместо того, чтобы создавать новую страницу, предназначенную для вывода списка клиентов, мы будем использовать уже существующую страницу Home
. Поэтому вернёмся к компоненту Home
и создадим новый массив в данных компонента, дав ему имя clients
, а также создадим массив clientHeaders
и логическую переменную budgetsVisible
:
return {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: true,
snackbar: false,
timeout: 6000,
message: ''
}
Теперь добавим новый метод:
getAllClients () {
Axios.get(`${BudgetManagerAPI}/api/v1/client`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.clients = this.dataParser(data, '_id', 'client', 'email', 'phone')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
Вызовем этот метод при монтировании компонента:
mounted () {
this.getAllBudgets()
this.getAllClients()
},
Как теперь вывести сведения о клиентах? Очень просто. Достаточно внести ещё некоторые изменения в компонент Home
:
<template>
<main class="l-home-page">
<app-header :budgetsVisible="budgetsVisible" @toggleVisibleData="budgetsVisible = !budgetsVisible"></app-header>
<div class="l-home">
<h4 class="white--text text-xs-center my-0">
Focus Budget Manager
</h4>
<list>
<list-header slot="list-header" :headers="budgetsVisible ? budgetHeaders : clientHeaders"></list-header>
<list-body slot="list-body"
:budgetsVisible="budgetsVisible"
:data="budgetsVisible ? budgets : clients">
</list-body>
</list>
</div>
<v-snackbar :timeout="timeout"
bottom="bottom"
color="red lighten-1"
v-model="snackbar">
{{ message }}
</v-snackbar>
</main>
</template>
<script>
import Axios from 'axios'
import Authentication from '@/components/pages/Authentication'
import ListHeader from './../List/ListHeader'
import ListBody from './../List/ListBody'
const BudgetManagerAPI = `http://${window.location.hostname}:3001`
export default {
components: {
'list-header': ListHeader,
'list-body': ListBody
},
data () {
return {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: false,
snackbar: false,
timeout: 6000,
message: ''
}
},
mounted () {
this.getAllBudgets()
this.getAllClients()
},
methods: {
getAllBudgets () {
Axios.get(`${BudgetManagerAPI}/api/v1/budget`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
getAllClients () {
Axios.get(`${BudgetManagerAPI}/api/v1/client`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.clients = this.dataParser(data, 'name', 'client', 'email', 'phone')
}).catch(error => {
this.snackbar = true
this.message = error.message
})
},
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
}
}
}
</script>
<style lang="scss" scoped>
@import "./../../assets/styles";
.l-home {
background-color: $background-color;
margin: 25px auto;
padding: 15px;
min-width: 272px;
}
</style>
Теперь мы передаём переменную budgetVisible
в Header
и, кроме того, используем эту переменную в тернарном операторе сравнения для вывода нужных данных. В Header
так же попадает переменная toggleVisibleData
, где мы инвертируем значение budgetsVisible
. Причина, по которой мы передаём в Header
свойства, заключается в том, что благодаря такому подходу мы можем сделать ещё некоторые улучшения, о которых поговорим ниже. Кроме того, в слотах list-header
и list-body
мы используем тернарные операторы сравнения.
Итак, теперь внесём улучшения в Header
:
<template>
<header class="l-header-container">
<v-layout row wrap :class="budgetsVisible ? 'l-budgets-header' : 'l-clients-header'">
<v-flex xs12 md5>
<v-text-field v-model="search"
label="Search"
append-icon="search"
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'">
</v-text-field>
</v-flex>
<v-flex xs12 offset-md1 md1>
<v-btn block
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"
@click.native="$emit('toggleVisibleData')">
{{ budgetsVisible ? "Clients" : "Budgets" }}
</v-btn>
</v-flex>
<v-flex xs12 offset-md1 md2>
<v-select label="Status"
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"
v-model="status"
:items="statusItems"
single-line>
</v-select>
</v-flex>
<v-flex xs12 offset-md1 md1>
<v-btn block color="red lighten-1 white--text" @click.native="submitSignout()">Sign out</v-btn>
</v-flex>
</v-layout>
</header>
</template>
<script>
import Authentication from '@/components/pages/Authentication'
export default {
props: ['budgetsVisible'],
data () {
return {
search: '',
status: '',
statusItems: [
'All', 'Approved', 'Denied', 'Waiting', 'Writing', 'Editing'
]
}
},
methods: {
submitSignout () {
Authentication.signout(this, '/login')
}
}
}
</script>
<style lang="scss">
@import "./../assets/styles";
.l-header-container {
background-color: $background-color;
margin: 0 auto;
padding: 0 15px;
min-width: 272px;
.l-budgets-header {
label, input, .icon, .input-group__selections__comma {
color: #29b6f6!important;
}
}
.l-clients-header {
label, input, .icon, .input-group__selections__comma {
color: #66bb6a!important;
}
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.btn {
margin-top: 15px;
}
}
</style>
Теперь цвет заголовка будет зависеть от состояния переменной budgetsVisible
. Если документы видимы, заголовок будет иметь светло-синий цвет, если нет — зелёный.
Кроме того, цвет и надпись на кнопке будут меняться в зависимости от значения budgetVisible
, по её щелчку вызывается обработчик соответствующего события, меняющий состояние логической переменной.
Кроме того, мы внесли некоторые изменения в scss.
И, наконец, займёмся компонентом ListBody
:
<template>
<section class="l-list-body">
<div class="md-list-item"
v-if="data != null"
v-for="item in data">
<div :class="budgetsVisible ? 'md-budget-info white--text' : 'md-client-info white--text'"
v-for="info in item"
v-if="info != item._id">
{{ info }}
</div>
<div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">
<v-btn small flat color="light-blue lighten-1">
<v-icon small>visibility</v-icon>
</v-btn>
<v-btn small flat color="yellow accent-1">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
</section>
</template>
<script>
export default {
props: ['data', 'budgetsVisible']
}
</script>
<style lang="scss">
@import "./../../assets/styles";
.l-list-body {
display: flex;
flex-direction: column;
.md-list-item {
width: 100%;
display: flex;
flex-direction: column;
margin: 15px 0;
@media (min-width: 960px) {
flex-direction: row;
margin: 0;
}
.md-budget-info {
flex-basis: 25%;
width: 100%;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
padding: 0 15px;
display: flex;
height: 35px;
align-items: center;
justify-content: center;
&:first-of-type, &:nth-of-type(2) {
text-transform: capitalize;
}
&:nth-of-type(3) {
text-transform: uppercase;
}
@media (min-width: 601px) {
justify-content: flex-start;
}
}
.md-client-info {
@extend .md-budget-info;
background-color: rgba(102, 187, 106, 0.45)!important;
&:nth-of-type(2) {
text-transform: none;
}
}
.l-budget-actions {
flex-basis: 25%;
display: flex;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
align-items: center;
justify-content: center;
.btn {
min-width: 45px !important;
margin: 0 5px !important;
}
}
.l-client-actions {
@extend .l-budget-actions;
background-color: rgba(102, 187, 106, 0.45)!important;
}
}
}
</style>
Изменения, внесённые сюда, похожи на те, что мы выполнили в коде компонента Header
.
Промежуточные результаты
Вот как теперь выглядит список документов:
А вот — список клиентов:
Теперь, когда мы можем видеть списки зарегистрированных документов и клиентов, создадим плавающую кнопку (Floating Action Button, FAB), которая будет содержать кнопки, позволяющие работать со списком. Всё ещё находясь в коде компонента Home
, добавим следующий код ниже v-snackbar
:
<v-fab-transition>
<v-speed-dial v-model="fab"
bottom
right
fixed
direction="top"
transition="scale-transition">
<v-btn slot="activator"
color="red lighten-1"
dark
fab
v-model="fab">
<v-icon>add</v-icon>
<v-icon>close</v-icon>
</v-btn>
<v-tooltip left>
<v-btn color="light-blue lighten-1"
dark
small
fab
slot="activator">
<v-icon>assignment</v-icon>
</v-btn>
<span>Add new Budget</span>
</v-tooltip>
<v-tooltip left>
<v-btn color="green lighten-1"
dark
small
fab
slot="activator">
<v-icon>account_circle</v-icon>
</v-btn>
<span>Add new Client</span>
</v-tooltip>
</v-speed-dial>
</v-fab-transition>
В FAB содержится три кнопки. Первая действует как активатор для FAB, вторая служит для добавления документов, третья — для добавления клиентов. Добавим теперь новое логическое значение для FAB в данные компонента Home
:
data () {
return {
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: true,
snackbar: false,
timeout: 6000,
message: '',
fab: false
}
},
Здесь мы добавили логическое значение fab
, которое используется для указания того, активна плавающая кнопка или нет.
Итоги
Сегодня мы внесли некоторые улучшения в компоненты, переработали их с прицелом на повторное использование кода, добавили функционал вывода списка клиентов. Полный вариант приложения, как обычно, можно найти в репозитории проекта.
В следующем материале мы продолжим работу над приложением, и, вероятнее всего, её завершим.
Уважаемые читатели! Стремитесь ли вы к возможности повторного использования кода при работе над своими проектами?
Автор: ru_vds