Last Updated: February 25, 2016
·
1.255K
· kelluvuus

Integrating Webix and Struts Website with Database

It is the last part of the tutorial that tells about developing a website by using Webix UI library and Java framework Struts 2. If you haven’t read the previous parts yet check the first first and the secondparts in the Webix blog.

Nowadays nobody is interested in static data. That’s why our website should be able to download a list of events and reports from database and save all the implemented changes. We will use the most popular database MySQL.

The database structure is simple. It is represented in the image below. We will use two tables for storing events and reports. Each record in the table “speakers” contains the event_id a certain report is related to.

Picture

Use the following requests to create the database:

CREATE TABLE IF NOT EXISTS `events` (
 `id` bigint(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) NOT NULL,
 `description` text NOT NULL,
 `date` date NOT NULL,
 `location` varchar(255) NOT NULL,
  `photo` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ;

 INSERT INTO `events` (`id`, `name`, `description`, `date`, `location`, `photo`) VALUES

 (1, 'Front-end #1', 'We will consider different aspects of web app development: Promises, AntiAliasing, HTML-import, using DevTools at the upcoming event in detail! We are waiting for you! ', '2014-06-06', 'Minsk,Сentralnaya str.1, Blue conference hall', 'frontend1.png'),
(2, 'Front-end #2', 'Interesting reports on Front-end development that will be presented by real gurus of JavaScript-programming!', '2014-06-20', 'Minsk,Сentralnaya str.1, Blue conference hall', 'frontend2.png'),
 (3, 'Front-end #3', 'Interesting reports on Front-end development that will be presented by real gurus of JavaScript-programming!', '2014-07-04', 'Minsk,Сentralnaya str.1, Blue conference hall', 'frontend3.png'),
(4, 'Front-end #4', 'Interesting reports on Front-end development that will be presented by real gurus of JavaScript-programming!', '2014-07-18', 'Minsk,Сentralnaya str.1, Blue conference hall', 'frontend4.png'),
 (5, 'Front-end #5', 'Interesting reports on Front-end development that will be presented by real gurus of JavaScript-programming!', '2014-08-01', 'Minsk,Сentralnaya str.1, Blue conference hall', 'frontend5.png'),
 (6, 'Front-end #6', 'Interesting reports on Front-end development that will be presented by real gurus of JavaScript-programming!', '2014-08-15', 'Minsk,Сentralnaya str.1, Blue conference hall', 'frontend6.png'),

 CREATE TABLE IF NOT EXISTS `speakers` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
 `author` varchar(255) NOT NULL,
   `topic` varchar(255) NOT NULL,
  `photo` varchar(255) NOT NULL,
  `event_id` bigint(20) NOT NULL,
  `description` text NOT NULL,
  PRIMARY KEY (`id`)
 ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ;

 INSERT INTO `speakers` (`id`, `author`, `topic`, `photo`, `event_id`, `description`) VALUES
 (1, 'Iron Man' , 'JavaScript Promises - There and Back Again', 'ironman.jpg', 1, 'One of the new features that browser developers are preparing for us together with the developers who write specifications of JavaScript Promises is that this template of writing asynchronous code popular with lots of users gets native support. What’s the point of Promises and how to deal with them?'),

(2, 'Hulk', 'Avoiding Unnecessary Re-rendering', 'halk.jpg', 2, 'Rendering elements for a site or an application can take too much time and may have a negative impact on performance. We will consider the reasons for re-rendering in a browser and learn how to avoid unnecessary calls.'),
(3, 'Spider-Man', 'Using Your Terminal from the DevTools', 'spiderman.jpg', 1, 'DevTools Terminal is a new Chrome extension which provides the command-line functionality right in your browser.'),
(4, 'Thor', 'High Performance Animations', 'thor.jpg', 2, 'Diving deep into getting faster animations in your projects. We'll discover why modern browsers can easily animate the following properties: position, scale, rotation, opacity.'),
(5, 'Batman', 'AntiAliasing. Basics', 'batman.jpg', 1, 'An introduction to antialiasing, explaining how vector shapes and text are rendered smoothly.'),
(6, 'Captain America', 'HTML-Import', 'captainamerica.jpg', 1, 'HTML-import is a way of including some HTML documents into others. You’re not limited to markup either. You can also include CSS, JavaScript or anything else an .html file can contain.');

Hibernate is frequently used for working with database in Java. Hibernate is an object-relational mapping library for the Java language. To put it simply, we work with database records as with objects while Hibernate is responsible for requests, saving and loading.

To start using Hibernate you need to add hibernate-core and mysql-driver mysql-connector-java dependencies into the file pom.xml:

<dependencies>
    …
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>4.3.5.Final</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version>
    </dependency>
<dependencies>

Thereafter you need to perform “Maven- Update project” to make Maven download the new libraries.

Let’s create a configuration file src/main/resources/hibernate.cfg.xml:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

 <hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/myapp</property>
         <property name="hibernate.connection.useUnicode">true</property>
        <property name="hibernate.connection.characterEncoding">UTF-8</property>
         <property name="hibernate.connection.charSet">UTF-8</property>
        <property name="connection.username">root</property>
        <property name="connection.password"></property>
        <property name="connection.pool_size">1</property>
        <property name="dialect">org.hibernate.dialect.MySQLDialect</property>
       <property name="current_session_context_class">thread</property>
       <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
       <property name="show_sql">true</property>
       <property name="hbm2ddl.auto">validate</property>

       <mapping class="com.myapp.model.Event" />
    <mapping class="com.myapp.model.Speaker" />
    </session-factory>
 </hibernate-configuration>

We configure connection with the database in this file:

  • connection.url – the connection string to the database in the format jdbc:driver://host:port/dbname.
  • connection.username – database username
  • connection.password – database user password

We also specify the classes that will be linked to the database in the file src/main/resources/hibernate.cfg.xml. In our case these classes are Event and Speaker. It means that one object of the Event class will correspond to one record in the database table “events”. The same logic is applied to the class Speaker.

The Event class already exists but it needs to be edited. You should add annotations to specify which database table the class will be bound to and how the class properties relate to the table columns:

 package com.myapp.model;

 import java.util.Date;

 import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

 import org.apache.struts2.json.annotations.JSON;

@Entity
@Table(name="events")
 public class Event {

private Long id;
private String name;
private String description;
private Date date;
private String location;
private String photo;

public Event() {

}

public Event(Long id, String name, String description, Date date, String location, String photo) {
    this.id = id;
    this.name = name;
    this.description = description;
    this.date = date;
    this.location = location;
    this.photo = photo;
}

@Id
@GeneratedValue
@Column(name="id")
public Long getId() {
    return id;
}
public void setId(Long id) {
    this.id = id;
}

@Column(name="name")
public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

@Column(name="description", columnDefinition="TEXT")
public String getDescription() {
    return description;
}

public void setDescription(String description) {
    this.description = description;
}

@Column(name="date")
@JSON(format = "yyyy-MM-dd")
public Date getDate() {
    return date;
}

public void setDate(Date date) {
    this.date = date;
}

@Column(name="location")
public String getLocation() {
    return location;
}

public void setLocation(String location) {
    this.location = location;
}

@Column(name="photo")
public String getPhoto() {
    return photo;
}

public void setPhoto(String photo) {
    this.photo = photo;
}

}

The annotation @Table(name=”events”) before the class declaration specifies the table that will be used while the annotation @Column(name=”location”) before the property “location” specifies which database field corresponds to this property.

Let’s create the class Speaker by analogy:

 package com.myapp.model;

 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.GeneratedValue;
 import javax.persistence.Id;
 import javax.persistence.Table;

@Entity
@Table(name="speakers")
 public class Speaker {

private Long id;
private String author;
private String topic;
private String description;
private String photo;
private Long event_id;

public Speaker() {

}

 public Speaker(Long id, String author, String topic, String description, String photo, Long event_id) {
    this.id = id;
    this.author = author;
    this.topic = topic;
    this.description = description;
    this.photo = photo;
    this.event_id = event_id;
}

@Id
@GeneratedValue
@Column(name="id")
public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

@Column(name="author")
public String getAuthor() {
    return author;
}

public void setAuthor(String author) {
    this.author = author;
}

@Column(name="topic")
public String getTopic() {
    return topic;
}

public void setTopic(String topic) {
    this.topic = topic;
}

@Column(name="description", columnDefinition="TEXT")
public String getDescription() {
    return description;
}

public void setDescription(String description) {
    this.description = description;
}

@Column(name="photo")
public String getPhoto() {
    return photo;
}

public void setPhoto(String photo) {
    this.photo = photo;
}

@Column(name="event_id")
public Long getEvent_id() {
    return event_id;
}

public void setEvent_id(Long event_id) {
    this.event_id = event_id;
}   
}  

To work with Hibernate you need a class that will create a session factory or return an existing session. Create this class in the package com.myapp.util and name it HibernateUtil:

package com.myapp.util;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

 public class HibernateUtil {

private static final SessionFactory sessionFactory = buildSessionFactory();
private static SessionFactory buildSessionFactory() {
    try {
        return new Configuration()
            .configure() // configures settings from hibernate.cfg.xml
            .buildSessionFactory();
    } catch (Throwable ex) {
        System.err.println("Initial SessionFactory creation failed." + ex);
        throw new ExceptionInInitializerError(ex);
    }
 }
 public static SessionFactory getSessionFactory() {
    return sessionFactory;
 }
 }

For manipulating database objects we should create the classes EventsManager and SpeakersManager. We will place these classes in the package com.myapp.controller. These classes are responsible for: selecting a list of events/reports (the list method), selecting the last three records (the lastList method), selecting records by an identifier (getById), adding a new record (the insert method), editing existing records (the update method), deleting a record (the delete method).

EventsManager.java:

package com.myapp.controller;

 import java.util.List;
 import org.hibernate.HibernateException;
 import org.hibernate.Session;

 import com.myapp.model.Event;
 import com.myapp.util.HibernateUtil;

  public class EventsManager extends HibernateUtil {

public List<Event> list() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Event> events = null;
    try {
        events = (List<Event>)session.createQuery("from Event").list();
    } catch (HibernateException e) {
        e.printStackTrace();
        session.getTransaction().rollback();
    }
    session.getTransaction().commit();
    return events;
}

public Event getById(Long eventId) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    Event event = (Event) session.get(Event.class, eventId);
    session.getTransaction().commit();
    return event;
}

public Event update(Event event) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.update(event);
    session.getTransaction().commit();
    return event;
}

public Event delete(Event event) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.delete(event);
    session.getTransaction().commit();
    return event;
}

public Event insert(Event event) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.save(event);
    session.getTransaction().commit();
    return event;
}
}
Speakers.java:

 package com.myapp.controller;

import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;

import com.myapp.model.Speaker;
import com.myapp.util.HibernateUtil;

public class SpeakersManager extends HibernateUtil {

public List<Speaker> list() {
    return list(null);
}

public List<Speaker> list(Long eventId) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Speaker> speakers = null;
    try {
        Query query = session.createQuery("from Speaker" + (eventId != null ? " where event_id=:event_id" : ""));
        if (eventId != null) {
            query.setParameter("event_id", eventId);
        }
        speakers = (List<Speaker>) query.list();
    } catch (HibernateException e) {
        e.printStackTrace();
        session.getTransaction().rollback();
    }
    session.getTransaction().commit();
    return speakers;
   }

   public List<Speaker> lastList() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Speaker> speakers = null;
    try {
        Query query = session.createQuery("from Speaker S ORDER BY S.id DESC");
        query.setMaxResults(3);
        speakers = (List<Speaker>) query.list();
    } catch (HibernateException e) {
        e.printStackTrace();
        session.getTransaction().rollback();
    }
    session.getTransaction().commit();
    return speakers;
   }

     public Speaker update(Speaker speaker) {
      Session session = HibernateUtil.getSessionFactory().getCurrentSession();
      session.beginTransaction();
      session.update(speaker);
      session.getTransaction().commit();
      return speaker;
   }

     public Speaker delete(Speaker speaker) {
     Session session = HibernateUtil.getSessionFactory().getCurrentSession();
     session.beginTransaction();
     session.delete(speaker);
     session.getTransaction().commit();
     return speaker;
   }

   public Speaker insert(Speaker speaker) {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    session.save(speaker);
    session.getTransaction().commit();
    return speaker;
 }
 }

The data is loaded into Webix components via ajax-requests. We should use the option url of the DataTable component to load data from the server. This option specifies the URL that defines what url the ajax-request should be sent to for data downloading. The response from the server can be in XML, JSON, CSV formats. The datatype option indicates the expected data format.

We will use responses in JSON format. There is the plugin “struts2-json-plugin”for Struts 2 that allows sending a response in JSON format. We added this plugin when we were configuring Struts 2 dependencies in the file pom.xml.

To download data we will use the following paths:

Let’s configure the paths in the file struts.xml:

<package name="default" namespace="/" extends="json-default">
<action name="events" class="com.myapp.action.EventAction" method="getEvents">
   <result type="json" />
</action>
<action name="speakers" class="com.myapp.action.SpeakerAction" method="getSpeakers">
   <result type="json" />
</action>
<action name="lastSpeakers" class="com.myapp.action.SpeakerAction" method="getLastSpeakers">
   <result type="json" />
</action>
</package>

To prevent Struts 2 from sending a view in response to request and make it return data in JSON format we should add the above mentioned paths into another package that has the value extends=”json-default”. In this case Struts 2 will serialize either EventAction or SpeakerAction object and will send it as a response.

Let’s edit the EventAction class so that it can return the list of events:

package com.myapp.action;

import java.util.ArrayList;
import java.util.List;

import com.myapp.controller.EventsManager;
import com.myapp.model.Event;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

public class EventAction extends ActionSupport {

private List<Event> data = new ArrayList<Event>();
private String eventId;
private Event event = null;

public String getEventById() {
    if (eventId != null) {
        EventsManager eventsManager = new EventsManager();
        event = eventsManager.getById(Long.parseLong(eventId, 10));
        return Action.SUCCESS;
    } else {
        return Action.ERROR;
    }

}

public String getEvents() {
    EventsManager eventsManager = new EventsManager();
    data = eventsManager.list();
    return Action.SUCCESS;
}

public List<Event> getData() {
    return data;
}
public void setData(List<Event> lists) {
    this.data = lists;
}

public String getEventId() {
    return eventId;
}
public void setEventId(String eventId) {
    this.eventId = eventId;
}

public Event getEvent() {
    return event;
}
public void setEvent(Event event) {
    this.event = event;
}
}

Then we should create the SpeakerAction class which returns the list of events:

SpeakerAction.java:

 package com.myapp.action;

 import java.util.ArrayList;
 import java.util.List;

 import com.myapp.controller.SpeakersManager;
 import com.myapp.model.Speaker;
 import com.opensymphony.xwork2.Action;
 import com.opensymphony.xwork2.ActionSupport;

 public class SpeakerAction extends ActionSupport {

private List<Speaker> data = new ArrayList<Speaker>();
private String eventId;

public String getSpeakers() {
    SpeakersManager speakersManager = new SpeakersManager();
    if (eventId != null) {
        data = speakersManager.list(Long.parseLong(eventId, 10));
    } else {
        data = speakersManager.list();
    }
    return Action.SUCCESS;
}

public String getLastSpeakers() {
    SpeakersManager speakersManager = new SpeakersManager();
    data = speakersManager.lastList();
    return Action.SUCCESS;
}

public List<Speaker> getData() {
    return data;
}
public void setData(List<Speaker> lists) {
    this.data = lists;
}

public String getEventId() {
    return eventId;
}
public void setEventId(String eventId) {
    this.eventId = eventId;
}
 }

After that open the page http://localhost:8080/MyApp/events in a browser and check the events in JSON format:

Picture

To begin using real data instead of the test ones we need to make small changes in the settings of the components:

  • on the page index.jsp replace:
    data: events
    with

    datatype:"json",
    url:"events?nocache=" + (new Date()).valueOf()

  • in the file myapp.js change:
    data: lastSpeakers
    with

    datatype: "json",
    url: "lastSpeakers?nocache=" + (new Date()).valueOf()

  • in the file add.jsp replace:
    data: events
    into

    datatype: "json",
    url: "events?nocache=" + (new Date()).valueOf()

and

$$("speakers").parse(speakers);

with

$$("speakers").load("speakers?nocache=" + (new Date()).valueOf());
  • in the file event.jsp change:
    data: speakers
    into

    datatype: "json",
    url:"speakers?eventId=<s:property value="event.id" />&nocache=" + (new Date()).valueOf()

Now data in the tables are not the test data from tempdata.js file (you can already delete this file) but real information from the database!

To add the opportunity of saving events and reports you should create two separate classes: SaveEventAction and SaveSpeakerAction.

SaveEventAction:

package com.myapp.action;

import java.util.Date;
import java.util.Locale;

import com.myapp.controller.EventsManager;
import com.myapp.model.Event;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

import java.text.ParseException;
import java.text.SimpleDateFormat;

public class SaveEventAction extends ActionSupport {

private String id;
private String name;
private String description;
private String date;
private String location;
private String photo;
private String webix_operation;

public String saveEvent() {
    Event event = new Event();
    event.setId(id!=null ? Long.parseLong(id, 10) : null);
    event.setName(name);
    event.setDescription(description);
    Date eventDate = null;
    try {
        eventDate = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date);
    } catch (ParseException e) {
        e.printStackTrace();
    }
    event.setDate(eventDate);
    event.setLocation(location);
    event.setPhoto(photo);

    EventsManager eventsManager = new EventsManager();
    if (webix_operation.equals("update"))
        event = eventsManager.update(event);
    else if (webix_operation.equals("delete"))
        event = eventsManager.delete(event);
    else if (webix_operation.equals("insert"))
        event = eventsManager.insert(event);

    id = event.getId().toString();
    return Action.SUCCESS;
}

public String getId() {
    return id;
}

public void setId(String id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getDescription() {
    return description;
}

public void setDescription(String description) {
    this.description = description;
}

public String getDate() {
    return date;
}

public void setDate(String date) {
    this.date = date;
}

public String getLocation() {
    return location;
}

public void setLocation(String location) {
    this.location = location;
}

public String getPhoto() {
    return photo;
}

public void setPhoto(String photo) {
    this.photo = photo;
}

public String getWebix_operation() {
    return webix_operation;
}

public void setWebix_operation(String webix_operation) {
    this.webix_operation = webix_operation;
}

}
SaveSpeakerAction.java:

package com.myapp.action;

import java.util.Date;
import java.util.Locale;

import com.myapp.controller.EventsManager;
import com.myapp.controller.SpeakersManager;
import com.myapp.model.Event;
import com.myapp.model.Speaker;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

import java.text.ParseException;
import java.text.SimpleDateFormat;

