Русский
Русский
English
Статистика
Реклама

Webix

Фреймворк Webix Jet глазами новичка. Часть 1. Композиция и навигация

20.03.2021 14:15:19 | Автор: admin

В предыдущих статьях из цикла JavaScript библиотека Webix глазами новичка вы узнали как создать приложение на основе компонентов Webix UI. В этой публикации я хочу подробно рассмотреть создание аналогичного приложения с использованием архитектуры и возможностей фреймворка Webix Jet, а также какие преимущества это дает. Статья будет полезна как для тех кто уже знаком с Webix UI, так и для новичков, ведь код библиотеки интуитивно понятен и удобен в использовании.

Обзор Webix Jet

Webix это библиотека UI компонентов. Для создания полноценного приложения на основе Webix UI вам не обязательно использовать дополнительные инструменты. Однако по мере расширения и увеличения количества кода могут возникнуть трудности с его организацией. Команда Webix позаботилась о решении этой проблемы и разработала собственный фреймворк для построения архитектуры приложения.

Фреймворк Webix Jet позволяет создавать очень гибкое и хорошо управляемое одностраничное приложение, используя паттерн Model-View. При таком подходе, логика работы с данными и отображение элементов интерфейса четко разделяются. Каждый компонент разрабатывается и тестируется отдельно от других. Также имеются готовые решения многих задач, которые реализуются через API, плагины и конфигурации.

Архитектура Jet-приложения основана на модулях view компонентов. Каждый компонент являет собой независимую часть интерфейса. Вы можете комбинировать модули, использовать повторно в разных частях приложения и управлять их отображением с помощью URL. Подробнее о работе с Webix-jet вы узнаете на примере создания собственного SPA.

Построение собственного Webix Jet приложения

Процесс создания приложения мы рассмотрим в следующем порядке:

  • Интерфейс приложения

  • Структура приложения

  • Создание и конфигурация приложения

  • View модули интерфейса

  • Модели работы с данными

  • Локализация приложения

В документациях Webix UI и Webix Jet можно ознакомиться с использованными в статье компонентами и API фреймворка.

С кодом готового приложения можно ознакомиться тут.

Интерфейс приложения

Начнем с описания интерфейса нашего приложения и разделения его на смысловые части. Сделать это крайне важно, так как каждую обособленную часть мы будем разрабатывать и хранить в отдельном модуле.

Интерфейс разделяем на 3 части:

  • Тулбар

  • Сайдбар

  • Панель сменяемых модулей

Тулбар находится в верхней части экрана. Он содержит лейбл с названием приложения в левой части и кнопки переключения локализации в правой.

Сайдбар находится в левой части экрана и содержит меню для навигации между сменяемыми модулями интерфейса.

Панель сменяемых модулей составляет основу приложения и расположена в центральной части экрана. Здесь вы сможете видеть 3 автономных модуля, которые содержат определенную часть интерфейса приложения. Они будут сменять друг друга по клику на опции меню.

Структура приложения

У нашего приложения будет следующая структура:

  • файл index.html является стартовой страницей и единственным html файлом

  • файл sources/myapp.js содержит необходимые конфигурации приложения

  • папка sources/views состоит из следующих файлов:

    • top.js содержит главный модуль, который объединяет все элементы интерфейса

    • toolbar.js содержит модуль с описанием тулбара

    • form.js содержит модуль с описанием формы

    • films.js содержит сменяемый модуль

    • users.js содержит сменяемый модуль

    • products.js содержит сменяемый модуль

  • папка sources/models содержит модули для операций с данными

  • папка sources/styles содержит CSS файлы

  • папка sources/locales содержит модули локализации.

Такая организация кода помогает разделять разные аспекты разработки на отдельные составляющие и эффективно управлять ими. Это особенно актуально при работаете с большим количеством кода.

Мы будем создавать интерфейс приложения комбинируя несколько subview модулей в файле top.js. Subview - это модули интерфейса, которые входят в состав других view. Они могут быть статическими и динамическими. Давайте подробнее их рассмотрим.

Статические subview импортируются при помощи команды import и включаются в интерфейс компонента напрямую. Например, мы будем использовать статический модуль toolbar.js в главном модуле top.js.

Для понимания принципа работы динамических subview важно упомянуть о такой сущности Jet, как URL. Он позволяет формировать иерархию отображения view модулей и осуществлять навигацию между ними. URL отображается в адресной строке браузера после имени домена и спецсимвола `/#!/`.

Например, с помощью URL мы можем указать модуль, который будет отображаться первым:

http://localhost:8080/#!/top - приложение отобразит интерфейс файла top.js

http://localhost:8080/#!/top/films - приложение отобразит интерфейс файла top.js и films.js (если в top.js указан плейсхолдер для films.js).

