В предыдущей статье “Фреймворк 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"); //получаем доступ к таблице через ее localId
    this.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){
  …
  //создаем окно с формой, которое хранится в модуле WindowView
  this.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 значительно упростят процесс разработки приложения любой сложности.