Статьи

Создание страниц и форм веб-сайта с помощью Struts и пользовательского интерфейса Webix

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

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

Страница «Предстоящие события»

Сначала мы создадим файл codebase / tempdata.js, который будет содержать тестовые события.

var events = [{
    id: "1",
    name: "Front-end #1",
    description: "Part 1: -  Task description. (Creating a service for photographers. Processing and developing of proprietary RAW formats: NEF, CR2, DNG, creating a photo editor in a browser, Collaboration service based on WebRTC)",
    date: "2014-06-06",
    location: "Minsk, Centralnaya str., 1, Blue conference hall",
    photo: "frontend1.png"
}, {
    id: "2",
    name: "Front-end #2",
    description: "Technical master class: Developing JavaScript applications",
    date: "2014-06-20",
    location: "Minsk, Centralnaya str., 1, Blue conference hall",
    photo: "frontend2.png"
}, {
    id: "3",
    name: "Front-end #3",
    description: "Technical master class: Developing JavaScript applications",
    date: "2014-07-04",
    location: "Minsk, Centralnaya str., 1, Blue conference hall",
    photo: "frontend3.png"
}, {
    id: "4",
    name: "Front-end #4",
    description: "Technical master class: Developing JavaScript applications",
    date: "2014-07-18",
    location: "Minsk, Centralnaya str., 1, Blue conference hall",
    photo: "frontend4.png"
}, {
    id: "5",
    name: "Front-end #5",
    description: "Technical master class: Developing JavaScript applications",
    date: "2014-08-01",
    location: "Minsk, Centralnaya str., 1, Blue conference hall",
    photo: "frontend5.png"
}, {
    id: "6",
    name: "JS full-stack - all the things you can create with JS",
    description: "Technical master class: Developing JavaScript applications",
    date: "2014-08-15",
    location: "Minsk, Centralnaya str., 1, Blue conference hall",
    photo: "frontend6.png"
}];

Обращать внимание! Не забудьте поменять кодировку файла на UTF-8!

Теперь мы должны включить файл  tempdata.js  в  index.jsp :

<script src="./codebase/tempdata.js" type="text/javascript" charset="utf-8"></script>

Затем мы редактируем конфигурацию контента:

{ cols: [
    { body: {
        view:"list",
        template:"<img src='./photos/#photo#' class='eventpic' /> <h3>#name# (#date#)</h3>#description# <a href='event?eventId=#id#' class='details'>Details</a>",
        css: "eventsList",
        type:{
            height:152
        },
        select:false,
        autoheight: true,
        data: events
    }},
    { header: "Latest reports", body: {
        template: "The list of latest records"
    }, width: 409 }
]},

Таким образом, мы разделили клетку на две части. Правая часть будет содержать список последних отчетов, а центральная часть представит список предстоящих событий. Последний список будет реализован с помощью компонента «список». Каждый элемент списка имеет высоту 152 пикселя и имеет шаблон, установленный свойством «template». Опция  select : false отключает выбор элементов списка. Нам также нужно добавить 150 * 150 пикселей изображения для каждого из отчетов.

Мы добавим несколько правил форматирования для списка предстоящих событий в файл  myapp.css:

.eventpic {
    width: 120px;
    height: 120px;
    display: block;
    float: left;
    margin: 7px;
    border: 1px solid #dddddd;
}
.details {
    position: absolute;
    right: 10px;
    bottom: 10px;
}
.eventsList .webix_list_item {
    position: relative;
    padding: 7px;
}
.eventsList .webix_list_item h3 {
    margin: 4px 0px 0px 0px;
}

Давайте добавим конфигурацию списка с последними отчетами в файл myapp.js, указав функцию
getLastSpeakersList :

function getLastSpeakersList() {
    return {
        id:"tweets",
        view:"list",
        template:"<img src='./photos/#photo#' class='eventpic' /><span class='speakerHeader'>#author# - #topic#</span><br><span class='speakerDetails'>#description#</span>",
        type:{
            height:190
        },
        select:false,
        data: lastSpeakers
    };
}

Мы также добавляем временные данные для списка «Последние отчеты» в файл tempdata.js:

var lastSpeakers = [{
    "id":6,
    "event_id":1,
    "author":"Captain America",
    "photo":"captainamerica.jpg",
    "description":"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",
    "topic":"HTML-import"
},{
    "id":5,
    "event_id":1,
    "author":"Batman",
    "photo":"batman.jpg",
    "description":"An introduction to antialiasing, explaining how vector shapes and text are rendered smoothly.",
    "topic":"AntiAliasing. Basics."
},{
    "id":4,
    "event_id":2,
    "author":"Thor",
    "photo":"thor.jpg",
    "description":"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.",
    "topic":"High Performance Animations"
}];

Поместите изображения 120 * 120 пикселей для отчетов в папку «фотографии»: batman.jpg, captainamerica.jpg, halk.jpg, ironman.jpg, spiderman.jpg, thor.jpg.

Добавьте стили для списка отчетов в файл  myapp.css.

.speakerHeader {
    line-height: 20px;
    font-weight: bold;
}
.speakerDetails {
    line-height: 20px;
    text-overflow: ellipsis;
}

Осталось только исправить конфигурацию, определенную в файле index.jsp: 

    

{ header: "Latest reports", body: getLastSpeakersList(), width: 409 }

Теперь запустите сервер и откройте страницу
http: // localhost: 8080 / MyApp / index.action в браузере. Ну, страница предстоящих событий готова.

Эта же страница будет отображаться по следующему адресу для удобного именования пунктов меню. Для этого необходимо добавить необходимые настройки в файл struts.xml:

<package name="defaultpages" namespace="/" extends="struts-default">
    <action name="index">
        <result>/index.jsp</result>
    </action>
    <action name="upcoming">
        <result>/index.jsp</result>
    </action>
    …

Перезагрузите сервер. Теперь главная страница сайта доступна по ссылке
http: // localhost: 8080 / MyApp / upcoming.

Страница «Контакты»

Давайте добавим отображение в файл struts.xml:

<package name="defaultpages" namespace="/" extends="struts-default">
    ...
    <action name="contacts">
        <result>/contacts.jsp</result>
    </action>
</package>

Теперь нам нужно создать файл
src / main / webapp / contacts.jsp, который отвечает за отображение страницы «Контакты»:

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
    <title>Webix - Front-End Events</title>
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="stylesheet" href="./codebase/webix.css" type="text/css" media="screen" charset="utf-8">
    <link rel="stylesheet" href="./codebase/myapp.css" type="text/css" media="screen" charset="utf-8">
    <script src="./codebase/webix.js" type="text/javascript" charset="utf-8"></script>
    <script src="./codebase/myapp.js" type="text/javascript" charset="utf-8"></script>
    <script src="./codebase/tempdata.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
    <jsp:include page="header.jsp" />
    <jsp:include page="footer.jsp" />
    <div id="contacts" style="display: none;">
        <span class='webix_icon fa-envelope-o'></span> 
        <a href="mailto:dmitry.radyno@gmail.com">dmitry.radyno@gmail.com</a><br>
        <span class='webix_icon fa-phone-square'></span> +375 29 123-45-67<br>
        <span class='webix_icon fa-twitter'></span> 
        <a href="https://twitter.com/radyno">@radyno</a><br>
        <span class='webix_icon fa-facebook'></span> 
        <a href="https://www.facebook.com/dmitry.radyno">Dmitry Radyno</a><br>
    </div>
    <div id='myapp'></div>

    <script type="text/javascript" charset="utf-8">
        webix.ui({
            container:"myapp",
            cols: [{}, {
                width: 1280,
                rows: [
                    {
                        height: 250,
                        borderless:true,
                        cols: [{
                            rows: [
                                { view: "template", template: "html->header", css: "header", borderless: true},
                                getTopMenu("contacts")
                            ]},
                            getPhotos()
                        ]},
                        { height: 400, cols: [
                            { template: "html->contacts", borderless: true, css: "contacts" },
                            { header: "Latest reports", body: getLastSpeakersList(), width: 409 }
                        ]},
                        getFooter()
                ]
            }, {}]
        });
        </script>