Мы реализуем 3 динамических subview модуля, каждый из которых будет отображаться при соответствующем значении URL:

  • films.js ( http://localhost:8080/#!/top/films )

  • users.js ( http://localhost:8080/#!/top/users )

  • products.js ( http://localhost:8080/#!/top/products )

Навигация между динамическими subview реализуется при помощи изменения URL. Например, если нам надо отобразить модуль users на панели сменяемых модулей, мы можем изменить текущий URL на http://localhost:8080/#!/top/users и увидим интерфейс файла top.js и users.js. Переключение между опциями меню работает по такому же принципу.

Навигация с помощью URL удобна и тем, что браузер хранит историю посещения разных url-адресов, и при клике на кнопку Назад в браузере, мы можем перейти к предыдущему модулю. Также URL хранит состояние приложения. Если пользователь переключится на вкладку Users при помощи меню и обновит страницу, то приложение отобразит тот модуль, URL которого был установлен в адресной строке браузера до перезагрузки.

Если такое отображение URL неудобно, вы можете либо убрать спец. символы из URL, либо вовсе не показывать его в адресной строке - на работу приложения это не повлияет.

Подробнее о настройках URL можно узнать тут.

Подробнее ознакомиться с тонкостями использования subview вы можете тут.

Создание и конфигурирование приложения

Итак, мы разобрали интерфейс и структуру файлов нашего демо приложения. Сейчас пришло время непосредственно заняться его созданием. Для начала важно правильно настроить нужные конфигурации и подключить необходимые инструменты.

В файле sources/myapp.js находится модуль конфигурации приложения. Он расширяется базовым классом JetApp, который содержит все необходимые инструменты управления.

Импортируем и подключаем стили css и базовый класс JetApp.

import "./styles/app.css";import { JetApp } from "webix-jet";

Создаем класс MyApp нашего приложения и наследуем его от базового JetApp. В конструкторе класса прописываем необходимые настройки.

Для активации режима отладки нужно установить параметр debug:true. Теперь фреймворк будет отображать все ошибки, которые могут возникнуть в процессе разработки модулей. Преимущество работы с Jet состоит в том, что сбои в работе отдельных модулей не будут влиять на работоспособность всего приложения, но без debug:true вы о них не узнаете.

Для определения стартовой страницы нужно установить параметр start и указать значение URL для начальной загрузки: start:"/top/films". Теперь при первоначальной загрузке приложения мы увидим главный модуль top.js и сменяемый модуль films.js.

export default class MyApp extends JetApp{constructor(config){const defaults = {//активируем режим отладки, для отображения ошибокdebug:true, //устанавливаем стартовый URL загрузки приложенияstart:"/top/films"   };   super({ ...defaults, ...config });}}

После создания класса и внесения необходимых конфигураций, нам нужно его инициализировать.

const app = new MyApp();

Далее отображаем приложение через метод render() и оборачиваем его в webix.ready(), чтобы HTML страница успела загрузиться до начала выполнения кода.

webix.ready(() => app.render());

Мы создали и настроили наше приложение. Теперь самое время перейти к непосредственной разработке view модулей интерфейса.

View модули интерфейса

Мы будем создавать view компоненты как классы ES6 и наследовать их от базового JetView. Такой подход позволяет расширить модуль встроенными Jet методами и получить доступ к управлению жизненным циклом компонента.

В начале каждого модуля необходимо подключить базовый JetView класс, который хранит конфигурации и инструменты для управления логикой view модулей.

В собственных классах мы используем методы config() и init(), которые наследуются от JetView, а также можем создавать собственные методы для хранения кастомной логики.

Элементы интерфейса описываем и возвращаем через метод config().

Работу с данными осуществляем через метод init().

Модуль TopView (top.js)

Давайте создадим главный модуль TopView в файле top.js, который будет включать сайдбар, футер, тулбар и динамические subview модули. В адресной строке он будет отображаться как #!/top/ .

Тулбар мы создадим в другом модуле и импортируем сюда. Он будет включаться как статический subview.

Импортируем базовый класс JetView и модуль ToolbarView.

import { JetView } from "webix-jet";import ToolbarView from "views/toolbar";

Создаем и наследуем класс TopView от базового JetView. В методе config() описываем основные элементы интерфейса и возвращаем комбинированный view.

export default class TopView extends JetView{  config(){    //здесь будет описан интерфейс  }}

Обратите внимание, что созданный класс необходимо экспортировать при помощи команды export default, чтобы Webix Jet включил его в приложение.

Внутри метода config() мы описываем сайдбар, который содержит меню с опциями для переключения динамических модулей интерфейса. Для этого используем компонент List библиотеки Webix UI. В свойстве data указываем названия опций меню и их id, которые будут использоваться для формирования URL.

const sidebar = {view:"list",data:[{ value:"Dashboard", id:"films" },{ value:"Users", id:"users" },{ value:"Products", id:"products" }]};

Описываем футер, который содержит информацию копирайта и находится в самом низу экрана. Мы не выделяем его в отдельный модуль, так как это незначительный элемент. Для этого используем компонент Template.

const footer = {template:"The software is provided by <a href='https://webix.com'> webix.com </a>. All rights reserved &#169;"};

Теперь нужно объединить все компоненты и сформировать структуру интерфейса приложения. Помимо собственных компонентов сайдбар и футер, мы включаем статический ToolbarView и динамические subview.

config(){  const ui = {    rows:[      ToolbarView, //включаем как статический subview модуль       {        cols:[          sidebar,          { view:"resizer" },          { $subview:true } //включаем динамические subview        ]      },      footer    ]  };  return ui; }

Возвращаем комбинированный view ui, который включает в себя все элементы интерфейса.

Теперь нужно настроить панель сменяемых модулей (плейсхолдер { $subview:true } ).

При выборе той или иной опции меню, наше приложение будет задавать необходимый сегмент URL (films, users, products) и отображать одноименный модуль на месте плейсхолдера {$subview:true}. Обязательное условие, чтобы id опций меню и названия файлов, хранящих модули интерфейса, совпадали.

Для того чтобы установить необходимый URL, воспользуемся встроенным методом JetView this.show(id). В качестве аргумента передаем id соответствующей опции меню. Этот id задает URL относительно того модуля, в котором вызван метод show(). В нашем случае метод изменит значение URL после #!/top/ и установит туда переданный в аргументе id. Таким образом, полный URL будет #!/top/{id}.

Например, при клике на опцию Users, виджет вызовет this.show("users") и установит URL #!/top/users. На месте { $subview:true } отобразится модуль файла views/users.js .

Модуль ToolbarView (toolbar.js)

Определяем модуль ToolbarView в файле toolbar.js, который включается как статический subview в главном модуле файла top.js.

Создаем и наследуем класс ToolbarView от базового JetView. В методе config() описываем необходимые компоненты и возвращаем их.

Описываем тулбар, который содержит лейбл с названием приложения и кнопки переключения локализации. Для этого используем такие компоненты Webix UI как Toolbar, Label и Segmented.

export default class TopView extends JetView{  config(){    const toolbar = {      view:"toolbar",      elements:[        { view:"label", label:"Demo App" },        {          view:"segmented",          options:[            { id:"en", value:"En" },            { id:"ru", value:"Ru" }          ]        }      ]    };    return toolbar;   }}

Теперь пришло время перейти к созданию динамических subview, которые будут сменяться в зависимости от сегмента URL, следующего после #!/top/. У нас будет 3 сменяемых модуля:

  • FilmsView

  • UsersView

  • ProductsView.

Каждый модуль мы будем разрабатывать в отдельном файле.

Модуль FilmsView (films.js)

Интерфейс модуля состоит из таблицы фильмов и формы для их редактирования. Таблица состоит из 5 столбцов, в которых мы реализуем сортировку и фильтрацию данных. Форму создадим в отдельном модуле и будем включать ее как статический subview.

Определяем модуль FilmsView в файле films.js, который будет включаться как динамический subview в файле top.js. Модуль будет отображаться при значении URL: #!/top/films.

В начале кода импортируем модуль FormView из файла form.js, который содержит форму для редактирования фильмов и будет включаться как статический subview.

import FormView from "views/form";

Создаем и наследуем класс FilmsView от базового JetView. В методе config() описываем основные элементы интерфейса и возвращаем комбинированный view.

Описываем таблицу фильмов с помощью компонента Datatable. Через свойство columns конфигурируем столбцы. У каждого столбца должен быть уникальный id. Сортировка данных столбца реализуется при помощи свойства sort. Для столбцов с текстовыми значениями мы используем sort:"text", а для числовых sort:"int". Чтобы добавить фильтр в хедер столбца, мы используем конфигурацию {content:"textFilter"} для текстовых значений и {content:"selectFilter"} для числовых.

const film_table = {  view:"datatable",  columns:[    { id:"id", header:""},    { id:"title", header:["Film title", { content:"textFilter" }], fillspace:true, sort:"text" },    { id:"year", header:["Released", { content:"selectFilter" }], sort:"int" },    { id:"votes", header:"Votes", sort:"int" },    { id:"rating", header:"Rating", sort:"int" }  ]};

Формируем интерфейс модуля. Включаем собственный компонент film_table с таблицей фильмов и статический модуль FormView, который хранит интерфейс формы для редактирования фильмов.

return {  cols:[    film_table,    FormView //включаем как статический subview модуль   ]};

Модуль FormView (form.js)

Интерфейс модуля содержит форму для редактирования фильмов. Форма состоит из 4 полей ввода, а также кнопок Сохранить и Очистить.

Определяем модуль FormView в файле form.js. Создаем и наследуем класс FormView от базового JetView. В методе config() описываем основные элементы интерфейса и возвращаем комбинированный view.

Описываем форму для редактирования фильмов с помощью компонента Form. Через свойство elements конфигурируем поля ввода и кнопки для управления формой.

config(){  const film_form = {    view:"form",    elements:[      { type:"section", template:"edit films" },      { view:"text", name:"title", label:"Title" },      { view:"text", name:"year", label:"Year" },      { view:"text", name:"rating", label:"Rating" },      { view:"text", name:"votes", label:"Votes" },      {        cols:[          { view:"button", value:"Save", css:"webix_primary" },          { view:"button", value:"Clear", css:"webix_secondary" }        ]      },      {}    ]  };  return film_form;}

Модуль UsersView (users.js)

Интерфейс модуля состоит из тулбара, списка пользователей и диаграммы пользователей. В тулбаре находится строка поиска, кнопка для добавления новых пользователей, а также 2 кнопки для сортировки списка пользователей по возрастанию и убыванию. Список пользователей содержит имя, возраст и страну. Диаграмма отображает возраст пользователей.

Определяем модуль UsersView в файле users.js, который будет включаться как динамический subview в файле top.js. Модуль будет отображаться при значении URL: #!/top/users.

Создаем и наследуем класс UsersView от базового JetView. В методе config() описываем элементы интерфейса при помощи компонентов List, Toolbar и Chart.

Описываем список пользователей при помощи компонента List. Через свойство template конфигурируем шаблон списка, который будет включать имя, возраст и страну.

const list = {view:"list",template:"#name#, #age#, #country#"};

Описываем тулбар для работы со списком при помощи компонента Toolbar . Через свойство elements конфигурируем строку поиска и кнопки для управления списком пользователей.

const list_toolbar = {  view:"toolbar",  elements:[    //кнопка добавления нового пользователя    { view:"button", value:"Add new person", css:"webix_primary" },    //строка поиска    { view:"search" },    //кнопки сортировки    { view:"button", value:"Sort asc" },    { view:"button", value:"Sort desc" }  ]};

Описываем диаграмму пользователей при помощи компонента Chart. Задаем тип диаграммы type:"bar", значения для столбцов value:"#age#". Конфигурируем оси при помощи свойств xAxis и yAxis.

const chart = {  view:"chart",  type:"bar",  value:"#age#",  xAxis:{    template:"#name#",    title:"Age"  },  yAxis:{    start:0,    end:100,    step:10  }};

Формируем интерфейс модуля. Комбинируем и возвращаем компоненты list_toolbar, list и chart.

return { rows:[list_toolbar, list, chart] };

Модуль ProductsView (products.js)

Интерфейс модуля состоит из древовидной таблицы, которая содержит элементы высшего и вложенного уровней.

Определяем модуль ProductsView в файле products.js, который будет включаться как динамический subview в файле top.js. Модуль будет отображаться при значении URL: #!/top/products.

Создаем и наследуем класс ProductsView от базового JetView. В методе config() описываем древовидную таблицу при помощи компонента Treetable. Через свойство columns конфигурируем столбцы таблицы. Используем конфигурацию template:"{common.treetable()} #title#", чтобы задать вложенные элементы.

config(){  const products_treetable = {    view:"treetable"    columns:[      { id:"id", header:"" },      { id:"title", header:"Title", fillspace:true,  template:"{common.treetable()} #title#" },      { id:"price", header:"Price" }    ]  };  return products_treetable;}

Мы описали интерфейс нашего приложения. Теперь пришла очередь рассказать о том, как осуществляется работа с данными.

Модели работы с данными

Давайте разбираться с тем, как работать с моделями данных. В принципе, подключать данные не обязательно, и можно загружать их сразу в компоненты минуя модели. Но для серьезного приложения такие сущности как view и model лучше разделять.

Логика работы с моделями данных хранится в файлах директории sources/models. Предполагается, что мы взаимодействуем с сервером и загружаем данные асинхронно. Для этого используем метод webix.ajax().

Функция webix.ajax() делает асинхронный запрос по указанному в аргументе адресу и возвращает промис-объект. В случае успешной загрузки мы получим объект, значения которого будем загружать в необходимые нам модули.

Данные нам нужны для 3 сменяемых subview:

  • FilmsView

  • UsersView

  • ProductsView.

Для каждого модуля создаем отдельную модель, как на примере ниже.

// models/films.jsexport function getData(){return webix.ajax("../../data/film_data.js");}

Все 3 модели будут отличаться только аргументом запроса, в котором мы указываем путь к данным на сервере. В нашем случае данные хранятся в папке ../../data/.

Теперь для каждого view модуля необходимо импортировать метод getData из соответствующей модели.

import { getData } from "models/films";  // для FilmsViewimport { getData } from "models/users";  //для UsersViewimport { getData } from "models/products";  //для ProductsView.

Далее мы должны передать данные из модели в необходимый компонент. Интерфейс модуля описывается внутри метода config() и становится доступным после инициализации. Внутри специального метода init() класса JetView, мы можем получить доступ к нужному data-компоненту (List, Datatable, Chart) и загрузить в него данные при помощи метода parse().

config(){...  return {    cols:[      film_table,      FormView     ]  };}init(view){  const datatable = view.queryView("datatable");  datatable.parse(getData());}

В метод init() приходит параметр view, который ссылается на объект, возвращенный методом config() того же модуля. Это может быть layout с таблицей и другими компонентами. Внутри layout мы ищем таблицу по названию компонента (view.queryView("datatable");), а затем загружаем в нее данные.

Чтобы передать данные из промис-объекта который возвращает нам метод getData(), нужно вызвать метод parse() для Webix компонента и передать туда промис в качестве аргумента.

Когда данные будут загружены, компонент их обработает и отобразит в интерфейсе приложения.

Локализация приложения

Инструментарий фреймворка Webix Jet позволяет с легкостью реализовать локализацию приложения. Для этого мы будем использовать встроенный сервис Locale.

Этот сервис нужно подключить в файле sources/myapp.js, т.к. он будет использоваться глобально для всего приложения. Для этого мы импортируем модуль plugins, который содержит встроенные сервисы, вместе с базовым JetApp.

import { JetApp, plugins } from "webix-jet";

После создания и инициализации класса MyApp, мы подключаем сервис plugins.Locale. Сделать это необходимо до вызова метода render().

const app = new MyApp();app.use(plugins.Locale);webix.ready(() => app.render());

Сервис Locale устанавливает связь с файлами локализации, которые находятся в директории sources/locales. Файлы en.js и ru.js хранят объекты с элементами локализации.

// locales/en.jsexport default {Dashboard : "Dashboard",Users : "Users",...}// locales/ru.jsexport default {Dashboard : "Панель",Users : "Пользователи",...}

Далее, в каждом отдельном view модуле мы получаем метод из сервиса локализации.

 const _ = this.app.getService("locale")._;

Этот метод мы будем вызывать каждый раз, когда нам надо перевести какое-либо текстовое выражение. В качестве аргумента нужно передавать ключи, по которым будут подтягиваться значения из объекта файла установленной локали: _("some text"). При смене локализации, Jet перерисует приложение с учетом значений установленной локали.

Почти готово, осталось только рассказать о том, каким образом пользователь сможет переключать языки в нашем приложении. Как вы помните, в тулбаре мы создали кнопку Segmented со значениями En и Ru. Теперь нам нужно создать обработчик, который будет вызываться при клике на соответствующую кнопку и применять нужную локализацию.

config(){...  {     view:"segmented",     options:[      { id:"en", value:"En" },      { id:"ru", value:"Ru" }    ],    click:() => this.toggleLanguage()   }...}toggleLanguage(){  const langs = this.app.getService("locale");   const value = this.getRoot().queryView("segmented").getValue();  langs.setLang(value);}

Для этого мы создаем функцию-обработчик toggleLanguage() как метод класса ToolbarView. Здесь же находится сегментная кнопка, при клике на которую мы будем вызывать наш обработчик.

Внутри функции получаем доступ к сервису локализации, считываем значение заданной опции (значение берется из id - "en" или "ru") и устанавливаем выбранную локаль с помощью метода setLang().

При описании функции используем метод this.getRoot(). С его помощью мы получаем доступ к view, возвращенный нам методом config() - здесь это Toolbar. Внутри view мы ищем компонент с именем segmented, чтобы получить объект кнопки и считать ее значение.

Вот такими простыми манипуляциями в Webix Jet реализуется смена локализации.

Заключение

С помощью фреймворка Webix Jet мы создали структурированное одностраничное приложение. Интерфейс разделен на модули view, которые хранятся в отдельной директории sources/views. Логика работы с данными отделена от компонентов интерфейса и хранится в директории sources/models. Реализована локализация приложения за счет использования плагина Locale базового класса JetApp.

Сейчас это мини-приложение, но с такой архитектурой его легко масштабировать и поддерживать.

С кодом готового приложения можно ознакомиться тут.

Webix Jet имеет достаточно широкий спектр инструментов и конфигураций, которые значительно упрощают процесс разработки приложения на основе компонентов Webix UI. Более детально мы рассмотрим их в следующих статьях посвященных Webix Jet.

Подробнее..

Фреймворк Webix Jet глазами новичка. Часть 2. Взаимодействие с интерфейсом

04.05.2021 12:13:46 | Автор: admin

В предыдущей статье Фреймворк Webix Jet глазами новичка. Часть 1. Композиция и навигация мы подробно разобрали создание интерфейса нашего приложения с помощью UI компонентов Webix и распределили полномочия между view-модулями и моделями внутри архитектуры Jet фреймворка.

В этой статье мы продолжим изучать Jet и библиотеку Webix и реализуем следующее:

  • добавим в уже известное вам приложение немного интерактива

  • организуем серверные модели с разными подходами к загрузке и сохранению данных.

С кодом готового приложения можно ознакомиться тут.

Коротко о Webix и Webix Jet

Не лишним будет напомнить, что Webix это библиотека UI компонентов. Для создания полноценного приложения на основе Webix UI вам не обязательно использовать дополнительные инструменты. Однако по мере расширения и увеличения количества кода могут возникнуть трудности с его организацией. Команда Webix позаботилась о решении этой проблемы и разработала собственный фреймворк для построения архитектуры приложения.

Фреймворк Webix Jet позволяет создавать очень гибкое и хорошо управляемое одностраничное приложение, используя паттерн Model-View. При таком подходе, логика работы с данными и отображение элементов интерфейса четко разделяются. Каждый модуль разрабатывается и тестируется отдельно от других. Также имеются готовые решения многих задач, которые реализуются через API, плагины и конфигурации. В этой статье мы постараемся детальнее разобраться как все работает и дополним наше приложение новым функционалом.

В документацияхWebix UIиWebix Jetможно ознакомиться с использованными в статье компонентами и API фреймворка.

Модель данных для фильмов

Начнем мы пожалуй из модели данных для фильмов. У нас есть 2 модуля, которые будут использовать одинаковые данные, это: FilmsView и FormView. Первый представляет собой таблицу, где отображаются данные о фильмах, а второй форму для их редактирования. В интерфейсе они выглядят следующим образом:

Интерфейс вкладки ПанельИнтерфейс вкладки Панель

Если 2 модуля используют одни и те же данные, хорошей практикой будет загрузить их 1 раз и импортировать в нужные модули. Для этих случаев у библиотеки Webix предусмотрена такая сущность как DataCollection. Именно ее мы будем использовать в качестве модели для работы с данными.

Давайте создадим нашу коллекцию, которая будет выступать этаким буфером между view-модулями и сервером:

export const data = new webix.DataCollection({  url:"../../data/film_data.js",  save:"rest->save/films"});

В качестве параметров мы передаем конструктору DataCollection объект со свойствами url и save. Свойство url хранит путь, по которому данные будут загружаться в коллекцию при ее инициализации. Свойство save хранит путь, по которому будут отправляться запросы на изменение данных (добавление, обновление и удаление) соответствующими методами (POST, PUT, DELETE).

Итак, хранилище данных у нас есть. Давайте импортируем созданную модель в каждый из модулей FilmsView и FormView с помощью следующей строки:

import { data } from "models/films";

В модуле FilmsView нам нужно загрузить данные коллекции в таблицу при помощи ее метода parse(). Лучше всего это делать после инициализации компонента. Для этого предусмотрен такой JetView метод как init():

init(){  this.datatable = this.$$("datatable"); //получаем доступ к таблице через ее localIdthis.datatable.parse(data); //загружаем данные в таблицу}

Взаимодействие между модулями FilmsView и FormView через URL

По нашему замыслу, приложение должно работать таким образом, что пользователь выбирает любой элемент в таблице с фильмами, а данные о фильме автоматически отображаются в форме редактирования. На первый взгляд задача может показаться мудреной, так как view-компоненты находятся в разных модулях и не знают о существовании друг друга. Но на самом деле все донельзя прозаично.

Есть несколько рекомендуемых способов коммуникации, и один из них реализуется через URL. При таком подходе, один модуль будет устанавливать определенное значение в параметр url, а другой считывать его. Давайте посмотрим как это реализуется на примере наших модулей FilmsView и FormView.

Настраиваем модуль FilmsView

Представим ситуацию, что пользователь пролистал таблицу и выбрал некий элемент под номером 100. Как зафиксировать это состояние? Можно решить эту проблему с помощью url. Расклад будет таким, что при выборе элемента в таблице мы будем устанавливать его id в качестве параметра url. Делается это с помощью такого JetView метода как setParam(). В качестве аргументов нужно передать название url-параметра и его значение (в нашем случае это id выбранного элемента таблицы), а также аргумент, который отвечает за отображение установленного url параметра (его нужно установить в значении true).

Чтобы все это реализовать, нужно задать обработчик события onAfterSelect для таблицы и там вызвать вышеупомянутый метод. Делаем это с помощью специального свойства on:

{  view:datatable,  columns:[  ],  on:{ onAfterSelect:(id) => this.setParam("id", id, true) }}

Теперь, при клике на любую запись таблицы, мы увидим ее id в параметре url. Выглядеть это будет следующим образом:

//значение url при выборе сотого элемента таблицыhttp://localhost:8080/#!/top/films?id=100 

Состояние мы зафиксировали, а теперь давайте сделаем так, чтобы при перезагрузке страницы выбранный элемент по прежнему оставался выбранным.

Дабы осуществить этот замысел, нам необходимо считать установленный в параметре url идентификатор и выделить соответствующий элемент в таблице.

Для этих целей JetView предусматривает специальный метод urlChange(), который вызывается при любых изменениях в url. В теле метода мы получаем значение id, установленного нами в параметре url с помощью такого JetView метода как getParam(). Далее, необходимо проверить наличие записи с таким идентификатором в нашей коллекции data, с которой связана таблица фильмов, а после выбрать ее в таблице.

Выглядит это следующим образом:

urlChange(){  const id = this.getParam("id");  if(data.exists(id)){ //проверка существования записи    this.datatable.select(id); //выбор записи  }}

Теперь можно усложнить задачу. Представим, что пользователь только загрузил страницу и еще не выбрал нужный ему элемент. Что будет в этом случае? Давайте сделаем так, чтобы выбирался первый элемент таблицы, которая связана с коллекцией.

Для этого нам нужно вернуться к методу urlChange() и немного его модернизировать. Чтобы получить id первого элемента, у коллекции предусмотрен такой метод как getFirstId(), который мы и будем использовать. Теперь код будет иметь следующий вид:

urlChange(){  const id = this.getParam("id") || data.getFirstId();  if(data.exists(id)){    this.datatable.select(id);    this.datatable.showItem(id);  }}

В таком состоянии метод сначала считывает и проверяет значение id c параметра url, а в случае, если он не установлен, получает id первого элемента коллекции. Далее он выбирает нужный нам элемент в таблице ее методом select(), а также прокручивает таблицу до выбранной записи с помощью метода showItem(). Это удобно, если таблица имеет много записей.

Теперь все работает красиво. Осталось учесть один небольшой нюанс. Если данные не успели загрузиться, наша задумка может не сработать, так как таблица будет еще пустой. Здесь нужно упомянуть, что у всех дата-компонентов, включая коллекцию, есть такое свойство как waitData, в котором хранится промис объект загружаемых данных. Давайте воспользуемся этим преимуществом и дождемся полной загрузки данных в коллекцию, а тогда уже выполним необходимые нам действия. В конечном итоге код метода urlChange() будет таким:

urlChange(){  data.waitData.then(() => {    const id = this.getParam("id") || data.getFirstId();    if(data.exists(id)){      this.datatable.select(id);      this.datatable.showItem(id);    }});}

По-моему, ничего сложного в этом нет. Если возникнут какие-либо вопросы, не стесняемся и задаем их в комментариях. А на очереди у нас модуль с формой редактирования.

Настройка модуля FormView

По нашему замыслу, если пользователь выбирает любую запись в таблице фильмов, соответствующие данные должны отображаться в полях формы модуля FormView.

Как вы помните, id выбранного элемента таблицы хранится в параметре url и меняется в соответствии с выбранной записью. Давайте поймаем его изменение в методе urlChange() нашего модуля и используем по назначению. Для этого нужно определить и настроить метод:

urlChange(){const id = this.getParam("id");if(id && data.exists(id)){const form_data = data.getItem(id);this.form.setValues(form_data);}}

Как и в примере с модулем FilmsView, мы получаем значение id из параметра url при помощи JetView метода getParam(). Дальше необходимо проверить, существует ли запись с указанным id в нашей коллекции data, которую мы ранее сюда импортировали. Если все условия соблюдены, остается получить данные из коллекции и установить их в соответствующие поля формы.

Чтобы получить необходимую запись коллекции по id, необходимо вызвать у нее метод getItem() и передать id нужного элемента в качестве параметра.

Дальше мы отправляем объект с данными в соответствующие поля формы с помощью ее метода setValues(). Он установит значения для всех полей сразу. Нужно учесть, что свойство name каждого поля формы должно соответствовать ключам, под которыми данные хранятся в объекте.

Теперь уже все работает достаточно серьезно. При первоначальной загрузке выбирается первый элемент таблицы, а форма заполняется соответствующими данными. При выборе другого фильма и последующей перезагрузке страницы, фильм по прежнему остается выбранным, а поля формы заполненными. Что задумали, то и реализовали. Но самое главное, что теперь мы знаем как настроить коммуникацию между view-компонентами разных модулей при помощи url и методов для работы с ним.

Работа с данными через коллекцию

Теперь же давайте разберемся с тем, как настроить сохранение, изменение и удаление фильмов. У нас уже есть модель данных и настроено взаимодействие между формой и таблицей через url. За все операции с данных будет отвечать модель, в качестве которой мы используем Webix DataCollection.

Как вы помните, для коллекции мы задали путь к серверному скрипту через свойство save, по которому будут отправляться запросы на изменение данных:

export const data = new webix.DataCollection({url:"../../data/film_data.js",save:"rest->save/films"});

Но как коллекция сможет их отправить?

Как и в случае с другими дата-виджетами Webix, свойство save у коллекции обеспечивает автоматизацию серверных запросов, а именно, при любом добавлении, обновлении и удалении данных, коллекция сможет сама отправить запрос на сервер.

Обратите внимание, что это оптимистический подход: сначала данные меняются в самой коллекции (а также синхронизированных с ней виджетах), а только потом запрос уходит на сервер. Клиент не ждет ответа от сервера, а обновляет данные сразу, что значительно ускоряет работу приложения. Правда, если сервер ответит ошибкой, нужно будет ее обработать и сообщить об этом пользователю.

Теперь давайте перейдем непосредственно к самим операциям.

Начнем мы пожалуй с формы, которая хранится в модуле FormView. Напомню, что данные попадают в форму при выборе любой записи в таблице. Что должно произойти дальше? Правильно, пользователь должен их отредактировать и отправить в коллекцию при клике на кнопку Save. Чтобы это реализовать, нужно создать обработчик. Давайте этим и займемся:

saveFilmHandler(){  const values = this.form.getValues();  if(this.form.validate()){    if(data.exists(values.id)){      data.updateItem(values.id, values);    }  }}

Здесь мы получаем объект со значениями формы с помощью ее метода getValues(), запускаем валидацию методом validate() и, в случае успеха, проверяем наличие этих данных в коллекции соответствующим методом exists().

Для обновления данных в коллекции у нее предусмотрен такой метод как updateItem(). В качестве аргумента мы должны передать id, под которым хранятся данные и непосредственно сам объект с данными:

if(data.exists(values.id)){data.updateItem(values.id, values); //обновляет данные в коллекции по id}

Выглядит уже неплохо. Давайте установим наш обработчик на событие клика по кнопке Save. Для этого мы используем специальное свойство click:

{ view:"button", value:"Save", click:() => this.saveFilmHandler() }

Обработчик установлен, осталось проверить как все работает. Выбираем любую запись в таблице, редактируем данные в форме и сохраняем их в коллекцию. Так как таблица синхронизирована с коллекцией, ее обновленные данные незамедлительно отобразятся в интерфейсе.

С изменением уже существующих данных все понятно. А как же добавить новые спросите вы? А это еще проще. Давайте предусмотрим такой сценарий, чтобы после очищения формы, введенные данные добавлялись в начало таблицы. Сперва создадим обработчик, который будет очищать форму при клике по кнопке Clear:

clearFormHandler(){this.form.clear(); //очищаем все поля формыthis.form.clearValidation(); //убираем маркеры валидации}

После этого, устанавливаем обработчик на событие клика по соответствующей кнопке:

{ view:"button", value:"Clear", click:() => this.clearFormHandler() }

После очистки формы можно ввести новые данные и попробовать сохранить их. Результата не будет, потому что наш обработчик saveFilmHandler() настроен только на обновление существующей записи по ее id. Давайте его немного модифицируем и добавим метод add(), который будет отправлять новые данные в коллекцию. Теперь наш обработчик выглядит следующим образом:

saveFilmHandler(){const values = this.form.getValues();if(this.form.validate()){if(data.exists(values.id)){data.updateItem(values.id, values);}else{data.add(values, 0); //добавляет новую запись в первую позицию}}}

Методу add() мы передаем объект с данными и индекс, под которым эти данные должны добавиться. Теперь осталось только проверить результат. Очищаем форму, вводим новые данные и сохраняем их. В начале таблицы должны появиться новые данные.

Все, с формой мы разобрались. Данные обновляются и добавляются. Такое чувство что чего то не хватает. Правильно, не хватает возможности удалять данные.

Такую возможность мы предоставим пользователю в самой таблице. Предлагаю добавить иконку для удаления в последний столбец и установить на нее соответствующий обработчик.

Чтобы добавить иконку, нужно перейти к настройкам столбца и добавить следующий объект в массив свойства columns:

columns:[...{ template:"{common.trashIcon()}", width:50 }] 

Свойство template задает шаблон для ячейки столбца, а common.trashIcon() вставляет соответствующую иконку. В интерфейсе это выглядит следующим образом:

Шаблон спискаШаблон списка

Теперь давайте зададим поведение по клику на эту иконку. Для таких случаев у таблицы предусмотрено свойство onClick. C его помощью можно установить обработчик на любой элемент таблицы, которому присвоен соответствующий css класс. В нашем случае это "wxi-trash". Выглядит обработчик следующим образом:

onClick:{"wxi-trash":function(e, id){...}}

Теперь перейдем непосредственно к действиям, а именно к удалению данных при клике по иконке. Чтобы удалить данные, у коллекции предусмотрен специальный метод remove(). В качестве параметра нужно передать id элемента, который мы хотим удалить. Теперь обработчик выглядит так:

onClick:{"wxi-trash":function(e, id){data.remove(id);}}

Сейчас все работает совсем по-взрослому. Если подытожить сделанное, то в этом разделе мы рассмотрели как реализуются операции с данными при работы с коллекцией. Хочу напомнить, что здесь мы применили оптимистический подход сохранения. Это повышает скорость работы приложения, так как запрос на сервер отправляется уже после того, как данные обновились во view-компонентах.

Модель данных для пользователей

А теперь давайт вспомним, что в нашем приложении есть вторая вкладка с модулем UsersView, который хранит список пользователей и диаграмму. В интерфейсе это выглядит следующим образом:

Интерфейс вкладки ПользователиИнтерфейс вкладки Пользователи

Для этих двух view-компонентов мы также используем серверные данные. Так как они находятся в пределах одного модуля, коллекция нам не нужна. Давайте пойдем другим путем и настроим загрузку/сохранение данных с помощью соответствующих функций модели:

  • getData()

  • saveData()

Функция getData() будет только загружать данные. Она имеет следующий вид:

export function getData(){return webix.ajax(load/users);}

Функция saveData() будет отправлять запросы для изменения данных на сервер разными методами (POST, PUT, DEL), в зависимости от типа операции. Она будет иметь следующий вид:

export function saveData(operation, data){const url = "save/users"; if(operation == "add"){return webix.ajax().post(url, data);}else if(operation == "update"){return webix.ajax().put(url, data);}else if(operation == "remove"){return webix.ajax().del(url, data);}}

Итак, с функциями модели данных мы определились. Сейчас нужно импортировать их в модуль UsersView при помощи следующей команды:

import { getData, saveData } from "models/users";

После этого необходимо загрузить данные в список при помощи метода parse() и синхронизировать диаграмму со списком ее методом sync(). Все это делаем после инициализации в методе init():

init(view){this.list = view.queryView("list"); //получаем доступ к спискуthis.list.parse(getData()); //загружаем данные в списокconst chart = view.queryView("chart"); //получаем доступ к диаграммеchart.sync(this.list); //синхронизируем диаграмму со списком}

Взаимодействие через методы классов и глобальные события

Помимо взаимодействия через url, WebixJet предлагает и другие подходы для коммуникации между компонентами разных модулей. Давайте рассмотрим способ взаимодействия через методы классов и глобальные события, а заодно применим их к модулям нашего приложения.

Вы наверняка должны знать, что мы можем вызвать любой публичный метод одного класса внутри другого. На основе этой особенности мы и будем настраивать взаимодействие между модулями.

Также, Jet App позволяет вызывать какое-либо событие в одном модуле, а считывать его в любом другом. Звучит хорошо, поэтому давайте разбираться как это все можно применить к нашему приложению, в частности к модулю UsersView.

Сценарий предполагает, что при клике по кнопке Add на тулбаре модуля, в ответ должно появиться всплывающее окно с формой редактирования списка пользователей, куда нужно ввести соответствующие данные. Для начала давайте создадим это окно и научимся его отображать.

Создаем всплывающее окно с формой

Чтобы создать всплывающее окно, нам не нужно придумывать велосипед. Webix все предусмотрел и предоставил в наше распоряжение такой компонент как window. Давайте создадим отдельный модуль WindowView в файле views/window.js, в котором и опишем данный компонент.

Создаем класс WindowView и наследуем его от JetView. В методе config() описываем и возвращаем компонент window:

export default class WindowView extends JetView {config(){    const window = {      view:"window",       position:"center", //позиционируем компонент в центре экрана      close:true, //добавляем иконку для закрытия окна      head:_("EDIT USERS"), //добавляем название в шапке окна      body:{  } //определяем содержимое окна    }return window;}}

В браузере мы получим следующий результат:

Всплывающее окноВсплывающее окно

Теперь надо добавить непосредственно форму для редактирования данных пользователя. Реализуется это в объекте свойства body с помощью компонента form:

body:{view:"form",localId:"form",elements:[/*элементы формы*/]}

В массиве свойства elements определяем нужные нам поля формы:

elements:[{ view:"text", name:"name", label:"Name", required:true },{ view:"text", name:"age", label:"Age", required:true, type:"number" },{ view:"text", name:"country", label:"Country" },{margin:10, cols:[{ view:"button", value:"Save", css:"webix_primary" }]}]

Теперь всплывающее окно с формой будет иметь следующий вид:

Всплывающее окно с формойВсплывающее окно с формой

По умолчанию, окна в Webix создаются скрытыми. Нам же нужно будет отображать окно с формой при клике по кнопке Add new в тулбаре модуля UsersView. Давайте определим метод модуля WindowView, который будет это делать:

showWindow(){this.getRoot().show();}

В теле метода мы используем JetView метод getRoot(), чтобы получить доступ к view компоненту window, который хранится в этом модуле. Далее мы отображаем окно его методом show().

Итак, мы описали всплывающее окно и создали метод для его отображения. Давайте посмотрим, как использовать это в другом модуле.

Настраиваем взаимодействие через методы

У нас есть модуль WindowView с интерфейсом всплывающей формы и методом для ее отображения. Нам нужно импортировать этот модуль в UsersView и создать там экземпляр окна.

В начале файла view/users.js импортируем модуль окна в UsersView:

import WindowView from "views/window";

В JetView методе init() класса UsersView создаем экземпляр окна с формой, а заодно получаем доступ к методу showWindow(), который отвечает за его отображение. Реализуется это следующим образом:

init(view){//создаем окно с формой, которое хранится в модуле WindowViewthis.form = this.ui(WindowView); }

Давайте воспользуемся этим преимуществом и вызовем доступный нам метод showWindow() в качестве обработчика на событие клика по кнопке Add new, которая находится в тулбаре модуля. Делаем мы это с помощью знакомого нам свойства click, которое предусмотрено у кнопок именно для таких случаев:

view:"toolbar",elements:[{ view:"button", value:"Add new", click:() => this.form.showWindow() },]

Теперь, при клике по кнопке Add new, приложение будет отображать всплывающее окно с формой для редактирования данных пользователя. Вот таким нехитрым образом мы настроили взаимодействие между компонентами двух модулей с использованием публичного метода класса.

Настраиваем взаимодействие через глобальные события

Сейчас мы можем вызывать всплывающее окно с формой и вводить туда данные. Давайте передадим значения полей формы в модуль UsersView при клике на кнопку Save. А реализуем мы это с помощью глобальных событий.

Нужно вернуться обратно в модуль WindowView и создать соответствующий обработчик, который будет получать значения формы и отправлять их вместе с глобальным событием:

saveUserHandler(){const data = this.form.getValues();this.app.callEvent("onDataChange", [data]);}

Как мы видим, реализуется это достаточно просто. Для создания и отправки события необходимо вызвать метод this.app.callEvent() в контексте всего приложения (this.app), а также передать название события и объект с данными в качестве параметров.

Чтобы получить объект со значениями полей формы, используем ее метод getValues(). Здесь также стоит упомянуть о том, что при создании формы мы указали свойство localId в значении form. Это и позволит нам получить доступ к форме. В нашем случае нужно будет несколько раз обращаться к ней, поэтому логичнее будет сохранить доступ в переменную, которая будет доступна только в пределах текущего модуля. Сделать это желательно в JetView методе init(), который хранит логику для выполнения после инициализации модуля:

init(){this.form = this.$$("form");}

Теперь форма доступна для всего модуля через переменную this.form.

Нужно учесть тот факт, что для формы заданы 2 правила (поля должны быть заполнены). Давайте проверим их методом validate(), который возвращает true или false, на успех или неудачу:

saveUserHandler(){if(this.form.validate()){    const data = this.form.getValues();    this.app.callEvent("onDataChange", [data]);}}

После всех проделанных действий нужно скрыть окно. Для этого у него предусмотрен метод hide(). В общем итоге, наш обработчик выглядит следующим образом:

saveUserHandler(){if(this.form.validate()){const data = this.form.getValues();this.app.callEvent("onDataChange", [data]);this.form.hide();}}

Давайте установим его на событие клика по кнопке Save. Напомню, что для таких случаев у Webix кнопок предусмотрено специальное свойство click:

{ view:"button", value:"Save", ... , click:() => this.saveUserHandler() }

Теперь, при клике по кнопке Save, модуль WindowView отправит глобальное событие вместе с объектом значений формы. Нам же необходимо поймать отправленное событие и получить этот объект в модуле UsersView, чтобы отправить данные на сервер с помощью функции модели. Этим мы сейчас и займемся.

Так как событие зверь мелкий, нам не нужно использовать винтовки и ружья. Достаточно будет установить специальную ловушку, которая отреагирует на его появление. Лучшим местом для этого будет метод init(), который мы не раз уже упоминали:

init(view){...this.form = this.ui(WindowView);this.on(this.app, "onDataChange", (data) => {...});}

Использованный нами JetView метод this.on() ловит событие onDataChange на уровне всего приложения (this.app), получает объект со значениями формы и выполняет указанные в обработчике действия.

Как мы видим, один модуль отправляет событие, а другой его ловит. Вот так и происходит взаимодействие между модулями через глобальные события.

Работа с данными через функцию модели

Мы создали функции модели данных, импортировали их и настроили взаимодействие между модулями WindowView и UsersView с помощью методов и глобальных событий. Теперь давайте посмотрим на то, как на самом деле происходит обмен данными между компонентами интерфейса и сервером через нашу функцию сохранения.

Вначале мы говорили про оптимистический подход в сохранении данных. Логично будет предположить, что существует и пессимистический подход, ибо одно без другого существовать не может.

Суть пессимистического подхода заключается в том, что для изменения данных мы должны сначала сделать запрос на сервер, дождаться ответа и только в случае успеха обновить полученные данные во view-компонентах. На самом деле, это наиболее вероятный путь сохранения данных, который ближе всего к реальному серверу, у которого может быть своя логика, уровни доступа и прочие нюансы.

Давайте посмотрим как реализуются операции с данными в модуле UsersView. И начнем мы с добавления новых пользователей.

Добавление данные новых пользователей

Итак, наш пользователь вызвал форму, ввел необходимые данные и кликнул по кнопке Save. С помощью глобального события данные пришли с модуля WindowView в модуль UsersView и доступны в обработчике, который поймал событие. Теперь нам нужно отправить данные на сервер. Как это сделать? Давайте воспользуемся функцией saveData(), которую мы создали и импортировали из модели. В качестве аргументов нужно передать название операции и сам объект с данными:

this.on(this.app, "onDataChange", (data) => {saveData("add", data).then((res) => this.list.add(res.data, 0));});

Сейчас метод модели может отправить данные на сервер и будет терпеливо ждать ответа. Если данные успешно сохранились на сервере, метод вернет промис, который содержит добавленный объект с серверным id. Этот объект мы добавляем в list, вызывая у него метод add(). Вторым аргументом мы передаем индекс позиции, в которую нужно добавить данные.

Проверяем результат и видим, что все работает, а данные успешно сохраняются на сервере и добавляются в начало списка. Но этого для нас мало. Давайте реализуем возможность редактировать и удалять данные.

Обновляем данные пользователей

Для этого нам нужно перейти к настройкам компонента list и внести некоторые коррективы. А менять мы будем шаблон отображения списка, который задается через свойство template. Давайте зададим 2 иконки, для редактирования и удаления, на которые в дальнейшем установим соответствующие обработчики. В коде это выглядит следующим образом:

{  view:"list",  template:"#name#, #age#, #country#   <span class='remove_list_item_btn webix_icon wxi-trash'></span>   <span class='edit_list_item_btn webix_icon wxi-pencil'></span>"}

В браузере мы получим следующий результат:

Иконки Редактировать и Удалить в правой части спискаИконки Редактировать и Удалить в правой части списка

Итак, интерфейс мы подготовили. Сначала реализуем обновление данных.

Замысел такой, что при клике на иконку редактирования должна появится форма с уже заполненными полями. Пользователь сохраняет данные и они меняются в списке. Давайте это реализуем.

Для начала создадим и установим обработчик, который будет показывать окно с формой. У списка, также как и у таблицы, предусмотрено свойство onClick, с помощью которого можно установить обработчик на любой элемент с указанным css классом. Иконке редактирования мы присвоили класс edit_list_item_btn с которым и будем работать:

onClick:{edit_list_item_btn:(e,id) => {this.form.showWindow();}}

Сейчас, при клике по иконке редактирования, приложение отобразит всплывающее окно с формой, поля которой будут пустыми. Нам же нужно заполнить их данными из выбранного элемента. Давайте получим эти данные и передадим их в качестве аргумента методу для отображения формы.

Чтобы получить объект с интересующими нас данными, воспользуемся методом списка getItem(). В качестве аргумента передадим id, который получили при клике по элементу:

onClick:{edit_list_item_btn:(e,id) => {const data = this.list.getItem(id);this.form.showWindow(data);}}

Но и это еще не все. Поля формы по прежнему остаются пустыми, так как метод showWindow() пока ничего не делает с нашим параметром. Чтобы это изменить, нужно вернуться в модуль WindowView и предусмотреть наличие объекта с данными в качестве параметра. Если он передан а на добавление он не передается необходимо установить данные в поля формы с помощью метода формы setValues():

showWindow(data){this.getRoot().show();if(data){this.form.setValues(data);}}

Теперь, при клике по иконке редактирования напротив любого элемента списка, мы увидим форму, поля которой заполнены соответствующими данными. Нужно также упомянуть о том, что вместе с данными для полей, мы передаем и id, под которым эти данные хранятся как в списке, так и на сервере.

Давайте отправим эти данные на сервер при клике по кнопке Save, которая передаст их в функцию нашей модели. Здесь нужно установить дополнительную проверку, которая будет определять дальнейшие действия. Если id существует, метод обновляет данные, если нет добавляет новые. На практике это выглядит следующим образом:

this.on(this.app, "onDataChange", (data) => {  if(data.id){    saveData("update", data).then(      (res) => this.list.updateItem(res.data.id, res.data)    );  }else{    saveData("add", data).then((res) => this.list.add(res.data, 0));  }});

Обновление данных работает по аналогии с добавлением. Метод модели отправляет запрос на сервер и ждет ответ. Если все проходит гладко, метод возвращает промис объект с измененными данными, а мы обновляем их в компоненте с помощью такого метода списка как updateItem(), которому передаем id и объект с данными.

Удаляем данные пользователей

Теперь давайте займемся удалением данных. Как вы помните, в интерфейсе списка мы создали специальную иконку и присвоили ей класс remove_list_item_btn. Сейчас нужно установить обработчик, который будет отправлять запрос на удаление данных. Выглядит это следующим образом:

onClick:{edit_list_item_btn:(e,id) => {  },remove_list_item_btn:(e,id) => {const data = this.list.getItem(id);saveData("remove", data).then((res) => this.list.remove(res.data.id));}}

Здесь мы получаем объект с данными и отправляем запрос на удаление через ту же функцию модели saveData(). Если сервер одобрит наш запрос, удаляем данные из списка его методом remove(), которому передаем id нужного элемента.

Если подытожить, то в это разделе мы рассмотрели как реализуются операции с данными (добавление, обновление и удаление) при работе с сервером через функцию модели. Хочу напомнить, что это пессимистический подход сохранения, где компонент вызывает функцию saveData(), которая отправляет запрос на сервер и ждет ответа. Если ответ приходит положительный, только тогда происходят изменения в интерфейсе.

Добавляем поиск и сортировку

Основной функционал мы реализовали. Но если приглядеться внимательно, то можно заметить, что в тулбаре модуля у нас остались без внимания строка поиска и кнопки для сортировки данных. Давайте сделаем их функциональными.

Контролы сортировки и поискаКонтролы сортировки и поиска

При вводе текста в поле поиска, данные списка должны фильтроваться. Для реализации этого функционала нам понадобится такой метод списка как filter(). Он сравнивает значения поля поиска с данными списка и делает так, чтобы подходящие данные остались в списке, а лишние исчезли.

Сортировка реализуется с помощью такого метода списка как sort(). Он будет сортировать данные списка по имени пользователя в указанном порядке (по возрастанию или убыванию).

Заключение

С помощью возможностей фреймворка Jet, а также методов и компонентов библиотеки Webix, мы создали и оживили полноценное одностраничное приложение. Интерфейс разделен на view модули, которые хранятся в отдельной директории sources/views, а логика работы с данными отделена от компонентов интерфейса и хранится в директории sources/models.

С кодом готового приложения можно ознакомиться тут.

В статье мы подробно разобрались с тем, как налаживать коммуникацию между view-компонентами при помощи URL, глобальных событий и публичных методов класса. Помимо этого, сделали небольшой экскурс в философию сохранения данных.

Помимо этого, WebixJet имеет огромный набор инструментов и конфигураций, которые нереально охватить в одной статье и реализовать в одном приложении. Но есть факты, с которыми трудно спорить - использование библиотеки Webix и фреймворка Jet значительно упростят процесс разработки приложения любой сложности.

Подробнее..

Webix Datatable. От простой таблицы к сложному приложению

14.06.2021 10:08:37 | Автор: admin

Эта статья будет интересна для тех, кто привык решать сложные задачи простыми методами. Работа с большими данными, на первый взгляд, может показаться сложной задачей. Но если вы владеете специальными инструментами, то организация и отображение больших наборов данных покажется вам не более чем забавным развлечением. Сегодня мы поговорим об одном из самых неординарных инструментов для работы с данными, который предоставляет нам команда Webix. Речь пойдет о таком простом и одновременно сложном виджете библиотеки Webix UI как DataTable. Давайте разбираться в чем его сила.

Библиотека Webix и виджет DataTable

Webix UI это современная и производительная JavaScript библиотека, которая позволяет создавать дружественный интерфейс на основе собственных ui компонентов. Диапазон ее возможностей представлен виджетами различной сложности, начиная обычной кнопкой и заканчивая комплексными виджетами. Помимо самих компонентов, библиотека предоставляет множество дополнительных инструментов для работы с ними. Здесь стоит упомянуть механизм обработки событий, методы для работы с данными, взаимодействие с сервером, темы для стилизации и многое другое. Обо всем этом и не только, вы можете узнать в документации.

Виджет DataTable - это один из самых функциональных компонентов библиотеки Webix. С его помощью вы можете отображать данные в виде таблиц и очень гибко их настраивать. Этот мощный и одновременно простой в использовании инструмент со стильным дизайном поддерживает различные форматы данных (XML, JSON, CSV, JSArray, HTML tables) и довольно быстро работает с большими объемами информации. Секрет его скорости заключается в так называемом "ленивом подходе отрисовке данных". Это не значит, что ему лень отрисовывать данные. Хотя, без сомнений, крупица правды в этом есть. Суть же подхода заключается в том, что даже если вы загрузите в таблицу 1 000 000 рядов, выджет отрисует только выдимые в окне браузера элементы. Стоит также сказать, что среди своих конкурентов виджет удерживает лидирующее место по скорости отрисовки.

Помимо прочего, DataTable обладает широким арсеналом API для фильтрации, сортировки, пагинации, копирования, изменения размеров, стилизации и многих других полезных фишек. Все это делает DataTable незаменимым помощником при работе с большими данными.

Можно много рассуждать об удобстве работы с таблицами Webix и их обширных возможностях, но я предлагаю оставить патетику ораторам и попробовать разобраться во всем на практике. Давайте создадим небольшое приложение, которое будет отображать таблицу данных об аренде автомобилей. На наглядном примере гораздо проще увидеть все преимущества работы с этим мощным инструментом. Успешное применение этого виджета также описано в статье "Создаем Booking приложение с Webix UI".

С готовым приложением можно ознакомиться тут.

Базовые приготовления

Для того чтобы использовать возможности библиотеки Webix в нашем приложении, необходимо подключить ее в главном файле index.html. Здесь стоит упомянуть о том, что существует 2 версии библиотеки: базовая и Pro-версия. Базовая версия бесплатная и предоставляет ограниченный набор возможностей, по сравнению с Pro-версией. Мы же воспользуемся тестовой лицензией расширенной Pro-версии, чтобы по максимуму реализовать возможности виджета DataTable. Необходимые файлы доступны через CDN по следующим ссылкам:

<script type="text/javascript" src="http://personeltest.ru/away/cdn.webix.com/site/webix.js"></script><link rel="stylesheet" type="text/css" href="http://personeltest.ru/away/cdn.webix.com/site/webix.css">

Нам остается только включить их в файл index.html нашего приложения. Теперь он будет так:

<!DOCTYPE html><html>  <head>    <title>Webix Booking</title>    <meta charset="utf-8">    <!--Webix sources -->    <script type="text/javascript" src="http://personeltest.ru/away/cdn.webix.com/site/webix.js"></script>    <link rel="stylesheet" type="text/css" href="http://personeltest.ru/away/cdn.webix.com/site/webix.css">  </head>  <body>    <script type="text/javascript">      //...    </script>  </body></html>

Внутри кода мы добавим теги <script>...</script>, где и будем собирать наше приложение.

Инициализация

Все основные действия будут разворачиваться внутри конструктора webix.ui(). Нам же нужно удостовериться в том, что код начнет выполняться после полной загрузки HTML страницы. Для этого необходимо обернуть конструктор в webix.ready(function(){}). Выглядит это следующим образом:

webix.ready(function(){  webix.ui({    /*код приложения*/  });});

Мы создали базовый index.html файл и подключили необходимые инструменты. Теперь самое время перейти непосредственно к настройке самого компонента DataTable.

Сила в простоте

Если вы минималист и не горите желанием разбираться во всех тонкостях настройки виджета, то для вас предусмотрен самый ленивый набор конфигураций, который позволяет отобразить нужную информацию в виде таблицы. В файле datatable.js нужно создать конструктор виджета и прописать три следующих строчки кода:

const datatable = {  view:"datatable",  autoConfig:true,  url:"./data/data.json"}

Сам компонент DataTable объявляется с помощью выражения view:"datatable". Через свойство url мы задаем путь, по которому виджет загружает данные. Стоит уточнить, что по умолчанию виджет ожидает получить данные в формате JSON. Если данные приходят в другом формате (xml, jsarray или csv), нужно указать его через свойство datatype. В случае, когда данные находятся на клиенте в виде массива, их можно передать компоненту через свойство data или метод parse().

Особое внимание стоит уделить свойству autoConfig, которое предназначено для максимально ленивых программистов. С его помощью данные автоматически распределяются по соответствующим столбцам и рядам таблицы. Удобнее только вообще ничего не делать.

Теперь давайте отобразим компонент в браузере и посмотрим, что же у нас получилось. Для удобства, мы сохраним конструктор виджета в переменную datatable, которую будем использовать для сборки в файле index.html.

С помощью следующего кода мы подключаем файл с компонентом DataTable в файле index.html:

<!--App sources --><script src="js/datatable.js" type="text/javascript" charset="utf-8"></script>

В конструкторе приложения мы указываем переменную, которая хранит настройки виджета:

<script type="text/javascript"> webix.ready(function(){  webix.ui( datatable ); });</script>

В браузере мы увидим следующий результат:

AвтонастройкаAвтонастройка

Вот так, с помощью 3 строчек кода, можно отобразить таблицу с соответствующими данными. При таком раскладе, структура таблицы будет напрямую зависеть от структуры загружаемых данных. На самом деле, автонастройка мало используется в реальных проектах. Гораздо практичнее настраивать столбцы таблицы вручную. В следующем разделе я расскажу вам как это сделать.

Тонкости настроек таблицы

Если автонастройка кажется вам слишком уж тривиальным вариантом, тогда давайте усложним задачу и зададим индивидуальные настройки для каждого столбца в отдельности. Сделать это можно в массиве свойства columns:[ ]. Для каждого столбца нужно задать объект с соответствующими настройками. Стоить учитывать то, что порядок отображения столбцов в таблице напрямую зависит от порядка объектов с настройками в массиве.

В первую очередь, для каждого столбца нужно задать уникальный id. Он должен соответствовать ключу, под которым данные хранятся в объекте (хранилище). Затем можно указать название для шапки столбца с помощью свойства header.

После этого можно задать ширину каждого столбца. Для этого предусмотрены такие свойства как width, minWidth, maxWidth и fillspace. Если мы хотим, чтобы ширина таблицы подстраивалась под ширину контейнера или под доступное ей место, нужно использовать свойство fillspace по крайней мере для одного столбца. Теперь настройки столбцов будут выглядеть так:

{  view:"datatable",  columns:[    { id:"rank", header:"Rank", width:45 },    //...    { id:"vin_code", header:"VIN", minWidth:50, width:180, maxWidth:300 },    //...    { id:"address", header:"Address", minWidth:200, fillspace:true },    //...  ],  url:"./data/data.json"}

В браузере мы получим следующий результат:

Индивидуальные настройки столбцовИндивидуальные настройки столбцов

В этом примере видно, что столбцы размещены в установленном нами порядке и с заданной шириной, а их названия соответствуют значениям свойства header.

Помимо индивидуальных настроек ширины для каждого столбца, вы можете задать глобальные настройки размеров для всех столбцов в конструкторе компонента. Для этого предусмотрено свойство autowidth, которое подстраивает ширину виджета к суммарной ширине всех столбцов таблицы, а также свойство columnWidth, которое задает фиксированную ширину для всех столбцов. По умолчанию стандартная ширина столбца равна 100px.

Для полноты картины давайте предоставим пользователям возможность изменять размеры столбцов. Для этого необходимо задать свойству resizeColumn значение true, а также установить границы для рядов, столбцов и их хедеров с помощью свойства css:"webix_data_border webix_header_border".

Так как у таблицы есть много столбцов, необходимо предусмотреть горизонтальную и вертикальную прокрутку. Сделать это можно с помощью свойства scroll, которое изначально установлено в значении false. Нам же нужно задать ему значение xy.

Теперь мы можем изменять размер столбцов, перетаскивая их правую границу в нужном направлении, а также скролить таблицу по горизонтали и вертикали.

Настройка содержимого ячеек

Работа с шаблонами

По умолчанию, ячейки таблицы заполняются данными, ключ которых задан в качестве id в настройках столбца. Но виджет позволяет нам управлять их отображением. С помощью свойства template мы можем задать необходимый шаблон, по которому данные будут отображаться в ячейке. Значение можно указать как в виде строки, так и в виде функции. Чтобы использовать в строковом шаблоне входящие данные, их ключ нужно указать как #data_key#. У нас есть несколько столбцов, для которых необходимо задать шаблоны.

Начнем мы пожалуй с первого столбца, в ячейках которого будут размещаться иконки в виде звездочек. У вас может возникнуть резонный вопрос, зачем нам такой столбец со звездочками? А мы сделаем так, чтобы наш потенциальный пользователь смог отмечать понравившиеся ему варианты при клике по звездочке. Давайте это реализуем с помощью темплейт-функции. Код будет выглядеть следующим так:

{  view:"datatable",  id:"car_rental_table",  //...  columns:[    { id:"stared", header:"",     template:function(obj){       return `<span class='webix_icon star mdi mdi-"+(obj.star ? "star" : "star-outline") + "'></span>`;     }, ...,    },     //...  ]}

Свойству template мы присваиваем функцию, которая возвращает элемент span с определенными классами. Классы star и star-outline мы будем менять динамически при клике по иконке. Давайте создадим функцию, которая будет менять классы для иконок этого столбца:

function selectStar(id){  const table = $$("car_rental_table");  const item = table.getItem(id);  const star = item.star?0:1;  item.star = star;}

В качестве аргумента функция принимает id выбранного ряда. Через метод $$("car_rental_table") мы получаем доступ к виджету по его id. С помощью метода таблицы getItem(), который принимает id элемента в качестве параметра, мы получаем объект данных ряда. Затем проверяем наличие ключа star и присваиваем ему значение 0 (если он существует) либо 1 (если его нет).

Чтобы переключение начало работать, необходимо установит эту функцию в качестве обработчика на событие клика по иконке со звездочкой. Для того чтобы установить обработчик на любой элемент таблицы с определенным css классом, у таблицы предусмотрено специальное свойство onClick:

//...url:"./data/data.json",onClick:{  "star":(e,id) => selectStar(id)},//...

Теперь мы можем отмечать нужные нам ряды при клике по звездочке в первом столбце таблицы. В браузере это выглядит следующим образом:

Шаблон для столбца со звездочкамиШаблон для столбца со звездочками

На очереди у нас столбец с названием Available. В его ячейках хранятся значения true и false, которые обозначают доступность автомобиля в текущий момент времени. Давайте зададим шаблон, который будет менять входящее значения ячейки на соответствующий текст Yes или No.

Для этого мы создадим специальную функцию, которая и будет менять нужные нам значения. Выглядит она так:

function customCheckbox(obj, common, value){  if(value){    return "<span class='webix_table_checkbox checked'> YES </span>";  }else{    return "<span class='webix_table_checkbox notchecked'> NO </span>";  }}

Теперь нужно установить эту функцию в качестве шаблона для столбца Available:

columns:[  //...  { id:"active", header:"Available", template:customCheckbox, ...,},]

В браузере результат будет следующим:

Шаблон для столбца "Available"Шаблон для столбца "Available"

Выглядит уже неплохо. Так как в первых трех столбцах у нас находятся данные, которые постоянно должны быть на виду, давайте их зафиксируем в левой части таблицы. Для этого необходимо добавить свойство leftSplit в конструктор виджета и задать ему количество желаемых столбцов (в нашем случае 3). Теперь эти столбцы будут отображаться поверх других элементов при вертикальном скролле.

А сейчас давайте займемся столбцом под названием Color. Его значения отображаются в виде HEX кодов, которые обозначают цвет автомобиля. Для обычного пользователя такие значения ничего не говорят. Давайте добавим визуальное представление цвета для каждой ячейки. И сделаем мы это также с помощью шаблона. Код будет выглядеть так:

columns:[  //...  { id:"color", header:"Color", template:`<span style="background-color:#color#; border-radius:4px; padding-right:10px;">&nbsp</span> #color#`},  //...]

Здесь мы используем строковый шаблон, в котором задаем фон неразрывного пробела (&nbsp) с помощью входящего HEX кода.

В браузере результат будет следующим:

Шаблон для столбца "Color"Шаблон для столбца "Color"

Работа с коллекциями

В некоторых случаях данные приходят и отображаются в виде чисел, которые указывают на id определенных элементов в других коллекциях данных. Но мы не можем отобразить их в таком виде, потому что для пользователя это мало информативно. В таких случаях, виджет позволяет заменить числа на соответствующие названия из других коллекций.

Для примера, давайте рассмотрим столбец с названием "Сar make", в котором должны отображаться марки автомобилей. Данные для его ячеек хранятся в виде чисел от 1 до 24 под ключем "car_make":

//data.json[  { "id":1, "rank":1, "car_make":22, ..., "country":1, "company":1, ..., },  { "id":2, "rank":2, "car_make":10, ..., "country":2, "company":3, ..., },  { "id":3, "rank":3, "car_make":16, ..., "country":1, "company":2, ..., },  //...]

Чтобы заменить числа в столбце на соответствующие названия автопроизводителей, нам нужно получить доступ к коллекции, в которой хранятся эти названия. Выглядит коллекция следующим образом:

//car_make.json[  { "id":22, "value":"Toyota" }, ...,  { "id":10, "value":"GMC" }, ...,  { "id":16, "value":"Mazda" }, ...,  //...]

В настройки столбца необходимо добавить свойство collection и присвоить ему путь к нужному объекту (коллекции):

columns:[  //...  { id:"car_make", header:"Car make", collection:"./data/car_make.json", ...,},  //...]

Вот таким образом, вместо числовых значений, в ячейках столбца Car make будут отображаться названия автопроизводителей. По такому же принципу мы заменяем значения для столбцов Company, Country и Card.

В браузере результат будет следующим:

Коллекции для столбцовКоллекции для столбцов

Работа с форматами данных

При работе с данными, не редко возникает потребность изменить формат их отображения. На этот случай, у таблицы имеется специальное свойство format. С его помощью мы можем задать желаемый формат отображения чисел и дат. Для этого предусмотрены специальные методы. Давайте зададим формат отображения для столбцов с датами и ценой заказа:

columns:[  //...  { id:"date", header:"Date", format:webix.i18n.longDateFormatStr, ..., },  { id:"price", header:"Price", format:webix.i18n.priceFormat, ..., },  //...]

Данные о датах приходят в виде строк 05/26/2021. Нам же нужно получить дату в формате 26 May 2021. Метод webix.i18n.longDateFormatStr, который мы применили в настройках столбца Date, должен получать объект Date и возвращать строку в нужном формате. Но сейчас он получает только строку типа 05/26/2021, поэтому результат может быть неожиданным. Давайте изменим входящие данные и преобразуем строки в соответствующие Date объекты.

Для этого у таблицы предусмотрено свойство scheme. В объекте этого свойства мы меняем строковое значение даты на соответствующий объект с помощью метода webix.i18n.dateFormatDate. Код будет выглядеть следующим образом:

{  view:"datatable",  //...  scheme:{    $init:function(obj){      obj.date = webix.i18n.dateFormatDate(obj.date)    }  },  columns:[...]}

С форматированием даты мы разобрались. Теперь давайте посмотрим как изменить цену в столбце "Price". А здесь все еще проще. Метод webix.i18n.priceFormat получает число (например 199) и возвращает строку со знаком доллара в начале: $199. Вот и вся хитрость.

В браузере результат будет следующим:

Форматирование даты и ценыФорматирование даты и цены

Узнать больше о возможностях форматирования данных библиотеки Webix можно в этой статье.

Сортировка данных

Одной из самых востребованных функций при работе с таблицами является сортировка данных. В обычных условиях нужно создавать обработчики, ловить события и делать массу мудреных манипуляций.

В виджете DataTable эта задача решается при помощи всего лишь одного свойства sort. Нам нужно только включить его в настройки необходимого столбца и задать правильный режим. Так как мы будем сортировать столбцы с датами, числами и строками, нам понадобятся следующие типы сортировки:

  • "int" - сравнивает числовые значения

  • "date" - сравнивает даты

  • "string" - сравнивает строковые значения в том виде, в котором они загружаются

  • "text"- сравнивает текст элемента, который отображается в ячейке (включая темплейт)

    columns:[  { id:"car_model", header:"Model", width:120, ..., sort:"string", }, ...,  { id:"car_year", header:"Year", width:85, ..., sort:"int" }, ...,{ id:"country", header:"Country", width:140, ..., sort:"text" }, ...,{ id:"date", header:"Date", width:150, ..., sort:"date" }, ...,]
    

Теперь данные будут сортироваться при клике по хедеру определенного столбца. Более того, мы можем установить режим, который позволяет сортировать данные по нескольким критериям одновременно. Для этого нужно задать свойству sort значение "multi" в конструкторе виджета. Чтобы отсортировать данные по нескольким условиям, нужно нажать клавишу Ctrl/Command и кликнуть по хедерам нескольких столбцов.

Если вы хотите применить другое поведение сортировки (отличное от встроенного), вы можете задать нужную логику в отдельной функции и присвоить ее атрибуту sort.

Если же нужно отсортировать данные таблицы при клике по какой либо внешней кнопке, вы можете воспользоваться методом таблицы sort(). Подробнее о сортировке данных вы можете узнать в этой статье.

Фильтрация данных

Сложно представить себе полноценную таблицу без возможности фильтровать в ней данные. И такая возможность предусмотрена в таблице Webix. С помощью свойства content, мы можем добавить один из нескольких встроенных фильтров или же задать собственные условия фильтрации. Это позволит нам фильтровать данные на клиенте или сервере по одному или нескольким критериям.

В зависимости от типа данных в столбцах, мы будем применять для них разные типы фильтров. Давайте добавим фильтр selectFilter с выпадающим списком опций в хедер столбца с названием Company. Для этого же столбца мы ранее указали свойство collection, которое заменяет числовые значения соответствующими названиями компаний. Фильтр сам подтянет данные этой коллекции и сформирует из них список опций. В настройках столбца это выглядит так:

columns:[  //...  {    id:"company",     header:["Company",{content:"selectFilter"}],     collection:"./data/company.json", ...,  }, ...,]

В браузере результат будет следующим:

Фильтр selectFilterФильтр selectFilter

Для столбца с названием Car make мы добавим фильтр textFilter. Он представляет собой обычное поле для ввода данных. Фильтр будет сравнивать введенные значения с данными столбца. Хочу напомнить, что данные приходят сюда в виде чисел, которые преобразуются в соответствующие названия моделей авто. Дело в том, что фильтр будет сравнивать введенные значения именно с числами, а это нас не совсем устраивает. Давайте изменим поведение фильтра по умолчанию и сделаем так, чтобы введенные значения сравнивались с данными из коллекции. Для этого мы добавим к фильтру специальную функцию для сравнения:

columns:[  //...  { id:"car_make", header:["Car make", {    content:"textFilter", placeholder:"Type car make",    compare:function(item, value, data){       const colValue = cars_make_data.getItem(item).value;      const toFilter = colValue.toLowerCase();      value = value.toString().toLowerCase();      return toFilter.indexOf(value) !== -1;    } }], collection:cars_make_data, ...,  },  //...]

Такой же фильтр мы добавим следующему столбцу с названием Model. Его данные приходят в виде строк, так что нам не придется устанавливать дополнительные условия:

columns:[  //...  { id:"car_model", header:["Model", {content:"textFilter", placeholder:"Type model"}, ...,],  //...]

В браузере результат будет следующим:

Фильтр textFilterФильтр textFilter

Теперь давайте перейдем к столбцу с названием Year, в ячейках которого отображается год производства автомобилей. Здесь мы добавим такой фильтр как excelFilter. Особенность его заключается в том, что мы можем вводить данные в поле ввода, задавать дополнительные условия и отмечать нежелательные элементы в специальных чекбоксах. Настройки столбца будут выглядеть так:

columns:[  //...  { id:"car_year", header:[{text:"Year", content:"excelFilter", mode:"number"}], ...,},  //...]

С помощью свойства mode мы указываем, что будем сравнивать числа. Вместо поля ввода, возле хедера появится специальная иконка, при клике по которой откроется окно для ввода условий. В браузере результат будет следующим:

Фильтр excelFilterФильтр excelFilter

Среди наших столбцов есть и такой, в котором отображаются доступные даты аренды автомобилей. Для работы с датами у виджета есть специальный фильтр datepickerFilter. Он позволяет выбрать необходимую дату в компактном всплывающем календаре. Настройки столбца будут выглядеть так:

columns:[  //...  { id:"date", header:["Date", {content:"datepickerFilter"}], ..., },  //...]

В браузере результат будет следующим:

Фильтр datepickerFilterФильтр datepickerFilter

Вот таким образом мы можем добавлять нужные нам фильтры для поиска данных в столбцах. С полным списком фильтров вы можете ознакомиться в этой статье документации.

Редактирование данных

Функционал виджета позволяет редактировать данные непосредственно в ячейках таблицы. Чтобы активировать эту опцию, необходимо задать свойству editable значение true в конструкторе таблицы. Также можно определить действие, по которому будет открываться редактор ячейки. По умолчанию, редактор открывается при клике по ячейке. Можно также определить открытие по двойному клику (dblclick) или указать собственное действие (custom).

Когда режим редактирования активирован, в настройках столбца нужно задать необходимый тип редактора. В зависимости от типа данных в столбце, мы будем применять соответствующий редактор.

А начнем мы из самого часто используемого редактора под названием text. Он превращает ячейку в обычное поле для ввода, куда можно ввести нужные нам данные. Давайте добавим его для нескольких столбцов нашей таблицы. Делается это очень просто: в настройках столбца нужно всего лишь указать свойство editor и присвоить ему соответствующий тип редактора. Настройки столбцов будут выглядеть так:

{  view:"datatable",  //...  editable:true,  editaction:"dblclick",  columns:[    { id:"rank", header:"Rank", editor:"text", ..., },    { id:"car_model", header:"Model", editor:"text", ..., },    { id:"manager", header:"Manager", editor:"text", ..., },    //...  ],  //...}

При двойном клике по любой из ячеек этих столбцов, в браузере мы увидим следующий результат:

Редактор "text"Редактор "text"

Если в ячейке будет находиться большой текст, то редактировать его в маленьком поле будет не очень удобно. Для таких случаев предусмотрен редактор popup. Он позволяет редактировать данные в специальном всплывающем окне. По умолчанию ширина и высота окна равны 250px и 50px соответственно. Давайте добавим этот редактор в настройки столбца Address:

columns:[  { id:"address", header:"Address", editor:"popup", ...,},  //...],//...

При двойном клике по любой ячейке столбца Address, в браузере мы увидим следующий результат:

Редактор "popup"Редактор "popup"

Теперь перейдем к столбцу c названием Available. Как вы помните, для него мы задали шаблон, который превращает значения true и false в соответствующие строки YES и NO. Давайте сделаем так, чтобы пользователь смог переключаться между этими значениями. Для этого мы используем специальный редактор inline-checkbox. Он позволяет менять значения в ячейке при клике по ней. Но для работы этого редактора также необходимо задать свойству checkboxRefresh значение true. Это свойство обновляет данные, полученные из checkbox-редакторов в таблице. Настройки столбца будут выглядеть так:

{  //...  checkboxRefresh:true  columns:[    //...    { id:"active", header:"Available", editor:"inline-checkbox", template:customCheckbox, ..., },  //...  ],  //...}

При клике по любой ячейке этого столбца, его значение изменится на противоположное.

А на очереди у нас столбцы, данные для которых подтягиваются из коллекций. Для них лучше всего подойдет редактор combo. Он позволяет выбирать нужную опцию из выпадающего списка. Список формируется автоматически исходя из данных коллекции столбца. Настройки столбцов будут выглядеть так:

columns:[  { id:"company", header:"Company", editor:"combo",    collection:"./data/company.json", ..., },  { id:"car_make", header:"Car make", editor:"combo",    collection:cars_make_data, ..., },  { id:"country", header:"Country", editor:"combo",   collection:"./data/country.json", ..., },  //...],//...

При двойном клике по ячейкам этих столбцов, в браузере мы увидим следующий результат:

Редактор "combo"Редактор "combo"

Особого внимания заслуживает столбец под названием Color. Хочу напомнить, что входящие данные представляют собой HEX коды различных цветов. У таблицы Webix есть специальный редактор, который позволяет выбрать необходимый цвет в специальном всплывающем окне, а его код отобразится в ячейке столбца. Речь идет о таком редакторе как color. Настройки столбца будут выглядеть так:

columns:[  { id:"color", header:"Color", editor:"color", template:..., },  //...], //...

При двойном клике по любой ячейке столбца Color, в браузере мы увидим следующий результат:

Редактор "color"Редактор "color"

Теперь нам осталось разобраться с редактированием данных в столбце Date. Для работы с датами у таблицы предусмотрен специальный редактор date. Как и в случае с фильтром, возле ячейки появится компактный календарь, где вы сможете выбрать нужную дату, которая сразу отобразится в редактируемой ячейке. Настройки столбца будут выглядеть так:

columns:[  {     id:"date", header:"Date", editor:"date",     format:webix.i18n.longDateFormatStr, ...,   },  //...], //...

При двойном клике по любой ячейке столбца Date, в браузере мы увидим следующий результат:

Редактор "date"Редактор "date"

Более подробно о редактировании таблицы можно узнать в соответствующей статье документации. А мы двигаемся дальше.

Валидация

После редактирования данных, они должны сохраняться на сервере. Здесь хорошим тоном будет проверить измененную информацию перед тем как ее сохранить. Как это сделать спросите вы? Да все донельзя просто. У библиотеки Webix есть специальные правила, которые можно применить для каждого редактируемого поля. В зависимости от типа данных, мы будем применять разные правила.

У нас есть несколько столбцов с числовыми значениями, для которых мы установим правило webix.rules.isNumber. Таблица будет проверять, является ли значение в текстовом поле числом.

Также у нас есть столбец, который содержит адреса электронной почты. Его значения мы будем проверять правилом webix.rules.isEmai.

Для столбца с ценами необходимо задать собственные условия проверки. Давайте сделаем так, чтобы пользователь мог ввести только данные в диапазоне от 20 до 500 долларов. Для этого мы создадим специальную функцию:

function(obj){ return (obj>20 && obj<500) }

Все остальные столбцы мы будем проверять правилом webix.rules.isNotEmpty. Это значит, что в любом случае они должны быть заполнены.

Чтобы применить все эти правила, у таблицы есть специальное свойство rules. Внутри объекта этого свойства необходимо указать id нужных столбцов и присвоить им соответствующие правила. Выглядит это так:

column:[...],rules:{  rank:webix.rules.isNumber,  company:webix.rules.isNotEmpty,  email:webix.rules.isEmail,  price:function(obj){ return(obj>20 && obj<500) },  // правила для других столбцов}

По умолчанию, валидация начнется после закрытия редактора. Если введенные значения не соответствуют правилам, то целый ряд подсветится красным, а в правом верхнем углу невалидной ячейки появится красная метка:

ВалидацияВалидация

Хедеры и футеры

Если вы работаете с большими данными, которые разделяются на множество столбцов, часто возникает необходимость объединить их названия в определенные категории. Такой подход помогает структурировать таблицу и упрощает поиск нужной информации. Виджет DataTable позволяет объединять хедеры с помощью свойств colspan и rowspan, которые немного похожи на настройки обычной HTML таблицы. Для примера, давайте посмотрим как объединить столбцы Price, Card и IBAN в категорию Payment information. Для этого нужно немного изменить свойство header вышеуказанных столбцов:

column:[  //...  { id:"price", header:[{text:"Payment information", colspan:3}, "Price"], ..., },  { id:"credit_card", header:["","Card"], ..., },  { id:"iban", header:["","IBAN"], ..., },  //...]

В браузере мы получим следующий результат:

Объединяем хедерыОбъединяем хедеры

Если хедеры подключены по умолчанию, то футеры нужно активировать отдельно. Для этого необходимо задать свойству footer значение true в конструкторе виджета. Давайте определим название футера для первого столбца и объединим его с футером второго столбца при помощи свойства colspan. А в футере столбца Available, где хранятся данные о доступных автомобилях, мы будем подсчитывать и отображать активные варианты. Настройки столбцов будут выглядеть так:

column:[  //...  { id:"stared", header:[...], ..., footer:{ text:"Available:", colspan:2 } },  //...  { id:"active", header:[...], ..., footer:{content:"summColumn"}, ..., },//...]

Элемент, заданный как {content:"summColumn"} , будет подсчитывать все значения равные true и отобразит их количество в футере. Все изменения в ячейках столбца Available незамедлительно отобразятся в его футере. В браузере мы получим следующий результат:

ФутерыФутеры

Управление видимостью столбцов

Если ваша таблица состоит из множества столбцов, то было бы очень удобно спрятать ненужные в данный момент варианты. Разработчики Webix позаботились о решении этого вопроса и реализовали такую опцию как headerMenu. С ее помощью мы можем управлять видимостью столбцов через специальный интерфейс. По желанию, его можно настроить по своему усмотрению.

Давайте добавим иконку headerMenu в качестве хедера для первых двух столбцов и настроим всплывающее окно меню. Код будет выглядеть так:

//...headermenu:{  width:210,  data:[     { id:"car_year", value:"Year" },    { id:"color", value:"Color" },    { id:"vin_code", value:"VIN" },    { id:"phone_number", value:"Phone" },    //...  ]},column:[  { id:"stared", header:[{ content:"headerMenu", colspan:2, ...,}], ..., },  { id:"rank", header:["",""], ..., },  //...]

В объекте свойства headermenu мы задаем ширину всплывающего окна и массив столбцов, которые можно будет прятать при клике по специальной иконке в виде глаза.

В браузере мы увидим такой результат:

Опция headermenuОпция headermenu

Теперь мы с легкостью можем скрывать и отображать нужные нам столбцы. Все что для этого требуется, просто кликнуть по иконке в левом верхнем углу и выбрать необходимые элементы в выпадающем списке.

Пагинация для таблицы

Если количество рядов таблицы достаточно большое, то использовать очень длинную вертикальную прокрутку не совсем удобно. Для решения этого вопроса можно добавить пагинацию. И как вы наверное уже догадались, виджет DataTable предоставляет нам такую возможность.

Для начала, давайте создадим модуль пагинации в файле pager.js. Его код будет выглядеть так:

//pager.jsconst pager = {  view:"pager",  id:"pager",  size:20,  group:5,  template:`{common.first()} {common.prev()} {common.pages()} {common.next()} {common.last()}`};

С помощью свойств size и group мы устанавливаем количество элементов на странице (20) и число видимых кнопок для пагинатора (5). Вот, в принципе, и все настройки. Также можно задать свойство template, которое определяет кнопки для переключения страниц (помимо кнопок с цифрами).

Теперь давайте подключим модуль с компонентом в файл index.html и добавим переменную с пагинатором в конструктор приложения:

//index.html<!--App sources --><script src="js/datatable.js" type="text/javascript" charset="utf-8"></script><script src="js/pager.js" type="text/javascript" charset="utf-8"></script>//...<script type="text/javascript">  webix.ready(function(){    webix.ui({      rows:[        datatable,        {cols:[          {},pager,{}        ]}      ]    });  });</script>

Ну и в завершение, давайте свяжем пагинатор с таблицей, чтобы при переключении кнопок, данные таблицы менялись в соответствии с настройками (по 20 рядов на странице). Для этого нужно задать свойству pager значение id созданного нами пагинатора в конструкторе таблицы. Вот такими нехитрыми манипуляциями реализуется пагинация для таблицы в библиотеке Webix. В браузере мы увидим следующий результат:

ПагинацияПагинация

Операции с рядами таблицы

Давайте предоставим потенциальному пользователю нашей таблицы возможность добавлять, удалять и перетаскивать ряды с данными. Для этого мы предусмотрим отдельные столбцы, в ячейках которых будут находиться соответствующие иконки для удаления и перетаскивания. В хедере этих столбцов мы разместим иконку для добавления новых рядов.

Стоит отметить, что в библиотеке Webix есть несколько способов добавлять иконки. Можно использовать иконки из встроенного шрифта (<span class='webix_icon wxi-drag'></span>), или специальные встроенные элементы (common.trashIcon()).

Чтобы это реализовать, нужно перейти к массиву свойства columns и добавить следующие настройки:

column:[  //...  {     header:[{text:"<span class='webix_icon wxi-plus-circle'></span>", colspan:2}],     width:50, template:"<span class='webix_icon wxi-drag'></span>"   },  { header:["",""], width:50, template:"{common.trashIcon()}" }]

Теперь в конце таблицы появятся 2 дополнительных столбца, в каждом ряду которых будут находиться иконки для удаления и перетаскивания. Так как столбцов у нас достаточно много, давайте, для удобства, зафиксируем эти 2 столбца с иконками в конце таблицы, чтобы они постоянно были на виду и не прокручивались. Для этого, в конструкторе виджета необходимо задать свойству rightSplit значение 2. В браузере мы увидим следующий результат:

Иконки для операций с рядамиИконки для операций с рядами

Иконки у нас готовы. Теперь давайте установим обработчики на событие клика по этим иконкам. Чтобы поймать событие клика по любому элементу таблицы с определенным css классом, необходимо воспользоваться свойством onClick. В объекте этого свойства нужно указать класс иконки и присвоить ему соответствующий обработчик. В нашем случае, мы ловим клик по иконкам с классами wxi-plus-circle и wxi-trash:

onClick:{  "wxi-plus-circle":() => addNewElement(), //добавляет элемент  "wxi-trash":(e,id) => removeElement(id), //удаляет элемент  //...,}

Теперь давайте создадим эти обработчики. Функция для добавления новых данных будет выглядеть так:

function addNewElement(){  const table = $$("car_rental_table"); //получаем доступ к таблице  //добавляем данные  const id_new_elem = table.add({"active":0,"color":"#1c1919","date":new Date()});   table.showItem(id_new_elem); //показываем новый элемент в таблице}

С помощью метода таблицы add() мы можем добавить в нее новые данные. Этот метод возвращает id новой записи, который мы передаем другому методу таблицы showItem(), чтобы показать (проскролить) этот элемент в таблице.

Функция для удаления записи будет выглядеть так:

function removeElement(id){  $$("car_rental_table").remove(id);}

Метод таблицы remove() получает id выбранного элемента в качестве параметра и удаляет его из таблицы.

Нам осталось решить вопрос с перетаскиванием рядов. Эта опция активируется с помощью свойства drag. В конструкторе таблицы ему нужно задать значение order. После этого, мы можем перетаскивать любой ряд (вверх или вниз) и оставлять его в нужном месте.

Сейчас перетаскивать элемент можно за любую его часть. Давайте ограничим зону перетаскивания на специально созданной иконке с классом wxi-drag .

Для этого мы воспользуемся свойством on, в объекте которого и установим обработчик на событие onBeforeDrag:

on:{  onBeforeDrag:function(data, e){     return (e.target||e.srcElement).className == "webix_icon wxi-drag";  }}

В браузере мы увидим следующий результат:

Перетаскивание рядовПеретаскивание рядов

Теперь мы можем добавлять, удалять и перетаскивать ряды таблицы при помощи специальных иконок.

Тулбар с дополнительными опциями

Давайте добавим нашему приложению возможность сбрасывать фильтры, добавлять новые столбцы и экспортировать данные в формате Excel. Для этого мы создадим тулбар, в котором разместим соответствующие кнопки.

В файле toolbar.js мы создаем компонент toolbar, внутри которого определяем кнопки Reset filters, Add column и Export to Excel. Выглядит это так:

const toolbar = {  view:"toolbar",  css:"webix_dark",  height:50,  //...  cols:[    //...    { view:"button", label:"Reset filters", click:resetFilters },    { view:"button", label:"Add column", click:addColumn },    { view:"button", label:"Export to Excel", click:exportToExcel }  ]};

С помощью свойства click мы устанавливаем обработчики на событие клика по кнопкам. Давайте создадим эти обработчик. А начнем мы из функции, которая будет сбрасывать фильтрацию данных таблицы и сами фильтры. Код будет выглядеть так:

function resetFilters(){  const table = $$("car_rental_table");  table.filter();   table.showItem(table.getFirstId());   table.setState({filter:{}}); }

Метод filter(), вызванный для таблицы без параметров, отображает данные в первоначальном порядке. С помощью метода таблицы setState() мы очищаем значения полей фильтров.

Следующей на очереди у нас функция, которая будет добавлять новые столбцы. Код будет выглядеть так:

function addColumn(){  const table = $$("car_rental_table");  table.config.columns.splice(3,0,{    id:"c"+webix.uid(),    header:`<span class="webix_icon wxi-close-circle" webix_tooltip="Delete column"></span>Extra column`,    editor:"text",    width:120  });  table.refreshColumns();}

С помощью свойства таблицы config.columns мы получаем массив с настройками столбцов и добавляем туда объект с настройками нового столбца в 4 позицию. Для этого используем js метод splice(). Когда данные изменены, нужно обновить представление столбцов с помощью метода таблицы refreshColumns().

И у нас осталась только функция, которая будет экспортировать данные таблицы в формате Excel. Код будет выглядеть так:

function exportToExcel(){  webix.toExcel("car_rental_table", {    filename:"Car Rental Table",    filterHTML:true,    styles:true  });}

Внутри функции мы используем метод webix.toExcel(), которому передаем id таблицы и объект с необходимыми настройками. Вот и вся хитрость.

Когда все уже готово, нужно включить файл toolbar.js в файл index.html и добавить переменную toolbar в конструктор приложения:

webix.ui({  rows:[    toolbar,    datatable,    {cols:[    {},pager,{}    ]}  ]});

В браузере наше приложение будет выглядеть следующим образом:

Тулбар с кнопками Тулбар с кнопками

Теперь мы можем сбрасывать фильтрацию данных, добавлять новые столбцы, а также экспортировать данные в формате Excel.

Остался еще один нюанс. При создании нового столбца, в его хедер мы добавляем иконку с классом wxi-close-circle. Нам нужно установить обработчик на событие клика по этой иконке, который будет удалять столбец. Сделать это можно в объекте свойства onClick:

onClick:{  //...  "wxi-close-circle":(e,id) => deleteColumn(id)}

Теперь давайте создадим этот обработчик:

function deleteColumn(id){  const table = $$("car_rental_table");  table.clearSelection();  table.editStop();  table.refreshColumns(table.config.columns.filter(i=>i.id !== id.column));}

Через свойство config.columns мы получаем массив настроек столбцов, отфильтровываем из него ненужный элемент и передаем обновленный массив методу таблицы refreshColumns().

В браузере мы увидим следующий результат:

Добавляем и удаляем новый столбецДобавляем и удаляем новый столбец

Теперь, помимо всего прочего, мы можем удалять новые столбцы при клике по иконке в правом верхнем углу их хедеров.

Заключение

С кодом готового приложения можно ознакомиться тут.

В этой статье мы детально разобрали, как создать приложение на основе виджета DataTable библиотеки Webix UI, а также научились настраивать таблицу с помощью большого разнообразия свойств и методов. Как вы могли убедиться, все настройки интуитивно понятны и просты в применении. На самом деле, это только малая часть всего, что может предложить нам таблица Webix. Для более детального ознакомления можно перейти к документации, где вы сможете найти подробное описание всех возможностей компонента с наглядными примерами.

Подробнее..

Создаем Booking приложение с Webix UI

12.04.2021 16:18:43 | Автор: admin
Webix UIWebix UI

Эта статья предназначена для тех, кто ценит свое время и не желает тратить многие месяцы на дотошное изучение нативных технологий web разработки. Знать и разбираться в них безусловно полезно, но в современном мире технологии развиваются настолько стремительно, что уследить за всеми тонкостями - задача не из легких. Чтобы не чахнуть над талмудами скучнейших документаций, которые изменятся уже завтра, можно использовать готовые решения.

Одно из лучших решений предлагает нам команда Webix. Их библиотеку UI компонентов мы и рассмотрим в этой статье.

Для наглядности создадим собственное приложение для поиска и бронирования авиабилетов. Не за горами сезон летних отпусков, так что такой выбор как никогда актуален.

С кодом готового приложения можно ознакомиться тут.

Немного о Webix и его возможностях

Webix UI это JavaScript библиотека, которая позволяет создавать отзывчивый дизайн и обеспечивает высокую производительность приложения. Диапазон возможностей представлен UI компонентами различной сложности, от обычной кнопки, до таких комплексных решений, как Report Manager, с помощью которого можно создавать и экспортировать отчеты данных. Помимо самих компонентов, библиотека предоставляет много дополнительных возможностей для работы с ними. Например, механизм обработки событий, методы работы с данными, взаимодействие с сервером, темы для стилизации и многое другое. Обо всем этом и не только можно узнать в документации. А сейчас самое время перейти к практической части.

Источник UI магии

Магия не происходит сама по себе. Для того, чтобы ее использовать, нужно знать заклинания и обладать секретом ее силы. Пришло время приподнять завесу таинственности и сделать эти знания доступными не только для узкого круга посвященных.

Итак, для того, чтобы начать использовать библиотеку, нужно сперва получить необходимые файлы (они же источники webix-магии). Для этого переходим на страницу загрузки, вводим необходимые данные и получаем ссылку на скачивание заветного zip-файла. Скачиваем и распаковываем его. Внутри находятся файлы license.txt, readme.txt и whatsnew.txt, которые могут заинтересовать тех, кто любит глубоко погрузиться в изучение вопроса. Кроме этого, в папке samples можно посмотреть примеры того, какие полезные вещи можно сотворить при помощи Webix UI.

Больше всего нас интересует содержимое папки codebase, а именно, два сакральных файла: webix.js и webix.css. Для того, чтобы Webix-магия начала действовать, нужно включить их в index.html файл будущего проекта:

<!DOCTYPE html><html>    <head>      <title>Webix Booking</title>      <meta charset="utf-8">      <link rel="stylesheet" type="text/css" href="codebase/webix.css">      <script type="text/javascript" src="codebase/webix.js"></script>    </head>    <body><script type="text/javascript">...</script>    </body></html>

Внутри кода мы добавим теги <script>...</script>, где и будем собирать наше приложение.

Впрочем, архив с библиотекой скачивать не обязательно. Необходимые файлы доступны также через CDN. Подключить их можно следующим образом:

<link rel="stylesheet" type="text/css" href="http://personeltest.ru/away/cdn.webix.com/edge/webix.css"><script type="text/javascript" src="http://personeltest.ru/away/cdn.webix.com/edge/webix.js"></script>

Инициализация

Теперь давайте перейдем непосредственно к работе с Webix.

Вся Webix-магия происходит внутри конструктора webix.ui(). Нам нужно убедиться в том, что код начнет выполняться после полной загрузки HTML страницы. Для этого обернем его в webix.ready(function(){}). Выглядит это следующим образом:

webix.ready(function(){webix.ui({    /*код приложения*/});});

После создания index.html файла и подключения необходимых инструментов, самое время перейти к непосредственной разработке нашего Booking приложения. Начнем с интерфейса.

Создаем приложение Booking

Интерфейс нашего приложение будет состоять из следующих частей:

  • Тулбар

  • Форма поиска рейсов

  • Таблица рейсов

  • Форма заказа.

Каждую из частей мы будем разрабатывать и хранить в отдельных файлах, а соберем их в файле index.html. Предлагаю детально ознакомиться с тем, как это реализуется на практике. А начнем мы с построения лейаутов, которые определяют способ расположения элементов на странице.

Лейаут

Чтобы наполнить страницу содержимым, нужно описать необходимые элементы в формате JSON. Давайте создадим каркас нашего приложения и постепенно будем добавлять туда новые компоненты.

Для сознания лейаута предусмотрены такие сущности, как rows и cols, с помощью которых можно разбить страницу на строки и столбцы, отделенные между собой границами. Каждое из свойств содержит массив, внутрь которого можно поместить другие компоненты.

Сначала разделим наш лейаут на 2 одинаковых ряда. Для этого воспользуемся свойством rows:

webix.ui({    rows: [        { template:"Row One" },        { template:"Row Two" }    ]});

Теперь лейаут будет выглядеть следующим образом:

В этом примере с помощью выражения template:"Row One" мы создали простой контейнер, в который можно поместить любой HTML-контент.

В верхний ряд этого лейаута мы поместим Тулбар. В нижнем будут находиться 2 сменяемых модуля:

  • Модуль поиска рейсов

  • Модуль заказа рейсов.

Модуль поиска рейсов нужно разделить на 2 столбца. В левом мы разместим форму поиска, а в правом таблицу данных. Для этого воспользуемся свойством cols:

webix.ui({  rows: [          { template:"Toolbar", height:50},          {              cols:[                { template:"Search Form" },                { template:"Data Table" }              ]          }  ]});

Теперь лейаут будет иметь следующий вид:

Комбинируя таким образом вложенные строки и столбцы, можно добиться необходимого результата. По умолчанию контейнеры заполняют все доступное пространство. Создавая два контейнера, мы получим 2 одинаковых прямоугольных области. Для того, чтобы задать нужные размеры элементов, можно использовать знакомые всем по CSS свойства width и height.

Теперь перейдем к модулю заказа рейсов и также разделим его на 2 столбца. В левом мы разместим форму заказа, а правый столбец оставим пустым. А почему же пустым, спросите вы? Ответ простой: если этого не сделать, форма растянется на весь экран, а это не очень эстетично. Пустой компонент, он же spacer, помогает выравнивать компоненты в интерфейсе. После этого мы можем задать необходимый размер нашей формы, а лишнее пространство будет заполнено этим отсутствующим компонентом, если можно так выразиться.

Здесь мы снова воспользуемся уже знакомым атрибутом cols. Стоит напомнить, что компоненты внутри cols и rows по умолчанию разделены тонкой серой линией. Такой разделитель между формой и спейсером нам ни к чему. Webix позволяет управлять им с помощью свойства type:

webix.ui({  rows: [    { template:"Toolbar", height:50 },    {      type:"clean", //убираем линию-разделитель      cols:[        { template:"Order Form" },        {}      ]    }  ]});

Результат будет следующим:

Мы создали отдельные лейауты для модулей поиска и заказа рейсов. Теперь нужно сделать их сменяемыми. Как вы уже догадались, Webix предусматривает и такую возможность. Реализуется она с помощью multiview компонента.

Для этого нужно поместить необходимые модули в массив свойства cells и присвоить им соответствующие id. Стоит упомянуть, что при первоначальной загрузке будет отображаться модуль поиска рейсов, который задан первым элементом массива, но о том, как сменяются модули, мы поговорим чуть позже.

А сейчас наш код будет выглядеть следующим образом:

webix.ui({  rows:[    { template:"Toolbar", height:50 },    {      cells:[        {          id:"flight_search",          cols:[            {template:"Search Form"},            {template:"Data Table"},          ]        },        {          id:"flight_booking",          type:"clean",          cols:[            {template:"Order Form"},            {},          ]        }      ]    }  ]});

Мы уже знаем, как работать с лейаутами. Теперь пришло время приступить к разработке непосредственно компонентов интерфейса нашего приложения.

Тулбар

Тулбар важная часть любого приложения. Зачастую там располагаются инструменты управления и прочие ништяки. В нашем случае тулбар будет иметь динамически сменяемый лейбл.

В файле toolbar.js создаем компонент с помощью следующей строки:

const toolbar = { view:"toolbar" };

Нужно уточнить, что каждый значимый компонент мы сохраняем в переменной, чтобы потом использовать его для сборки в лейауте.

Как мы видим, тип создаваемого компонента определяется значением свойства view. В нашем случае это toolbar.Давайте стилизуем компонент и добавим лейбл с названием:

{  view:"toolbar"  css:"webix_dark", //стилизация  cols:[    {      id:"label", //указываем id для обращения      view:"label",      label:"Flight Search", //название    }  ]}

Выше мы упоминали, что вставлять компоненты друг в друга необходимо при помощи свойств rows и cols. Поэтому компонент label мы включаем в массив свойства cols компонента toolbar.

С помощью свойства id будет происходит обращение к элементу. Он должен быть уникальным. В нашем случае это нужно для динамической смены лейбла, но к этому мы еще вернемся.

Вот такими нехитрыми маневрами мы создали тулбар нашего приложения.Чтобы использовать компонент в лейауте, нужно сначала подключить файл toolbar.js в index.html и включить переменную в массив свойства rows вместо {template:Toolbar}:

webix.ui({rows: [    toolbar,    {      cells:[        ...      ]    }]});

На странице браузера мы увидим лейаут с интерфейсом тулбара:

Выглядит уже неплохо. Теперь настало время перейти к разработке модуля поиска рейсов. Он состоит из 2 частей:

  • Форма поиска рейсов

  • Таблица рейсов.

Давайте рассмотрим их детально.

Форма поиска рейсов

Форма поиска нам нужна для фильтрации данных о рейсах. Давайте разбираться, по каким параметрам должен происходить поиск и как это реализовать на практике.

Классическую форму можно создать при помощи элемента <form>, а внутри определить необходимые контролы. При этом нужно оперировать большим количеством тегов и атрибутов. Для обработки же данных и вовсе требуется чуть ли не докторская степень.

Webix предлагает сделать это гораздо проще. Давайте с этим разбираться.

Создадим нашу форму в файле search_form.js с помощью компонента form:

const search_form = { view:form };

Постепенно мы будем задавать элементы, которые нужны нам для поиска. Для этого воспользуемся свойством elements:

{  view:form,  ...  elements:[     { /*элемент формы*/ },    { /*элемент формы*/ },    ...  ]  ...}

Как вы видите, определять элементы формы необходимо внутри массива elements. Напомню, что каждый компонент библиотеки описывается с помощью json объекта. Давайте же узнаем, что нам могут предложить разработчики Webix для работы с формами.

Форма Webix имеет множество преимуществ. Она позволяет устанавливать и считывать значения всех входящих в нее полей сразу, а не по одному. Для этого нужно указать свойство name в конфигурации каждого поля. Также вы можете связывать ее с другими компонентами или сохранять данные непосредственно на сервер. Подробнее о работе с формами можно узнать здесь.

А сейчас нужно определиться с элементами, которые мы будем реализовывать. Чтобы выбрать пункты отправления и назначения, необходимо установить селекторы выбора городов. Также мы будем искать рейсы по дате отправления и возвращения. Контрол даты возвращения нужно спрятать и отображать его при необходимости. Реализуем это с помощью радиокнопок One Way и Return. При поиске нужно учитывать количество необходимых билетов. Для этого установим специальный счетчик. Ну и конечно, как же без кнопок управления формой Reset и Search. В итоге наша форма будет иметь следующий вид:

Теперь можно приступить к реализации задуманного.

Радиокнопки

В самом начале формы нужно определить радиокнопки, с помощью которых мы будем отображать и прятать контрол выбора даты возвращения. Делается это с помощью элемента radio:

{  view:"radio",  label:"Trip",  name:"trip_type",  value:1,  options:[    { id:1, value:"One-Way" },    { id:2, value:"Return" }  ]}

Значения радиокнопок задаем через свойство options. Помимо этого, указываем название компонента через свойство label, а имя, по которому будем получать значение, через name. По умолчанию мы будем искать полеты только в одну сторону, поэтому устанавливаем для value значение именно этой опции.

Селекторы выбора городов

Чтобы указать пункты отправления и назначения, нам понадобятся специальные селекторы. Создадим их с помощью элемента combo:

{    view:"combo",    id:"from",    clear:"replace",    ...    placeholder:"Select departure point",    options:cities_data}

Иногда пользователю необходимо подсказать, что нужно делать с тем или иным контролом. И тут на помощь нам приходит свойство placeholder. Мы указываем нужные действия, а приложение отобразит их в поле необходимого элемента. Когда пользователь ввел или выбрал нужные данные, но потом вдруг передумал, а такое случается довольно часто, мы предоставим ему возможность очистить поле одним кликом. Для этого нужно задать свойство clear в значении replace. Теперь, когда поле будет заполнено, в правой его части появится иконка, при клике по которой введенные ранее данные исчезнут (поле будет очищено).

При клике по селектору должен появиться выпадающий список опций с названиями городов. Давайте это реализуем. Данные для списка опций можно задавать через свойство options . Есть несколько способов загрузить данные в компоненты Webix. Можно задать их в виде массива напрямую, или же хранить в отдельном файле и просто указать необходимый URL, а компонент сам загрузит их.

Но дело в том, что эти же данные нам нужны для нескольких компонентов, а загружать их для каждого по отдельности будет несколько затратно. Webix решает эту проблему с помощью такой сущности, как DataCollection. Внутри этого компонента необходимо всего лишь указать URL, по которому будут загружаться данные. Информация загрузится один раз и будет доступна для многоразового использования. В нашем случае объект с данными хранится в файле ./data/cities.json. Давайте создадим коллекцию и для удобства сохраним ее в переменную:

const cities_data = new webix.DataCollection({  url:"./data/cities.json"});

Теперь данные доступны для использования в наших селекторах и не только.

Выше мы описали селектор выбора точки отправления. Второй селектор, который определяет пункт назначения, имеет незначительные отличия, на которых мы не будем останавливаться.

Селекторы выбора дат

Расписание - одна из важнейших составляющих современной жизни. При поиске рейсов обязательно нужно это учитывать. Давайте предоставим пользователю возможность указывать желаемое время вылета при поиске необходимого рейса. Сделаем мы это с помощью такого элемента, как datepicker:

{  view:"datepicker",  name:"departure_date",  ...  format:"%d %M %Y"}

При клике на селектор появится компактный календарь, в котором можно выбрать необходимую дату. Более того, у нас есть возможность задать необходимый формат, в котором выбранная дата будет отображаться в поле селектора. Реализуется это через свойство format.

Зададим такой же селектор для выбора даты обратного рейса, но изначально спрячем его при помощи свойства hidden. Именно этот селекор мы будем отображать при выборе радиокнопки Return, но об этом позже.

Счетчик количества билетов

Представим себе ситуацию, что наш потенциальный пользователь летит в долгожданный отпуск не один, а со всей семьей или компанией друзей. В таком случае ему понадобится не один билет, а некое энное количество. Давайте позволим ему устанавливать необходимое число билетов. Для этого предусмотрен специальный элемент counter. Использовать его очень удобно. С правой и левой стороны находятся иконки со знаками плюс и минус, с помощью которых и происходит увеличение или уменьшение значения. Нужно также установить минимальное значение через свойство min, чтобы пользователь не смог установить меньше 1 билета (согласитесь, это выглядело бы абсурдно). Код элемента будет выглядеть следующим образом:

{ view:"counter", name:"tickets", label:"Tickets", min:1 }

Кнопки поиска и сброса формы

Пользователь определился с параметрами поиска и заполнил необходимые поля. Теперь нужно создать кнопки, которые позволят запустить поиск нужных рейсов по указанным параметрам или очистить форму.

Для этого мы предусмотрим соответствующие кнопки Search и Reset. Определяем их с помощью элемента button. Название кнопок указываем через value, а стилизацию добавляем через свойство css. Здесь нужно уточнить, что библиотека предусматривает встроенную стилизацию кнопок. Мы будем использовать класс "webix_primary" для стилизации кнопки Search. Подробнее о стилизации кнопок можно узнать здесь.

Для удобства мы разместим наши кнопки в виде столбцов:

{  cols:[    { view:"button", value:"Reset" },    { view:"button", value:"Search", css:"webix_primary" }  ]}

Интерфейс формы поиска мы создали. Теперь давайте интегрируем его в наш лейаут - вы еще помните о нём? Для этого, как и в примере с тулбаром, нужно включить файл search_form.js в index.html. Интерфейс формы хранится в переменной search_form, которую мы и пропишем в лейауте:

[  toolbar,  {    cells:[      {        id:"flight_search",        cols:[          search_form,          { template:"Data Table" }        ]      }, ...    ]  },]

Интерфейс приложения будет выглядеть следующим образом:

Таблица рейсов

Пользователь ввел данные, нажал кнопку Search и ожидает увидеть результат. А результат будет отображаться в специальной таблице, созданием которой мы сейчас и займемся.

Возможности Webix таблицы сложно переоценить. Ее функционал начинается с отображения, фильтрации и сортировки данных, а заканчивается экспортом в PDF или Excel. Можно написать целую книгу о ее возможностях, продать и заработать баснословные деньги, но в данный момент нас больше интересует то, как сконфигурировать таблицу именно под наши нужды.

В файле datatable.js создаем таблицу рейсов с помощью компонента datatable:

const flight_table = {   view:"datatable",  url:flights_data};

Таблицу нужно заполнить данными о расписании рейсов. В нашем случае они хранятся в файле ./data/flights_data.json. Одно из преимуществ работы с таблицами Webix заключается в том, что можно просто указать путь к файлу или скрипту, а компонент сам сделает запрос, загрузит данные и отобразит их в соответствии с настройками. Проще только вообще ничего не делать.

Путь к данным нужно указывать через свойство url. Для удобства мы сохраним путь в переменную flights_data и присвоим ее нашему свойству.

Данные мы загрузили, теперь нужно их красиво отобразить. Для ооочень ленивых прогеров, Webix предусмотрел такое свойство как autoConfig. Если задать для него значение true, то данные автоматически распределятся в столбцы в соответствии с имеющимися полями.

Но мы еще не настолько разленились, чтобы прибегать к таким хитростям. Тем более, настройка столбцов вручную сулит нам множество увлекательных и полезных возможностей. Давайте взглянем на них более пристально.

Конфигурация столбцов происходит в массиве свойства columns:

columns:[  { /*конфигурации столбца*/ },  { /*конфигурации столбца*/ },  ...]

Чтобы данные из файла попали в нужный нам столбец, необходимо указать id, значение которого должно совпадать с полем, в котором хранятся соответствующие данные в файле.

У столбца должно быть название, или по-простому шапка. Шапку мы задаем при помощи свойства header.

Нередко бывают ситуации, когда данные приходят в одном виде, а отобразить их нужно в другом. Было бы нелепо предположить, что разработчики Webix не учли такой вероятности и не решили эту проблему заранее. Поэтому нам доступно свойство format.

Если мы уже коснулись вопроса изменения данных, то нужно остановиться на этом более детально. Вопрос актуален для нас и тем, что для некоторых столбцов мы будем применять форматирование. Речь идет о датах и названиях городов.

Давайте рассмотрим пример с датами.

Дело в том, что их значения хранятся и загружаются в виде строк типа "2021-03-26". Как вам должно быть известно, оперировать строчными датами не самое приятное занятие (для некоторых даже болезненное). Исходя из этого, можно сразу при загрузке перевести их в JS Date объекты. С помощью свойства scheme мы можем переопределить все строки указанного поля (данные которого попадут в столбец с таким же id) в соответствующие объекты Date перед их загрузкой в таблицу:

scheme:{  $init:function(obj){    obj.date = webix.Date.strToDate("%Y-%m-%d")(obj.date);  }}

Теперь значения дат попадают в столбец Date в виде объектов, а не строк. Так будет гораздо удобнее фильтровать рейсы при поиске билетов. Но на этом все не заканчивается. Нужно настроить их отображение непосредственно в ячейках столбца. Для этого в конфигурации столбца мы прописываем уже упомянутое свойство format в значении webix.i18n.longDateFormatStr. Теперь полученный объект Date преобразуется в заданное свойством format значение и отображается как 26 March 2021. Вот такая вот магия.

Столбцы, которые отображают названия городов, также заслуживают особого внимания. Дело в том, что данные о пунктах отправления и назначения хранятся и приходят в виде чисел. Зачем такие сложности спросите вы? Ответ простой. Сами названия городов хранятся в отдельной серверной таблице как id - value, а в данных о рейсах вместо названий хранятся только id городов. Чтобы получить название города по его id, нам нужно воспользоваться свойством collection.

Хочу напомнить, что при разработке селекторов выбора городов, мы создали коллекцию данных и сохранили ее в переменной cities_data. Вся прелесть работы с коллекциями заключается в том, что данные загружаются 1 раз и доступны для использования в неограниченном количестве.

Так вот, свойство collection отправляет запрос в коллекцию данных cities_data и получает соответствующее название города по его id, который соответствует числовому значению ячейки столбца. Здесь и добавить-то больше нечего. Такие мелочи делают процесс разработки приятным развлечением.

Но развлечения мы оставим на вечер пятницы. Сейчас нужно представить, что потенциальный пользователь ввел данные в форму поиска, нажал кнопку Search и нашел необходимый рейс в таблице (пока нужно только представить, так как реализацией интерактива мы займемся во второй части). Какие дальнейшие действия? Безусловно, нужно как можно быстрее помочь ему забронировать рейс. Как это сделать?

Для удобства бронирования в конце каждого ряда будет находиться кнопка, при клике по которой пользователь будет переходить к форме заказа. Но ни слова больше. Весь интерактив будет во второй части статьи, где мы вдохнем жизнь в статические интерфейсы.

Если вас заинтересовали возможности таблицы и непосредственно конфигурации столбцов, дополнительную информацию можно получить здесь.

А мы же продолжаем настраивать таблицу.

Наш пользователь нашел несколько рейсов (10, 100 или больше), которые соответствуют его запросам. Нужно помочь ему найти нужный рейс непосредственно в таблице. Давайте добавим строку поиска и разместим ее над таблицей, чтобы потенциальный пользователь нашел наконец-то свой рейс и с довольным лицом рассказывал своим друзьям и знакомым, какой крутой у нас сервис.

Для этого мы воспользуемся компонентом search:

const search_bar = { view:"search" };

Это текстовое поле с красивой иконкой поиск.

Когда все уже готово, остается интегрировать компоненты в наш лейаут. И выглядит это следующим образом:

[   toolbar,   {     cells:[       {         id:"flight_search",         cols:[           search_form,           {              rows:{               search_bar,               flight_table             }           }         ]       }, ...     ]   },]

Уже всё по-взрослому. Результат в браузере будет таким:

Форма заказа

Пользователь определился с рейсом и готов сделать заказ. Давайте поможем ему сделать это. Пришло время заняться формой заказа. Она будет отображаться вместо модуля поиска рейсов при клике по кнопке Book в таблице. Форма будет иметь следующий вид:

Для того, чтобы реализовать весь этот функционал, воспользуемся уже знакомым нам компонентом form и пропишем следующее:

const order_form = {  view:"form",  elements:[    ...  ]};

Теперь давайте опишем нужные нам элементы.

Поля ввода

Для заказа рейса нам нужно получить от пользователя его личные данные. Чтобы это сделать, мы создадим соответствующие поля ввода.

Давайте воспользуемся элементом text и его преимуществами:

elements:[  { view:"text", name:"first_name", , invalidMessage:"First Name can not be empty" },  { view:"text", name:"last_name", , invalidMessage:"Last Name can not be empty" },  { view:"text", name:"email", , invalidMessage:"Incorrect email address" },  ...]

В отличие от формы поиска здесь нам нужно добавим валидацию. Личные данные вещь серьезная, и пока пользователь не введет их правильно, он не сможет сделать заказ. Давайте предусмотрим такой сценарий, где пользователь не ввел личные данные или ввел их неправильно и отправил на обработку. Что же должно произойти между делом? Правильно, данные должны проверяться.

Webix предусматривает такое развитие событий и предоставляет нам готовое решение в виде свойства rules и набора условий для проверки введенных данных. Можно, конечно, и самому написать эти правила, создать несколько функций, прочитать пару книг по регулярным выражениям, сходить на курсы по их применению и вернуться обратно к встроенным правилам, потому что они реально удобны. Все, что нужно сделать, это присвоить правила соответствующим именам (свойствам name) полей формы в массиве rules:

{  elements:[  ],  rules:{    first_name:webix.rules.isNotEmpty, //поле не должно быть пустым    last_name:webix.rules.isNotEmpty,     email:webix.rules.isEmail //значение должно быть в формате email  }}

Если значение поля соответствует установленному правилу, форма разрешит дальнейшие действия. В противном случае, поля подсветятся красным. Но пользователю красное поле ни о чем конкретном не говорит. Он поймет, что сделал что то неправильное, но не поймет, что именно мы от него хотим.

Для этого у Webix элементов есть хорошее решение в виде свойства invalidMessage. Для каждого поля можно задать сообщение, которое появится в случае неправильных данных:

{ ..., invalidMessage:"First Name can not be empty", ... }

Закрадывается мысль, что у Webix есть готовые решения на все случаи жизни. Но нужно помнить, что это всего лишь UI библиотека, а настоящая валидация по-хорошему должна происходить на сервере.

Счетчик

В форме поиска наш пользователь установил нужное количество билетов и нажал кнопку Book напротив необходимого рейса. Приложение отобразило форму заказа, где он может подтвердить его. Но у нашего пользователя вдруг возникло желание заказать не 2 билета (как он указал при поиске), а три. Давайте дадим ему возможность изменять количество билетов прямо в форме заказа.

Для этого используем уже знакомый нам элемент counter. По сути, он такой же, как и в предыдущей форме. Его первоначальное значение будет зависеть от значения счетчика формы поиска и количества свободных мест на выбранном рейсе. С его помощью пользователь сможет изменять количество необходимых билетов, не возвращаясь назад к поиску:

{ view:"counter", id:"tickets_counter", name:"tickets", label:"Tickets", min:1 }

Чекбоксы

Каждый из нас хоть раз пользовался услугами авиакомпаний и знает, что за дополнительный багаж, который не влез в разрешенную ручную кладь, нужно доплатить. Помимо этого, если перелет планируется длинным, мы предоставим возможность включить стоимость еды в цену билета. Давайте посмотрим, как это реализовать.

Для этого мы создадим 2 чекбокса Checked-in Baggage и Food и будем их использовать для того, чтобы повышать цену за билет, если пользователь выбрал эти дополнительные плюшки. Чекбокс реализуется с помощью элемента checkbox:

[  { view:"checkbox", name:"baggage", label:"Checked-in Baggage", checkValue:15 },  { view:"checkbox", name:"food", label:"Food", checkValue:10 }]

Через свойство checkValue мы задаем значение отмеченного чекбокса. В нашем случае за дополнительный багаж и питание пользователю придется доплатить 15 и 10 долларов соответственно.

Радиокнопки

Теперь давайте перейдем к удобствам полета. За это у нас отвечают радиокнопки Economy и Business. Реализуются они с помощью уже знакомого нам элемента radio:

{   view:"radio", name:"class", label:"Class",  options:[    { id:1, value:"Economy" },    { id:2, value:"Business" }  ]}

Мы установим класс Economy по умолчанию, а при переключении на Business стандартная цена будет удваиваться. А что вы хотите, за комфорт нужно платить.

Лейбл

Хороший сервис информативный сервис. Именно поэтому нам нужно отображать актуальную информацию о стоимости заказа. Давайте настроим отображение итоговой цены. Реализуется это с помощью уже знакомого нам компонента label:

{ view:"label", id:"order_price" }

Изначальную цену мы устанавливаем при клике по кнопке Book в таблице рейсов. Она учитывает стоимость билета и их количество. По мере добавления плюшек или изменения количество билетов цена будет пересчитываться автоматически.

Кнопки Go Back и Make Order

Наш пользователь наконец-то определился с заказом и готов его сделать или передумал и хочет вернуться назад к поиску. Давайте реализуем такую возможность. Для этого мы создадим соответствующие кнопки Go Back и Make Order с помощью знакомого нам элемента button:

{   cols:[    { view:"button", value:"Go Back" },    { view:"button", value:"Make Order" }  ] }

Мы создали интерфейс формы заказа рейсов. Давайте поместим его в наш лейаут и посмотрим, что получилось:

[  toolbar,  {    cells:[      {...},      {        id:"flight_booking",        type:"clean",        cols:[           order_form,            {}        ]  }]}]

Результат в браузере:

Мы описали все элементы интерфейса и создали лейаут. Теперь пришло время оживить наше приложения.

Заставляем приложение работать

В первой части мы описали лейаут и внешний вид компонентов интерфейса нашего приложения с помощью Webix UI. Но это только вершина айсберга. Давайте оживим наш проект и сделаем его интерактивным. Раньше мы только предполагали, что будет, если пользователь сделает то или иное. Сейчас пришло время осуществить это на практике, тем более Webix предоставляет нам огромный набор инструментов для реализации задуманного.

Форма поиска

Радиокнопки

Итак, в самом верху формы поиска у нас находятся радиокнопки One-Way, которая установлена по умолчанию, и Return, при клике по которой должен отображаться спрятанный селектор даты возвращения. Как нам это реализовать? А все очень просто, нужно установить специальный обработчик на событие переключения между радиокнопками. Для работы с событиями предусмотрены специальные свойства. Самым универсальным из них является свойство on, с помощью которого можно установить обработчик сразу на несколько событий. Выглядит это следующим образом:

{  view:"radio",  ...  on:{    onChange:function(newv){      if(newv == 2){        $$("return_date").show(); //отображаем селектор даты возвращения      }else{        $$("return_date").hide(); //прячем селектор даты возвращения      }    }  }}

Теперь при переключении между радиокнопками функция будет прятать и отображать селектор даты возвращения. Реализуется это с помощью специальных методов show() и hide(), названия которых говорят сами за себя. Универсальный метод $$(id) позволяет получить доступ к соответствующему компоненту, id которого передан в качестве аргумента.

Селекторы городов

Наш пользователь выбрал Берлин в качестве пункта отправления. После этого он переходит ко второму селектору и видит, что там тоже есть Берлин. Почему же так случилось? Дело в том, что данные о городах загружаются из одной коллекции данных. Но пользователя это не интересует, он не должен иметь возможность искать рейсы из Берлина в Берлин. Давайте это исправим.

Для того, чтобы выбранная опция одного селектора не повторялась в списке опций второго, нужно установить специальный обработчик, который будет анализировать их данные и исключать ненужные. Делается это при открытии выпадающего списка:

...{  options:cities_data,  on:{    onShow:function(v){      optionsData("from","to");    }  }}

Снова на помощь приходит свойство on, которое позволяет устанавливать обработчик на необходимые события. В нашем случае это событие onShow. При клике на селектор приложение запустит функцию optionsData(), которая отфильтрует и отобразит список доступных опций компонента, для которого она вызвана. Она имеет следующий вид:

function optionsData(first_id, second_id){  const options = $$(first_id).getList(); //получаем объект значений списка  const exception = $$(second_id).getValue(); //получаем выбранное значение  options.filter(obj => obj.id != exception); //фильтруем список}

Здесь мы используем такие полезные методы Combo, как getList() и getValue(). Первый метод получает список опций одного селектора, а второй установленное значение другого. С помощью метода filter() выпадающего листа функция фильтрует список и исключает из него выбранный город (полученный методом getValue).

Теперь перейдем к кнопкам Reset и Search.

Кнопка Search

Наш пользователь ввел необходимые условия поиска, кликнул по кнопке Search и видит результат в таблице рейсов. Такой сценарий более чем вероятен, поэтому нужно его реализовать. Для этого необходимо создать и установить обработчик на событие клика по кнопке. Давайте сначала создадим этот обработчик:

function lookForFlights(){const vals = $$("search_form").getValues(); //получаем объект со значениями полей формыconst table = $$("flight_table"); //получаем доступ к таблице данныхtable.filter(function(obj){ /*условия для фильтрации*/ });}

Одно из множества преимуществ формы Webix заключается в том, что можно получит значения всех полей сразу в одном объекте. Реализуется это с помощью метода getValues(), вызванного для формы. Полученные значения используются для определения условий фильтрации в методе filter(), вызванного для таблицы данных. Метод перебирает элементы данных таблицы, фильтрует их с помощью условий, полученных из формы, и перестраивает таблицу с учетом изменившихся данных. Можно только представить, сколько времени займет реализация подобного функционала на чистом JS.

Итак, обработчик мы создали, сейчас нужно его вызвать на событие клика по кнопке. Для таких случаев у кнопки предусмотрено свойство click:

{ view:"button", value:"Search", ... click: lookForFlights }

Кнопка Reset

Бывает,что пользователь уже заполнил форму, но ему нужно очистить ее и начать заполнять заново, или он уже начал искать и хочет вернуть таблицу в первоначальное состояние. Такой сценарий вполне вероятен, поэтому давайте его реализуем при клике по кнопке Reset.

Для начала нужно создать соответствующий обработчик:

function resetForm(){const form = $$("search_form"); form.clear(); //очищаем формуform.setValues({trip_type:1}); //устанавливаем значения радиокнопки по умолчанию$$("flight_table").filter(); //сбрасываем данные таблицы по умолчанию}

С помощью метода clear(), функция очищает все поля формы сразу. Но у нас есть поле с типом билета (прямой или обратный) значение которого нужно установить по умолчанию. Исправляем этот недочет с помощью метода setValues(). В качестве аргумента, мы передаем объект с нужным значением для этого поля.

Выше мы упоминали метод filter() у таблицы, с помощью которого фильтровали её данные в соответствии с условиями поиска. Если вызвать этот метод без указания параметров с условиями, то ее можно сбросить фильтрацию, и тогда ее данные будут выглядеть как при первоначальной загрузке. Все коротко и ясно. Нет нужды создавать массу дополнительных функций или перезагружать приложение. Интуитивно понятные методы значительно упрощают процесс разработки. Все, что нам осталось, это установить обработчик для нашей кнопки:

{ view:"button", value:"Reset", ... click:resetForm }

С формой поиска мы разобрались. Давайте перейдем к таблице рейсов и строке поиска.

Строка поиска

Как вы помните, мы создали строку поиска, с помощью которой пользователь может искать данные прямо в таблице рейсов.

Чтобы заставить строку поиска работать, нужно добавить обработчик, который будет сравнивать введенные значения с данными таблицы и выделять их. Для начала создадим этот обработчик:

function searchFlight(){  //получаем значение строки поискаconst value = $$("search").getValue().toLowerCase(); const table = $$("flight_table");  //формируем объект с совпадениямиconst res = table.find(function(obj){ /*условия поиска*/ }); table.clearCss("marker", true);  //убираем предыдущие стилиfor(let i = 0; i < res.length; i++){table.addCss(res[i].id, "marker", true); //вешаем  css класс marker}table.refresh(); //перерисовываем таблицу}

Функция получает значение строки поиска через метод getValue() и сравнивает его с данными таблицы. Для этого используется метод find(), вызванный для таблицы рейсов. Ряды таблицы, значения которых совпали с введенными данными, помечаются с помощью css класса marker (стили которого находятся в нашем css файле). После проделанных действий, нужно обязательно обновить представление с помощью метода refresh(), чтобы заданный css класс подсветил нужные ряды.

Теперь необходимо установить этот обработчик на событие onTimedKeyPress, который будет обрабатывать значения текстового поля при вводе. Делается это при помощи знакомого нам свойства on. Код будет выглядеть следующим образом:

{  view:"search",  id:"search",  ...  on:{    onTimedKeyPress:function(){ //срабатывает при наборе текста      searchFlight(); //анализируем данные таблицы и подсвечиваем совпадения    },    onChange:function(){ //срабатывает при нажатии на иконку очистить      if(!this.getValue()){        $$("flight_table").clearCss("marker"); //убираем подсветку      }    }  }}

Дополнительно, мы установим обработчик на событие onChange. Он будет срабатывать при клике по иконке очистить, которую мы установили с помощью свойства clear, и убирать подсвечивание с рядов.

Таблица рейсов

На данный момент пользователь уже может искать информацию с помощью формы поиска и строки поиска, а необходимые рейсы отображаются и подсвечиваются в таблице.

Как вы помните, напротив каждого рейса мы создали кнопку Book, с помощью которой пользователь может перейти к форме заказа. Давайте посмотрим, как организовать переход и какие тут есть дополнительные нюансы.

Давайте начнем со смены модулей.

При клике на кнопку Book приложение спрячет модуль поиска и отобразит модуль с формой заказа. Нужно напомнить, что при создании лейаута, мы использовали компонент multiview. Наши модули находятся в массиве свойства cells, и каждому присвоен соответствующий id:

cells:[  {    id:"flight_search", ...  },  {    id:"flight_booking", ...  }]

Смена multiview модулей реализуется с помощью вызова метода show() для модуля, который нужно отобразить. Доступ к модулю мы получаем через универсальный метод $$(id). В качестве аргумента передаем id нужного модуля:

$$("flight_booking").show(); //отображаем форму регистрации

Эту строку нам нужно включить в тело нашего обработчика. Давайте его создадим:

function bookFlight(id){//получаем количество искомых билетовconst required_tickets = $$("search_form").getValues().tickets;//получаем количество свободных местconst available_places = flight_table.getItem(id).places;//устанавливаем максимальное количество билетов $$("tickets_counter").define("max", available_places); //устанавливаем необходимые значения в поля формы регистрации$$("flight_booking_form").setValues({//количество билетов должно быть меньше или соответствовать свободным местамtickets:required_tickets <= available_places ? required_tickets : available_places,//устанавливаем значение цены билета в скрытый инпутprice:flight_table.getItem(id).price,//устанавливаем "Эконом" класс по умолчаниюclass:1});$$("flight_booking").show(); //отображаем форму регистрации...}

Если коротко, то функция получает объект с данными о необходимом рейсе, устанавливает нужные значения в поля формы заказа и отображает модуль с формой заказа.

Здесь стоит обратить внимание на такой метод для работы с таблицей, как getItem(). С его помощью мы получаем объект значений ряда таблицы, в котором находится кнопка Book. Из этого объекта мы извлекаем цену билета и количество свободных мест. В качестве аргумента передаем id соответствующего ряда.

Как вы помните, при описании тулбара мы определили, что лейбл будет сменяемым. Сменяться он будет при переходе на другой модуль. Реализуется это с помощью методов define() и refresh().

Метод define() позволяет изменить любое свойство компонента, а метод refresh() обновляет его визуальное представление:

$$("label").define("label", "Flight Booking"); //меняем лейбл на тулбаре$$("label").refresh(); //обновляем новый лейбл

Теперь нужно установить обработчик на событие клика по кнопке Book. При создании, мы присвоили ей класс webix_button и сделали это не просто так. Webix предусматривает специальный хендлер onClick, предназначенный для кликов по элементам ячеек таблицы, которые отмечены тем или иным css классом:

{  columns:[  ],  onClick:{    "webix_button":function(e,id){      bookFlight(id);    }  }}

Теперь все работает и пользователь может переходить к форме заказа.

Форма заказа

После клика по кнопке Book пользователь переходит к форме заказа. Выше мы описали, как это реализуется. Теперь давайте разберемся, что происходит непосредственно в форме заказа. И начнем мы с системы подсчета стоимости.

Изначальную цену мы устанавливаем при клике по кнопке Book в таблице рейсов. Она учитывает стоимость билета и их количество. По мере добавления пользователем плюшек или изменения количества билетов цена будет пересчитываться автоматически. Чтобы это реализовать, нам необходимо прибегнуть к старому доброму свойству on, через которое реализуется подписка на события onChange (реагирует на изменение значений пользователем) и onValues (реагирует на установку значений при клике на кнопку Book):

on:{onChange:function(){orderPrice(this.getValues());},onValues:function(){orderPrice(this.getValues());}}

Функция имеет следующий вид:

function orderPrice(vals){//получаем количество билетовconst tickets = vals.tickets; //получаем цену с учетом класса и количества билетовconst price = vals.price*1*vals.class*tickets; //получаем стоимость дополнительного багажаconst baggage = vals.baggage * tickets;//получаем стоимость питания const food = vals.food * tickets; //формируем итоговую суммуconst total = price+baggage+food; //отображаем итоговую сумму$$("order_price").setValue(total+"$"); }

Функция-обработчик этих событий получает в качестве аргумента объект со значениями формы через метод getValues(), анализирует их и устанавливает итоговую стоимость внутри лейбла Price через метод setValue(). В отличие от метода setValues(), которому можно передать объект со значениями всех полей формы, setValue() устанавливает только одно значение элементу, для которого вызван.

С подсчетом стоимости заказа мы разобрались. Пользователь добавляет плюшки, а цена автоматически пересчитывается и отображается.

Теперь пользователь решил сделать заказ, ввел данные и кликнул по кнопке Make Order. Для того, чтобы определить сценарий дальнейших действий, нужно создать соответствующий обработчик:

function makeOrder(){const order_form = $$("flight_booking_form"); //получаем доступ к формеif(order_form.validate()){ //запускаем валидацию формыwebix.alert({ //в случае успешной валидации выводим сообщение о заказеtitle: "Order",text: "We will send you an information to confirm your Order"}).then(function(){goBack(); //очищаем валидацию и возвращаемся к таблице рейсов});}}

Функция запускает валидацию формы при помощи метода validate(). Этот метод анализирует значения полей формы, для которых мы установили правила в свойстве rules. Если значения полей соответствуют правилам, валидация пройдет успешно и метод вернет true. В противном случае метод вернет false, поля с некорректными данными подсветятся красным, а внизу появятся сообщения об ошибках. Напомню, что эти сообщения мы определяем через свойство invalidMessage.

В случае успешной валидации функция выведет сообщение о готовности заказа. Здесь стоит сказать, что Webix имеет несколько методов для вывода сообщений. Мы будем использовать webix.alert(), для которого можно указать действия, которые выполнятся при нажатии на кнопку OK метода (в нашем случаи это функция goBack()):

webix.alert({  title: "Order is successfull",  text: "We will send you an information to confirm your Order"}).then(function(){  goBack();});

Функцию goBack() мы также устанавливаем в качестве обработчика на кнопку Go Back. Она очищает валидацию с помощью метода clearValidation(), изменяет лейбл тулбара, а также возвращает нас в модуль с таблицей рейсов и формой поиска:

function goBack(){const order_form = $$("flight_booking_form"); //получаем доступ к формеconst label = $$("label"); //получаем доступ к лейблу на тулбареorder_form.clearValidation(); //очищаем валидациюlabel.define("label", "Flight Search"); //изменяем значение лейблаlabel.refresh(); //обновляем лейбл$$("flight_search").show(); //отображаем лейаут с таблицей рейсов и формой поиска}

Заключение

С кодом готового приложения можно ознакомиться тут.

В этой статье мы детально разобрали, как создать приложение с использованием Webix. Научились создавать лейауты и описывать UI компоненты с помощью json синтаксиса. Косвенно затронули некоторые методы, которые позволяют работать с данными. Как вы могли убедиться, компоненты и методы интуитивно понятны и просты в использовании. На самом деле, это только малая толика всего, что может предложить нам библиотека. Для более детального ознакомления можно перейти к документации, в которой есть подробное описание всех возможностей с наглядными примерами.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru