Перед вами четвёртая часть серии материалов, которые посвящены разработке веб-приложения Budget Manager с использованием Node.js, Vue.js и MongoDB. В первой, второй и третьей частях речь шла о создании основных серверных и клиентских компонентов приложения. Сегодня мы продолжим развитие проекта, а именно — займёмся списками документов и клиентов. Кроме того, нельзя не заметить, что к настоящему моменту сделано уже немало, поэтому вполне можно критически взглянуть на то, что получилось, и поработать над повторным использованием кода.
Начнём с изменения имени папки
Откроем файл компонента
Тут мы изменили имя класса таким образом, чтобы оно соответствовало имени компонента. Кроме того, мы поменяли имена слотов.
Откроем файл компонента
Здесь, опять же, мы поменяли имена классов, а так же отредактировали шаблон, настроив его на вывод данных из свойств (
Теперь пришёл черёд компонента
Этот компонент мы тоже подготовили к повторному использованию, задействовав вывод данных из свойств.
Теперь откроем файл компонента
Здесь мы поменяли команды импорта и теги, привели их в соответствие переименованным компонентам, добавили
Мы будем использовать
Теперь, вместо того, чтобы передавать данные документов напрямую, мы используем новый метод:
Этот метод, в качестве первого аргумента, принимает массив и произвольное число аргументов, которые сформируют массив
Метод будет брать каждый элемент из массива, в данном случае это — документы, и создавать новый объект
Этот объект будет содержать данные массива
И, наконец, мы перехватываем ошибки (если таковые возникнут), активируя
Теперь нужно отредактировать код маршрутизатора, для этого перейдём в папку
Тут мы поправили команды импорта, имена компонентов, теги и пути, а также сделали некоторые улучшения в
Вместо того, чтобы создавать новую страницу, предназначенную для вывода списка клиентов, мы будем использовать уже существующую страницу
Теперь добавим новый метод:
Вызовем этот метод при монтировании компонента:
Как теперь вывести сведения о клиентах? Очень просто. Достаточно внести ещё некоторые изменения в компонент
Теперь мы передаём переменную
Итак, теперь внесём улучшения в
Теперь цвет заголовка будет зависеть от состояния переменной
Кроме того, цвет и надпись на кнопке будут меняться в зависимости от значения
Кроме того, мы внесли некоторые изменения в scss.
И, наконец, займёмся компонентом
Изменения, внесённые сюда, похожи на те, что мы выполнили в коде компонента
Вот как теперь выглядит список документов:
А вот — список клиентов:
Теперь, когда мы можем видеть списки зарегистрированных документов и клиентов, создадим плавающую кнопку (Floating Action Button, FAB), которая будет содержать кнопки, позволяющие работать со списком. Всё ещё находясь в коде компонента
В FAB содержится три кнопки. Первая действует как активатор для FAB, вторая служит для добавления документов, третья — для добавления клиентов. Добавим теперь новое логическое значение для FAB в данные компонента
Здесь мы добавили логическое значение
Сегодня мы внесли некоторые улучшения в компоненты, переработали их с прицелом на повторное использование кода, добавили функционал вывода списка клиентов. Полный вариант приложения, как обычно, можно найти в репозитории проекта.
В следующем материале мы продолжим работу над приложением, и, вероятнее всего, её завершим.
Уважаемые читатели! Стремитесь ли вы к возможности повторного использования кода при работе над своими проектами?
Совершенствование компонентов
Начнём с изменения имени папки
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
, которое используется для указания того, активна плавающая кнопка или нет.Итоги
Сегодня мы внесли некоторые улучшения в компоненты, переработали их с прицелом на повторное использование кода, добавили функционал вывода списка клиентов. Полный вариант приложения, как обычно, можно найти в репозитории проекта.
В следующем материале мы продолжим работу над приложением, и, вероятнее всего, её завершим.
Уважаемые читатели! Стремитесь ли вы к возможности повторного использования кода при работе над своими проектами?