Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет хабровцы и любители фронтенда!
Это моя первая статья, в которой я хочу поделиться своими первыми шагами в мир frontend разработки на VueJS. И в качестве примера для изучения я решил реализовать вариант грида со стандартным набором функционала: сортировкой, фильтрацией и пагинацией. Несмотря на то, что в интернете очень много подобных решений и у каждого есть все вышеперечисленные функции (и даже больше), думаю что реализация этого компонента позволит читателю, особенно новичку, познакомится со многими аспектами разработки на VueJS.
Данная статья описывает frontend часть, а в следующей статье будет описание backend приложения на NestJS, к которому будет обращаться наш грид за данными по REST API и делать запрос в БД с помощью TypeORM.
Предполагается, что у читателя уже имеются некоторые базовые знания по HTML, CSS и JS. Все ключевые моменты и код буду стараться подробно комментировать. А пока оставлю ссылку на гитхаб с готовым проектом. Песочница с фронтенд приложением находится здесь, но прежде чем запустить его нужно убедиться что запущен контейнер с backend приложением.
Если все пройдет успешно, то должен открыться вот такой вид
Для демонстрации выбрана СУБД SQLite, файл которой находится в backend приложении (test_db.sqlite). На момент написания статьи в таблице находилось 100 записей. Но почему-то иногда это количество сокращается. Видимо песочница каким-то образом влияет на это. Но это не точно.
Писать будем под Linux (Ubuntu 20.04.4 LTS, Focal Fossa), поэтому навыки обращения с этой ОС не помешают.
Для реализации нашей идеи нам понадобятся (в скобочках я напишу версии, которые стоят у меня, вы же можете скачивать самые свежие):
NodeJS (v10.19.0), инструкции по установке на Linux можно посмотреть здесь
npm (v6.14.4) — по хорошему должен идти в комплекте с NodeJS
Vue CLI (v4.5.17) — система для быстрой разработки на VueJS
огромное желание, выдержка и терпение
Ну и скачайте VS Code – в блокноте писать такое себе.
P.S> компонент корректно отображается в Google Chrome и Яндекс браузере, за остальные браузеры ничего сказать не могу.
Создание проекта
Создавать проект будем с помощью визуального интерфейса Vue CLI.
Откроем терминал и запустим команду vue ui:
Запуск vue ui в терминале
По адресу http://localhost:8000 должен быть доступен дашбоард Vue CLI:
Рабочий стол проекта
В левом нижнем углу перейдем в менеджер проектов:
Открываем менеджер проектов
На вкладке создания проекта выбираем директорию:
Выбор директории
и нажимаем "Создать новый проект здесь":
Кнопка создания проекта
Вводим название нашего проекта, выбираем npm в качестве менеджера пакетов, остальные параметры оставляем как на скриншоте:
Выбор названия и менеджера пакетов
На вкладке создания пресетов выбираем пункт "Вручную" и идем дальше:
Выбор пресетов
Чекаем следующие библиотеки и нажимаем "Далее":
Выбор библиотек и функций проекта
На следующем этапе выбираем 3-ю версию Vue (хотя вполне хватило бы и второй, но будем идти в ногу со временем) и ставим галочку напротив history mode:
Выбор версии Vue
Нажимаем "Создать проект" и отвечаем на запрос по созданию пресета (если пресет не нужен - кликаем "Продолжить без сохранения").
После этого Vue CLI начнет создавать проект. Если все пройдет успешно – можно закрывать визуальный интерфейс и останавливать сервер, т.к. он нам больше не понадобится – все остальные действия мы будем выполнять в VS Code.
Однофайловые компоненты
Перед тем как перейти к коду, поговорим немного про т.н. Single-File Components (SFC, однофайловые компоненты). Любое приложение на VueJS, каким бы сложным оно ни было, в своей основе состоит из однофайловых компонентов (файлы с расширением .vue). Данные компоненты — это краеугольный камень всего фреймворка VueJS. Каждый такой компонент стоит на трех китах содержит в себе три основные сущности мира Web — это разметка (html), стили (CSS) и поведение (JS), хотя и не обязательно должен содержать сразу все три.
Проведу аналогию. Если у вас был опыт создания десктопных приложений (например, WinForms на C#) вы наверняка использовали кнопочки (Button), текстовые поля ввода (TextBox) и другие компоненты для создания своих крутых приложений. У каждого такого компонента есть свои свойства и события. Свойства позволяют задавать визуальное оформление и определенное содержание, которое необходимо для дальнейшей работы этого компонента. Например, у кнопки есть такие свойства как Width, Height, Text и Name, которые задают визуальный стиль и идентифицируют кнопку среди других компонентов. Помимо этого, у кнопки есть события (например, Click), которые генерируются (файрятся, эмитятся) при определенных действиях пользователя.
Так вот, когда мы создаем однофайловый компонент во Vue, то кроме вышеупомянутых шаблона, стиля и поведения мы также можем создавать свойства этого компонента и определять события, которые будут генерироваться на определенные действия пользователя и/или на события других компонентов. При этом события зачастую содержат в себе аргументы, которые передаются вместе с этим событием.
Предварительные настройки
Откроем нашу папку с проектом в VS Code. Сразу после создания проекта его структура должна выглядеть примерно следующим образом:
Структура проекта после его создания
В папке node_modules хранятся все зависимости, поэтому ее я не раскрывал. Слегка подредактируем файл package.json, чтобы сразу после запуска у нас открывалось окно браузера.
Редактирование команды для запуска приложения
Открываем новый терминал и запускаем наше приложение с помощью команды npm run serve:
Запуск приложения
Если все прошло успешно — по адресу http://localhost:8080/ должно быть доступно запущенное приложение:
Окно браузера с запущенным приложением
Благодаря такой технологии как Hot Reload, которая доступна из коробки во Vue CLI, нам не обязательно останавливать запущенный сервер, — все изменения будут тут же отображаться в окне браузера, что очень сильно упрощает и ускоряет процесс разработки.
Создание компонента InputComponent
Сразу оставлю ссылку на Vue SFC Playground.
Создадим в папке public папку img, а в ней папку icons. В папке icons создадим файл clear-icon.svg и наполним его следующим содержимым:
public/img/icons/clear-icon.svg
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1000pt" height="1280.000000pt" viewBox="0 0 1280.000000 1280.000000"
>
<g transform="translate(0.000000,1280.000000) scale(0.100000,-0.100000)"
fill="#979799" stroke="none">
<path d="M1545 12784 c-85 -19 -167 -51 -243 -95 -69 -41 -1089 -1049 -1157
-1144 -101 -141 -140 -263 -140 -440 0 -169 36 -293 125 -427 29 -43 705 -726
2149 -2170 l2106 -2108 -2111 -2112 c-1356 -1358 -2124 -2133 -2147 -2169 -88
-137 -121 -249 -121 -419 -1 -181 37 -302 139 -445 68 -95 1088 -1103 1157
-1144 273 -159 604 -143 853 42 22 17 986 976 2143 2131 l2102 2101 2103
-2101 c1156 -1155 2120 -2114 2142 -2131 69 -51 130 -82 224 -113 208 -70 431
-44 629 71 69 41 1089 1049 1157 1144 101 141 140 263 140 440 0 166 -36 290
-121 422 -25 39 -746 767 -2148 2171 l-2111 2112 2107 2108 c2207 2208 2162
2161 2219 2303 75 187 77 392 4 572 -53 132 -74 157 -615 700 -289 291 -552
548 -585 572 -141 101 -263 140 -440 140 -166 0 -289 -35 -420 -120 -41 -26
-724 -702 -2172 -2149 l-2113 -2111 -2112 2111 c-1454 1452 -2132 2123 -2173
2150 -64 41 -149 78 -230 101 -79 22 -258 26 -340 7z"/>
</g>
</svg>
Это будет иконка с крестиком в формате svg для нашего инпута.
Далее создаем в папке src/components файл InputComponent.vue и наполним его такими данными:
InputComponent.vue
<template>
<div id="container">
<table>
<tbody>
<tr>
<td>
<input
v-model="inputValue"
ref="input"
:type="inputType"
@keyup.enter="changeInputValue"
/>
</td>
<td id="clear-icon-container">
<img
v-if="inputValue"
id="clear-icon"
src="../../public/img/icons\clear-icon.svg"
alt="clear"
@click="clearInputValue"
/>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: "InputComponent",
emits: ["inputValueChanged", "clearButtonClicked"],
props: {
inputType: String,
},
data() {
return {
inputValue: "",
};
},
methods: {
clearInputValue() {
this.inputValue = "";
this.$refs.input.focus();
this.$emit("clearButtonClicked");
},
changeInputValue() {
this.$emit("inputValueChanged", this.inputValue);
},
},
};
</script>
<style scoped>
table {
/* border: 1px solid black; */
width: -webkit-fill-available;
}
#clear-icon {
width: 0.7em;
height: 0.7em;
cursor: pointer;
}
input {
border: none;
outline: none;
width: -webkit-fill-available;
}
#clear-icon-container {
width: 1em;
height: 1em;
text-align: center;
/* border: 1px solid; */
}
</style>
В качестве шаблона для компонента использован обычный <table> с одной строкой и двумя столбцами (ячейками) помещенный в тэг <div> (вообще не обязательно оборачивать все содержимое шаблона в div, но лучше придерживаться данного стиля, чтобы избежать подобной ошибки).
В первой ячейке содержится стандартный html инпут:
<td>
<input
v-model="inputValue"
ref="input"
:type="inputType"
@keyup.enter="changeInputValue"
/>
</td>
а во второй — svg иконка, о которой говорилось выше:
<td id="clear-icon-container">
<img
v-if="inputValue"
id="clear-icon"
src="../../public/img/icons\clear-icon.svg"
alt="clear"
@click="clearInputValue"
/>
</td>
Начнем с иконки. Она будет видима только в том случае, если значение inputValue не является пустым, это делается с помощью директивы v-if (условная отрисовка).
А с помощью этой строки
@click="clearInputValue"
мы обрабатываем событие щелчка по иконке с крестиком. Логика обработчика находится в методе clearInputValue():
clearInputValue() {
this.inputValue = ""; // После щелчка мы затираем значение инпута
this.$refs.input.focus(); // устанавливаем фокус на инпут
this.$emit("clearButtonClicked"); // и генерируем событие clearButtonClicked
}
Чтобы в методе сослаться на элемент DOM внутри шаблона компонента — используется атрибут ref (кстати, в официальной документации как раз и описан кейс с установкой фокуса ввода).
Еще один метод этого компонента — changeInputValue():
changeInputValue() {
this.$emit("inputValueChanged", this.inputValue);
}
это обработчик события клавиатуры keyup.enter. После нажатия клавиши Enter файрится событие inputValueChanged в аргументы которого передается значение инпута.
Теперь немного о первой ячейке с самим инпутом. В ней используется такая классная директива как v-model которая позволяет осуществлять двустороннюю привязку данных и еще много чего интересного. Этой директиве можно посвятить целую статью, но это не является целью данной работы. В нашем компоненте эта директива осуществляет двустороннюю привязку, используя inputValue, которое находится в опции data():
data() {
return {
inputValue: "",
};
}
Еще немного о v-model
Вообще, данная директива работает не только с элементом <input>, но и с другими элементами ввода данных (textarea, checkbox, radio, select) и призвана заменить вот такую часто используемую операцию:
вот такой единственной строчкой:
Список свойств и событий, с которыми работает данная директива для каждого элемента ввода можно посмотреть в официальной документации. Но иногда требуется прослушивать и другие события пользователя, как, например, нажатие клавиши Enter, — при этом директиву v-model можно оставить.
У данного компонента одно единственное свойство — inputType типа String, которое будет задаваться извне (в нашем случае его будет устанавливать FilterComponent, о котором пойдет речь дальше):
props: {
inputType: String,
}
это свойство привязывается к свойству type инпута в следующей строчке шаблона:
:type="inputType"
и рассчитано на то, чтобы принимать одно из двух значений — "Text" или "Number". И тогда наш инпут будет, соответственно, либо текстовым полем ввода, либо числовым. Вся остальная логика при этом никак не меняется.
Вот, в принципе, и все, что касается данного компонента. Если подытожить — у него есть единственное свойство inputType, которое определяет его тип и два события, которые генерируются при изменении значения inputValue и щелчке на иконку с крестиком:
emits: ["inputValueChanged", "clearButtonClicked"]
Кратко о стиле этого компонента. Все стили заключены в тэг <style> с атрибутом scoped. Этот атрибут говорит о том, что данные стили будут применяться непосредственно к элементам этого компонента и не будут влиять на стили дочерних компонентов.
Таблица по ширине будет занимать все возможное пространство родительского элемента:
table {
/* border: 1px solid black; */
width: -webkit-fill-available;
}
У инпута удаляем границу и аутлайн, т.к. они нам не понадобятся:
input {
border: none;
outline: none;
width: -webkit-fill-available;
}
Компоненты ввода с чекбоксом и датой
Ссылки на песочницы: чекбокс, дата
Помимо стандартного поля ввода для текстовых и числовых значений создадим отдельно два компонента в папке src/components с соответствующим содержимым:
CheckboxInputComponent.vue
<template>
<div id="container">
<table>
<tbody>
<tr>
<td>
<input
v-model="inputValue"
type="checkbox"
:indeterminate="isIndeterminate"
@change="changeInputValue"
/>
</td>
<td id="clear-icon-container">
<img
v-if="!isIndeterminate"
id="clear-icon"
src="../../public/img/icons\clear-icon.svg"
alt="clear"
@click="clearInputValue"
/>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: "CheckboxInputComponent",
emits: ["inputValueChanged", "clearButtonClicked"],
data() {
return {
inputValue: false,
isIndeterminate: true,
};
},
methods: {
clearInputValue() {
this.isIndeterminate = true;
this.$emit("clearButtonClicked");
},
changeInputValue() {
this.isIndeterminate = false;
this.$emit("inputValueChanged", this.inputValue);
},
},
};
</script>
<style scoped>
table {
/* border: 1px solid black; */
width: -webkit-fill-available;
}
#clear-icon {
width: 0.7em;
height: 0.7em;
cursor: pointer;
}
input {
border: none;
outline: none;
}
#clear-icon-container {
width: 1em;
height: 1em;
text-align: center;
/* border: 1px solid; */
}
</style>
DateTimeInputComponent.vue
<template>
<div id="container">
<table>
<tbody>
<tr>
<td>
<input
v-model="inputValue"
ref="input"
type="datetime-local"
@change="changeInputValue"
/>
</td>
<td id="clear-icon-container">
<img
v-if="inputValue"
id="clear-icon"
src="../../public/img/icons\clear-icon.svg"
alt="clear"
@click="clearInputValue"
/>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: "DateTimeInputComponent",
emits: ["inputValueChanged", "clearButtonClicked"],
data() {
return {
inputValue: "",
};
},
methods: {
clearInputValue() {
this.inputValue = "";
this.$refs.input.focus();
this.$emit("clearButtonClicked");
},
changeInputValue() {
this.$emit("inputValueChanged", this.inputValue);
},
},
};
</script>
<style scoped>
table {
/* border: 1px solid black; */
width: -webkit-fill-available;
}
#clear-icon {
width: 0.7em;
height: 0.7em;
cursor: pointer;
}
input {
border: none;
outline: none;
width: -webkit-fill-available;
}
/* #clear-icon:hover {
background-color: bisque;
} */
#clear-icon-container {
width: 1em;
height: 1em;
text-align: center;
/* border: 1px solid; */
}
</style>
Здесь я лишь опишу ключевые отличия данных компонентов от предыдущего.
В CheckboxInputComponent фигурирует такой атрибут как indeterminate, который позволяет перевести чекбокс в "неопределенное" состояние, отличное от true / false. Это нам понадобится в дальнейшем при формирования объекта фильтрации для его передачи на backend.
DateTimeInputComponent практически аналогичен InputComponent.
В этих компонентах вместо события нажатия клавиши Enter мы следим за событием change:
@change="changeInputValue"
Также здесь отсутствует опция props со свойством inputType, так как эти компоненты имеют целевое назначение — для ввода булевого значения и даты/времени, соответственно.
У читателя, знакомого с таким принципом ООП как наследование, наверняка возник вопрос — "А нельзя ли было создать базовый компонент и наследоваться от него, чтобы сэкономить на коде?".
Разумеется, я не первый задался этим вопросом. Для реализации наследования во Vue есть такие интересные вещи как provide, inject, mixins и extends. Но для своего проекта я все-таки оставлю код таким, какой он есть и не буду нагромождать его новыми страшными словами. Понятно, что в сложных и больших проектах без наследования не обойтись, поэтому оправляю читателя к официальной документации по ссылкам, приведенным выше.
Еще один вариант избежать дублирования кода и сделать один компонент для всех типов ввода — попробовать создать свою кастомную директиву взамен директивы v-model, поведение которой будет зависеть от параметра inputType, однако даже в официальной документации не рекомендуют использовать кастомные директивы (либо в очень крайних случаях):
Централизованное хранилище данных. Vuex
В любом языке программирования злоупотребление глобальными переменными является не очень хорошей затеей. Я думаю, поэтому создатели Vuex и придумали эту великолепную библиотеку (да что уж там, целый паттерн!), чтобы иметь глобальный доступ к переменным и методам из любого участка приложения. Помимо официальной документации и еще с десяток статей (одна из которых на хабре) советую все-таки в начале посмотреть видео одного крутого парня в мире фронтенд-разработки.
А теперь к практике! Для чего нам мог понадобится Vuex?!
Vuex нам понадобится для того, чтобы определить два глобальных объекта (state):
для хранения данных, связанных с сортировкой, фильтрацией и пагинацией, чтобы на основе этих данных сформировать тело запроса на бэкенд;
для хранения общих (преимущественно стилевых) настроек грид компонента (список столбцов и их свойств, пэддинги, маргины и т.п.);
Создадим в папке store папку modules, а в ней два файла grid.js и gridOptions.js (вы можете придумать названия и получше =)), которые и будут хранить два состояния (объекты state) упомянутые выше.
Первый state (в модуле grid.js) будет выглядеть следующим образом:
state для реализации сортировки, фильтрации и пагинации:
state: {
dataGridRows: [], // массив строк, получаемых от бэкенда
dataGridRowsCount: null, // количество возвращаемых строк
dataSourceUri: "http://127.0.0.1:3000/documents/findPaginated", // адрес, куда мы будем ходить за данными
sorting: {
sortColumn: "createdOn", // название столбца для сортировки
sortDirection: "DESC", // направление сортировки
},
pagingData: {
pageSize: 5, // количество строк на странице
pageNumber: 1, // текущий номер страницы
},
filter: {} // объект с фильтрами
}
также сюда добавлен массив для хранения строк, возвращаемых бэкендом и их количество (да, выглядит странно, т.к. кол-во можно легко вычислить как число элементов массива, но забегая наперед скажу, что TypeORM, которая будет использоваться для запросов к БД, помимо строк возвращает в отдельном параметре и их количество, поэтому воспользуемся этим). И для удобства поместим сюда также URI backend-приложения.
А вот реальный пример итогового тела POST-запроса для отправки на бэкенд в формате JSON:
{
"paging": {
"skip": 0,
"take": 5
},
"sorting": {
"createdOn": "DESC"
},
"filtering": {
"createdOn": {
"columnType": "datetime-local",
"comparisonOperator": "greaterThanOrEqualOperator",
"valueToFilter": "2000-06-19T13:39"
},
"inArchive": {
"columnType": "checkbox",
"comparisonOperator": "equalOperator",
"valueToFilter": true
},
"id": {
"columnType": "number",
"comparisonOperator": "greaterThanOperator",
"valueToFilter": 10
}
}
}
Кратко о свойствах. Объект paging содержит два свойства — skip и take, которые определяют сколько строк "пропускать" и сколько "брать", соответственно. Они вычисляются нехитрым способом на основе объекта pagingData, находящемся в state. В данном функционале предусмотрено, что сортировка будет выполняться только по какому-либо одному столбцу. Название этого столбца и направление сортировки находятся в объекте sorting. Объект filtering содержит в себе данные о фильтрации — список столбцов, их типы, операторы сравнения и значения для фильтрации.
Экшн fetchDataGridRows выполняет асинхронный POST-запрос на бэкенд для получения данных из БД используя популярную библиотеку axios. Также стоит упомянуть про мутацию addNewColumnDataToFilter, которая накапливает фильтры, устанавливаемые пользователем. Остальные мутации и геттеры в файле grid.js предназначены для изменения и получения свойств объекта state, соответственно, и не требуют особых пояснений.
Второй глобальный state, о котором шла речь выше, находится в модуле gridOptions.js. Помимо state этот файл содержит лишь геттеры для получения свойств состояния и их последующей передачи в грид компонент для его настройки.
Пагинация
Компонент для пагинации состоит из 7 составляющих:
Селектор с предзаполненным списком количества строк на странице;
Блок с общим количеством строк в гриде;
Кнопка перехода на первую страницу;
Кнопка перехода на предыдущую страницу;
Текстовое поле с текущим номером страницы и возможностью его редактирования;
Кнопка с общим количеством страниц (по клику - переход на последнюю страницу);
Кнопка с переходом на следующую страницу;
Все пункты кроме второго изменяют состояние этого компонента, поэтому чтобы оповестить грид о переходах по страницам (пункты 3, 4, 5, 6, 7), об изменение кол-ва строк на странице (1) после любого изменения мы будем генерировать событие pagingDataChanged:
emits: ["pagingDataChanged"]
В этом компоненте используется новая опция — computed. Для тех, кто программировал на C# — это аналог свойств. Computed свойства вычисляются на основе данных в опции data. При этом мы также можем использовать геттеры и сеттеры для осуществления двусторонней привязки с помощью директивы v-model.
Самым "сложным" computed-свойством здесь является totalPageCount(), которое вычисляет итоговое количество страниц для кнопки (п.6) на основе двух параметров — количества загруженных строк в гриде this.$store.getters.dataGridRowsCount и количества строк на странице this.$store.getters.pageSize. А эти параметры, в свою очередь, хранятся в глобальном состоянии (state), о котором шла речь в предыдущем разделе.
Помимо computed свойств в этом компоненте используются методы, с помощью которых осуществляется навигация по страницам. Чем методы отличаются от свойств можно почитать тут.
FilterComponent
Добавим в папку с компонентами файл FilterComponent.vue.
Данный компонент будет состоять из выпадающего списка операторов сравнения и компонента ввода, о котором шла речь выше, тип которого будет зависеть от типа столбца (числовой, текстовый, дата и т.д.):
Данный компонент состоит из других компонентов, поэтому нужно это указать:
components: {
InputComponent,
CheckboxInputComponent,
DateTimeInputComponent,
}
имеет два свойства:
props: {
columnType: String,
columnName: String,
}
и генерит единственное событие:
emits: ["filterChanged"]
Метод defaultComparisonOperatorByColumnType(columnType) определяет оператор сравнения, который будет стоять в момент загрузки грида по умолчанию в зависимости от типа столбца. А метод getComparisonOperatorsByColumnType(columnType) формирует список операторов для каждого типа столбца.
Метод commitFilterAndEmit() вызывается после любого изменения, — он формирует объект с данными для фильтрации, коммитит мутацию по добавлению фильтров в store:
this.$store.commit("addNewColumnDataToFilter", columnData);
и эмитит событие изменения фильтра:
this.$emit("filterChanged");
GridComponent
Добавим файл грид-компонента. Для демонстрации возможностей грида была создана небольшая табличка с четырьмя колонками разных типов данных ("Id", "Шифр документа" , "Дата создания" и "В архиве"), см. рисунок.
Грид состоит из нескольких блоков:
Хедер c заголовками столбцов и чекбоксом "Выделить все" в левой части;
Строка с фильтрами;
Основная область с данными;
Пагинация;
Возможность фильтрации, пагинацию и мультистрочное выделение можно отключить с помощью соответствующих глобальных настроек грид-компонента в файле gridOptions.js:
behaviorOptions: {
allowMultipleSelection: true,
allowSelectAll: true,
allowPaging: true,
allowFiltering: true
}
В качестве шаблона грид-компонента выступает стандартная html таблица. Для стилизации этой таблицы используется возможность привязки атрибута style к JS-объектам. Например, на второй строчке стили для <table> привязываются через computed-свойство getCommonTableOptions:
<table :style="getCommonTableOptions">
Данное свойство (и ему подобные) имеет одну интересную особенность — его тело находится во Vuex-модуле в разделе getters:
getCommonTableOptions(state) {
let commonTableOptions = [];
Array.prototype.push.apply(commonTableOptions,
[state.commonTableOptions.borders.top,
state.commonTableOptions.borders.right,
state.commonTableOptions.borders.bottom,
state.commonTableOptions.borders.left,
state.commonTableOptions.collapse]
)
return commonTableOptions;
}
где в массив commonTableOptions накапливаются стили из state, а чтобы использовать этот геттер в файле компонента используется такая фича Vuex как mapGetters, которая позволяет смаппить геттеры Vuex в computed-свойства компонента. Большая часть таких стилевых свойств в коде компонента маппится именно с помощью mapGetters.
Грид генерирует только одно событие — selectionChanged:
emits: ["selectionChanged"]
которое файрится при изменении выделенных строк (либо одной строки) грида. Такая фича возможна благодаря уже знакомой директиве v-model и привязке к ней массива selectedRows:
<input
type="checkbox"
:value="row.id"
v-model="selectedRows"
@change="$emit('selectionChanged', selectedRows)"
/>
однако в массив selectedRows будут сохраняться не сами строки, а только значения столбца id, о чем говорит строчка:
:value="row.id"
обычно для большинства практических целей этого более чем достаточно — остальные значения строки можно найти по этому уникальному id.
После сортировки, фильтрации и пагинации (короче говоря, после любых действий пользователя, которые могут изменить табличные данные в гриде) диспатчится метод fetchDataGridRows
this.$store.dispatch("fetchDataGridRows");
о котором говорилось выше. Этот метод посылает запрос на бэкенд для получения данных из БД согласно фильтрам, сортировке и пагинации.
Еще немного о компонентах
Немного авторских мыслей. Как было сказано выше — основа фреймворка VueJS — это однофайловые компоненты. Они выступают в роли строительных блоков всего приложения. Однако слово компонент, лично у меня, ассоциируется именно с элементом управления (кнопка, комбобокс, грид и т.п.). Но ведь нужно где-то хранить макеты сайтов, лейауты и т.д., а это уже сложно назвать компонентами. Например, в таких паттернах как MVVM или MVC существует четкое разделение концепций. Там для отрисовки интерфейса используется представление (View), которое отвечает только за отображение данных и не участвует в логике работы приложения.
Так вот, к чему я это все...во Vue отсутствует эта строгая концепция разделения. Здесь компоненты — это наше ВСЕ! И это, наверное, главная особенность фреймворка VueJS. Любой файл с расширением .vue считается однофайловым компонентом. Поэтому одна из задач хорошего фронтендера при разработке больших приложений/сайтов/систем на Vue — это хорошо понимать и эффективно отделять компоненты (самопальные кнопки, выпадающие списки, гриды и т.д.) от компонентов-представлений (вьюхи, макеты, лейауты). Обычно первые хранятся в паке src/components, а для компонентов-представлений создают отдельную папку src/views. Хотя по сути — это все однофайловые компоненты.
В нашем небольшом приложении компоненты общаются между собой в основном посредством свойств и событий (props/emits), а также глобального store. В этой статье можно ознакомиться с другими способами взаимодействия компонентов.
Для демонстрации грида был добавлен компонент GridComponentView.vue с необходимыми мутациями.
Заключение
Надеюсь, мой труд пойдет кому-нибудь на пользу. А может быть кто-нибудь даже стянет себе исходники и добавит этот грид в свой небольшой, но перспективный проект.
Всем добра и качественного кода!
P.S. пулл риквесты и доработки никто не запрещал...милости прошу.