</body>
</html>

Затем мы добавляем стили для страницы «Контакты» в файл
src / main / weapp / codebase / myapp.css :

.contacts {
    padding: 40px 40px 40px 72px;
    box-sizing: border-box;
    font-size: 20px;
    line-height: 30px;
}
.contacts .webix_icon {
    font-size: 23px;
    color: #580B1C;
    text-align: center;
}
.contacts a {
    color: #580B1C;
}

Откройте в браузере следующую страницу — http: // localhost: 8080 / MyApp / contacts:

Откройте следующую страницу в браузере

Страница «Контакты» заполнена!

Информация о предстоящем событии

For displaying information about a coming event we’ll use the page event. An event identifier is passed as the parameter eventId in the URL of the page: http://localhost:8080/MyApp/event?eventId=1.

Let’s configure mapping in the file struts.xml. You should take into account that the content of this page will depend on the parameter eventId, which means that specifying the resulting view alone is not enough. It is necessary to look for a suitable event which has the above mentioned identifier in the database. The class EventAction will be responsible for this search:

<action name="event" class="com.myapp.action.EventAction" method="getEventById">
    <result>/event.jsp</result>
</action>

Таким образом, мы определили, что класс
com.myapp.action.EventAction и метод
getEventById будут использоваться для генерации динамического содержимого страницы. Файл
event.jsp будет отвечать за визуализацию данных. Далее мы создаем пакет
com.myapp.model и класс
Event в нем. Не забудьте создать папку
main / src / java, если она не создана автоматически. Этот класс представляет предстоящее событие:

package com.myapp.model;
import java.util.Date;
import org.apache.struts2.json.annotations.JSON;

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;
    }

    public Long getId() {
        return id;
    }
    public void setId(Long 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;
    }

    @JSON(format = "yyyy-MM-dd")
    public Date getDate() {
        return date;
    }
    public void setDate(Date 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;
    }
}

После этого мы создаем пакет
com.myapp.action с классом
EventAction внутри. Этот класс унаследован от класса
ActionSupport :

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

package com.myapp.action;
import java.util.Date;
import com.myapp.model.Event;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;

public class EventAction extends ActionSupport {
    private String eventId;
    private Event event = null;

    public String getEventById() {
        if (eventId != null) {
            event = new Event(new Long(1), "Front-end #1", "Part 1: -  Task description. (Creating a service for photographers. Processing and developing of proprietary RAW formats: NEF, CR2, DNG, creating a photo editor in a browser, Collaboration service based on WebRTC)", new Date(), "Minsk, Centralnaya str., 1, Blue conference hall", "frontend1.png");
            return Action.SUCCESS;
        } else {
            return Action.ERROR;
        }
    }
    public Event getEvent() {
        return event;
    }
    public void setEvent(Event event) {
        this.event = event;
    }
    public String getEventId() {
        return eventId;
    }
    public void setEventId(String newEventId) {
        eventId = newEventId;
    }

}

На данный момент интеграция с базой данных отсутствует, поэтому один и тот же статический объект будет возвращаться постоянно, независимо от запрошенного события. Позже мы изменим метод 
getEventById таким образом, чтобы он возвращал запись с идентификатором «eventId». Сейчас мы создадим файл 
event.jsp :

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
    <title>Webix - Front-End Events</title>
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="stylesheet" href="./codebase/webix.css" type="text/css" media="screen" charset="utf-8">
    <link rel="stylesheet" href="./codebase/myapp.css" type="text/css" media="screen" charset="utf-8">
    <script src="./codebase/webix.js" type="text/javascript" charset="utf-8"></script>
    <script src="./codebase/myapp.js" type="text/javascript" charset="utf-8"></script>
    <script src="./codebase/tempdata.js" type="text/javascript" charset="utf-8"></script>s
</head>
<body>
    <div id="header" style="display: none;">
        <div class="confname"><s:property value="event.name" /></div>
        <div class="conftime"><s:date name="event.date" format="dd/MM/yyyy" /> - <s:property value="event.location" /></div>
        <div class="confdsc"><s:property value="event.description" /></div>
    </div>
    <jsp:include page="footer.jsp" />
    <div id='myapp'></div>

    <script type="text/javascript" charset="utf-8">
        webix.ui({
            container:"myapp",
            cols: [{}, {
                width: 1280,
                rows: [
                    {
                        height: 250,
                        borderless:true,
                        cols: [{
                            rows: [
                                { view: "template", template: "html->header", css: "header", borderless: true },
                                getTopMenu()
                            ]
                        },
                        getPhotos()
                    ]},
                    { cols: [
                        { body: {
                            view:"list",
                            template:"<img src='./photos/#photo#' class='userpic' /> <h3>#author#: #topic#</h3>#description#",
                            css: "speakersList",
                            type:{
                                height:170
                            },
                            select:false,
                            minHeight: 400,
                            autoheight: true,
                            data: speakers
                        } },
                        { header: "Latest reports", body: getLastSpeakersList(), width: 409 }
                    ]},
                    getFooter()
                ]
            }, {}]
        });
    </script>
</body>
</html>

In the above code snippet we’ve created two items of the type “datatable”. This element presents a table, for which it’s possible to define the list of columns that should be rendered in this table.

Webix tables support the binding operation, that is rendering data in a slave table depending on the row which is selected in a master table. In our case the master table is the list of events, and the slave table is the list of reports.

To bind the tables the following code is used:

$("speakers").bind($("events"), function(slave, master){
            if (!master) return false;
            return master.id == slave.event_id;
        });

Такая конструкция показывает, что таблица «докладчики» должна быть привязана к таблице «события». Это также показывает, что должны отображаться только те строки, для которых callback-функция будет возвращать true. В данном конкретном случае это строки, в которых speaker.event_id равен event.id. Давайте также добавим стили для форматирования значков, используемых для добавления, редактирования и удаления строк:

.control {
    padding-top: 6px;
    cursor: pointer;
}
.bigControl {
    cursor: pointer;
    color: #ffffff;
    font-size: 20px;
}

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

{
    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'></span>", width: 34, template: "<span class='webix_icon fa-trash-o removeEvent control'></span>"}
    ],
    onClick: {
        removeEvent: removeEventClick
    },
    autoheight:true,
    select:"row",
    data: events
},

removeEventClick into the JavaScript section of 
add.jsp file. It will delete a currently selected item from the list. To implement such a behaviour is really easy as Webix DataTable provides a very handy API for data processing:

function removeEventClick(ev, id, html){
    var event = this.getItem(id.row),
        self = this;

    webix.confirm({
        title:"Deleting an event",
        ok:"Yes, delete it", 
        cancel:"No, leave it",
        text:"Are you sure you want to delete the event " + event.name + "?",
        callback: function(result) {
            if (result) {
                self.remove(id);
            }
        }
    });
}

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

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

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

A separate form will appear in a modal window for adding/editing a row . By clicking the “Save” button the changes will be added into the table of events.

The editing form will be implemented as a separate function. Its first parameter is either an event object (in case of the change of an existing event) or null (in case of the creation of a new event). The second parameter is a function which will be called after the user has filled in the fields and clicked the “Save” button. So let’s create the file codebase/eventForm.js:

function editEvent(value, callback) {

    var saveEvent = function() {
        var newValue = $("eventForm").getValues();
        $("eventForm").clear();
        $("eventWindow").close();
        callback(newValue);
    };
    var discardChanges = function() {
        $("eventForm").clear();
        $("eventWindow").close();
    };

    webix.ui({
        id: "eventWindow",
        view:"window",
        height:420,
        width:400,
        position:"center",
        modal: true,
        head: "Editing an event",
        body:{
            rows: [{
                id: "eventForm",
                view:"form",
                elements:[
                    { view:"text", name:"name", label:"Name"},
                    { view:"datepicker", name:"date", label:"Date", stringResult: true },
                    { view:"text", name:"location", label:"Address"},
                    { view:"text", name:"photo", label:"Image"},
                    { view:"textarea", name:"description",height:170, label:"Description", labelPosition:"top" }
                ],
                rules:{
                    name: webix.rules.isNotEmpty,
                    description: webix.rules.isNotEmpty
                }
            }, { view:"toolbar", cols:[
                {},
                { view:"button", label:"Save", type:"form", click:saveEvent, width: 200 },
                { view:"button", label:"Cancel", click:discardChanges, width: 200 },
                {}
            ]}]
        }
    }).show();

    if (value) {
        $("eventForm").setValues(value);
    }
}

Теперь мы включаем этот файл на страницу add.jsp:

<script src="./codebase/eventForm.js" type="text/javascript" charset="utf-8"></script>

После этого мы добавляем onclick-обработчики для иконок добавления и редактирования событий:

{
    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",
    data: events
},

Давайте определим функции, которые будут вызывать форму добавления / редактирования событий:

function addEventClick() {
    editEvent(null, function(newValue) {
        $("events").add(newValue);
    });
}
function editEventClick(ev, id, html){
    editEvent(this.getItem(id.row), webix.bind(function(newValue) {
        this.updateItem(newValue.id, newValue);
    }, this));
}

Откройте страницу http: // localhost: 8080 / MyApp / add в браузере и попробуйте добавить новое событие или отредактировать существующее:

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

 speakerForm.js:
function editSpeaker(value, callback) {

    var saveSpeaker = function() {
        var newValue = $("speakerForm").getValues();
        $("speakerForm").clear();
        $("speakerWindow").close();
        callback(newValue);
    };
    var discardChanges = function() {
        $("speakerForm").clear();
        $("speakerWindow").close();
    };

    webix.ui({
        id: "speakerWindow",
        view:"window",
        height:420,
        width:400,
        position:"center",
        modal: true,
        head: "Editing a report",
        body:{
            rows: [{
                id: "speakerForm",
                view:"form",
                elements:[
                    { view:"text", name:"author", label:"Author"},
                    { view:"text", name:"topic", label:"Topic"},
                    { view:"text", name:"photo", label:"Photo"},
                    { view:"textarea", name:"description",height:200, label:"Description", labelPosition:"top" }
                ],
                rules:{
                    name: webix.rules.isNotEmpty,
                    description: webix.rules.isNotEmpty
                }
            }, { view:"toolbar", cols:[
                {},
                { view:"button", label:"Save", type:"form", click:saveSpeaker, width: 200 },
                { view:"button", label:"Cancel", click:discardChanges, width: 200 },
                {}
            ]}]
        }
    }).show();

    if (value) {
        $("speakerForm").setValues(value);
    }
}

add.jsp:

...
<script src="./codebase/speakerForm.js" type="text/javascript" charset="utf-8"></script>
…
{
    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
}

…

function removeSpeakerClick(ev, id, html){
    var topic = this.getItem(id.row),
        self = this;

    webix.confirm({
        title:"Deleting a report",
        ok:"Yes, delete it", 
        cancel:"No, leave it",
        text:"Are you sure you want to delete the report'" + topic.topic + "'?",
        callback: function(result) {
            if (result) {
                self.remove(id);
            }
        }
    });
}
function editSpeakerClick(ev, id, html){
    editSpeaker(this.getItem(id.row), webix.bind(function(newValue) {
        this.updateItem(newValue.id, newValue);
    }, this));
}
function addSpeakerClick() {
    editSpeaker({
        event_id: $('events').getSelectedId()
    }, function(newValue) {
        $("speakers").add(newValue);
    });
}

Conclusions

In the second part of the tutorial we’ve added some useful functionality to the website by using JavaScript UI library Webix and Java framework Struts.

Thus, we’ve created two important pages “Contacts” and “Forthcoming Events” as well as made it possible to add and manage the conference events. So we have an almost ready website with lots of functions and attractive appearance.

You can download the resulting project via this link. 

You’ll get the final information about developing a website with Webix and Struts in the third part of our tutorial that will be soon published in the Webix blog.