public class SaveSpeakerAction extends ActionSupport {

private String id;
private String author;
private String topic;
private String description;
private String photo;
private String event_id;
private String webix_operation;

public String saveSpeaker() {
    Speaker speaker = new Speaker();
    speaker.setId(id!=null ? Long.parseLong(id, 10) : null);
    speaker.setAuthor(author);
    speaker.setTopic(topic);
    speaker.setDescription(description);
    speaker.setPhoto(photo);
    speaker.setEvent_id(Long.parseLong(event_id, 10));
    Date eventDate = null;

    SpeakersManager speakersManager = new SpeakersManager();
    if (webix_operation.equals("update"))
        speaker = speakersManager.update(speaker);
    else if (webix_operation.equals("delete"))
        speaker = speakersManager.delete(speaker);
    else if (webix_operation.equals("insert"))
        speaker = speakersManager.insert(speaker);

    id = speaker.getId().toString();
    return Action.SUCCESS;
}

public String getId() {
    return id;
}

public void setId(String id) {
    this.id = id;
}

public String getAuthor() {
    return author;
}

public void setAuthor(String author) {
    this.author = author;
}

public String getTopic() {
    return topic;
}

public void setTopic(String topic) {
    this.topic = topic;
}

public String getDescription() {
    return description;
}

public void setDescription(String description) {
    this.description = description;
}

public String getPhoto() {
    return photo;
}

public void setPhoto(String photo) {
    this.photo = photo;
}

public String getEvent_id() {
    return event_id;
}

public void setEvent_id(String event_id) {
    this.event_id = event_id;
}

public String getWebix_operation() {
    return webix_operation;
}

public void setWebix_operation(String webix_operation) {
    this.webix_operation = webix_operation;
}
}

All the values included into the request are automatically set into the Action class variables with the appropriate names. Thus, for saving a new event it is enough to create a new object new Event(), set the accepted values into it and pass this object to the method EventsManager.insert(…).

The request includes not only information on an event but also the webix_operation parameter which defines what action that should be performed with an event.

Let’s add the settings into the struts.xml file into package json-default:

<action name="saveEvent" class="com.myapp.action.SaveEventAction" method="saveEvent">
<result type="json" />
</action>
<action name="saveSpeaker" class="com.myapp.action.SaveSpeakerAction"     method="saveSpeaker">
<result type="json" />
</action>

We should add the save property for tables into the page add.jsp:

{
id: "events",
view:"datatable",
columns:[
    { id:"date",    header:"Date" , width:80 },
    { id:"name",    header:"Name", fillspace: true },
    { id:"location",header:"Location",  width:400 },
    { id:"edit",header:"", width: 34, template: "<span class='webix_icon fa-edit editEvent control'></span>"},
    { id:"remove", header:"<span class='webix_icon fa-plus bigControl' onclick='addEventClick();'></span>", width: 34, template: "<span class='webix_icon fa-trash-o removeEvent control'></span>"}
  ],
    onClick: {
            removeEvent: removeEventClick,
            editEvent: editEventClick
    },
    autoheight:true,
    select:"row",
    datatype: "json",
    url: "events?nocache=" + (new Date()).valueOf(),
    save: "saveEvent"
},

{ width: 10 },
{
id: "speakers",
view:"datatable",
columns:[
    { id:"author",  header:"Author", width:150 },
    { id:"topic",   header:"Topic", width:300 },
    { id:"edit",header:"", width: 34, template: "<span class='webix_icon fa-edit editSpeaker control'></span>"},
    { id:"remove", header:"<span class='webix_icon fa-plus bigControl' onclick='addSpeakerClick();'></span>", width: 34, template: "<span class='webix_icon fa-trash-o removeSpeaker control'></span>"}
],
onClick: {
    removeSpeaker: removeSpeakerClick,
    editSpeaker: editSpeakerClick
},
select: "row",
autoheight:true,
autowidth:true,
datatype: "json",
save: "saveSpeaker"
}

That’s all! Now we have a ready website which has several pages.

You can download a ready demo of the app

In conclusion, it should be noted that Webix has proved to be a handy tool for fast website development.
It allows you to build not only static pages but also full-fledged data management systems. Moreover, Webix components can be easily customized, integrated with other libraries and various technologies. The library also gives the possibility to mark up a page in usual HTML, which saves a lot of time.