Это обещанное продолжение Java EE 7 с Angular JS — часть 1 . Это заняло больше времени, чем я ожидал (чтобы найти время для подготовки кода и публикации в блоге), но это, наконец, здесь!
Приложение
Исходное приложение в части 1 — это всего лишь простой список с нумерацией страниц и службой REST, которая подает данные списка.
В этом посте мы собираемся добавить возможности CRUD (создание, чтение, обновление, удаление), привязать сервисы REST для выполнения этих операций на стороне сервера и проверить данные.
Настройка
Настройка аналогична части 1 , но вот список для справки:
Код
Бэкэнд — Java EE 7
Серверная часть не требует много изменений. Поскольку нам нужна возможность создавать, читать, обновлять и удалять, нам необходимо добавить соответствующие методы в службу REST для выполнения этих операций:
PersonResource
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
package com.cortez.samples.javaee7angular.rest; import com.cortez.samples.javaee7angular.data.Person; import com.cortez.samples.javaee7angular.pagination.PaginatedListWrapper; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.ws.rs.*; import javax.ws.rs.core.Application; import javax.ws.rs.core.MediaType; import java.util.List; @Stateless @ApplicationPath ( "/resources" ) @Path ( "persons" ) @Consumes (MediaType.APPLICATION_JSON) @Produces (MediaType.APPLICATION_JSON) public class PersonResource extends Application { @PersistenceContext private EntityManager entityManager; private Integer countPersons() { Query query = entityManager.createQuery( "SELECT COUNT(p.id) FROM Person p" ); return ((Long) query.getSingleResult()).intValue(); } @SuppressWarnings ( "unchecked" ) private List<Person> findPersons( int startPosition, int maxResults, String sortFields, String sortDirections) { Query query = entityManager.createQuery( "SELECT p FROM Person p ORDER BY " + sortFields + " " + sortDirections); query.setFirstResult(startPosition); query.setMaxResults(maxResults); return query.getResultList(); } private PaginatedListWrapper<Person> findPersons(PaginatedListWrapper<Person> wrapper) { wrapper.setTotalResults(countPersons()); int start = (wrapper.getCurrentPage() - 1 ) * wrapper.getPageSize(); wrapper.setList(findPersons(start, wrapper.getPageSize(), wrapper.getSortFields(), wrapper.getSortDirections())); return wrapper; } @GET public PaginatedListWrapper<Person> listPersons( @DefaultValue ( "1" ) @QueryParam ( "page" ) Integer page, @DefaultValue ( "id" ) @QueryParam ( "sortFields" ) String sortFields, @DefaultValue ( "asc" ) @QueryParam ( "sortDirections" ) String sortDirections) { PaginatedListWrapper<Person> paginatedListWrapper = new PaginatedListWrapper<>(); paginatedListWrapper.setCurrentPage(page); paginatedListWrapper.setSortFields(sortFields); paginatedListWrapper.setSortDirections(sortDirections); paginatedListWrapper.setPageSize( 10 ); return findPersons(paginatedListWrapper); } @GET @Path ( "{id}" ) public Person getPerson( @PathParam ( "id" ) Long id) { return entityManager.find(Person. class , id); } @POST public Person savePerson(Person person) { if (person.getId() == null ) { Person personToSave = new Person(); personToSave.setName(person.getName()); personToSave.setDescription(person.getDescription()); personToSave.setImageUrl(person.getImageUrl()); entityManager.persist(person); } else { Person personToUpdate = getPerson(person.getId()); personToUpdate.setName(person.getName()); personToUpdate.setDescription(person.getDescription()); personToUpdate.setImageUrl(person.getImageUrl()); person = entityManager.merge(personToUpdate); } return person; } @DELETE @Path ( "{id}" ) public void deletePerson( @PathParam ( "id" ) Long id) { entityManager.remove(getPerson(id)); } } |
Код точно такой же, как обычный Java POJO, но использует аннотации Java EE для улучшения поведения. @ApplicationPath("/resources")
и @Path("persons")
предоставят службу REST по URL-адресу yourdomain/resources/persons
@Path("persons")
будет хостом, на котором выполняется приложение). @Consumes(MediaType.APPLICATION_JSON)
и @Produces(MediaType.APPLICATION_JSON)
принимают и форматируют запрос и ответ REST как JSON.
Для операций REST:
Аннотация / HTTP метод | Java-метод | URL | Поведение |
---|---|---|---|
@GET / GET |
listPersons | Http: // Имя_домена / ресурсы / чел | Возвращает нумерованный список из 10 человек. |
@GET / GET |
getPerson | Http: // имя_домена / ресурсы / чел / {ID} | Возвращает сущность Person по ее идентификатору. |
@POST / POST |
savePerson | Http: // Имя_домена / ресурсы / чел | Создает или обновляет человека. |
@DELETE / DELETE |
deletePerson | Http: // имя_домена / ресурсы / чел / {ID} | Удаляет сущность Person по ее идентификатору. |
URL, вызываемый для каждой операции, очень похож. Магия, позволяющая определить, какая операция должна быть вызвана, определяется в самом методе HTTP при отправке запроса. Проверьте определения метода HTTP .
Для getPerson
и deletePerson
обратите внимание, что мы добавили аннотацию @Path("{id}")
которая определяет необязательный путь для вызова службы. Поскольку нам нужно знать, какой объект мы хотим получить или удалить, нам нужно как-то указать id
. Это делается в URL-адресе службы, который необходимо вызвать, поэтому, если мы хотим удалить Person с идентификатором 1, мы будем вызывать http://yourdomain/resources/persons/1
с HTTP-методом DELETE.
Вот и все, что касается бэкэнда. Только 30 строк кода добавлено к старому сервису REST. Я также добавил новое свойство в объект Person для хранения ссылки на изображение с целью отображения аватара человека.
UI — угловой JS
Что касается пользовательского интерфейса, я решил разделить его на 3 секции: сетка, форма и секции сообщений обратной связи, каждая со своим собственным угловым контроллером. Сетка в основном такая же, как и в первой части , но она требует некоторых настроек для нового материала:
Сетка HTML
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
<!-- Specify a Angular controller script that binds Javascript variables to the grid.--> < div class = "grid" ng-controller = "personsListController" > < div > < h3 >List Persons</ h3 > </ div > <!-- Binds the grid component to be displayed. --> < div class = "gridStyle" ng-grid = "gridOptions" ></ div > <!-- Bind the pagination component to be displayed. --> < pagination direction-links = "true" boundary-links = "true" total-items = "persons.totalResults" items-per-page = "persons.pageSize" ng-model = "persons.currentPage" ng-change = "refreshGrid()" > </ pagination > </ div > |
Здесь нет ничего особенного. Практически так же, как часть 1 .
Сетчатый угловой контроллер
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
app.controller( 'personsListController' , function ($scope, $rootScope, personService) { // Initialize required information: sorting, the first page to show and the grid options. $scope.sortInfo = {fields: [ 'id' ], directions: [ 'asc' ]}; $scope.persons = {currentPage: 1 }; $scope.gridOptions = { data: 'persons.list' , useExternalSorting: true , sortInfo: $scope.sortInfo, columnDefs: [ { field: 'id' , displayName: 'Id' }, { field: 'name' , displayName: 'Name' }, { field: 'description' , displayName: 'Description' }, { field: '' , width: 30 , cellTemplate: '<span class="glyphicon glyphicon-remove remove" ng-click="deleteRow(row)"></span>' } ], multiSelect: false , selectedItems: [], // Broadcasts an event when a row is selected, to signal the form that it needs to load the row data. afterSelectionChange: function (rowItem) { if (rowItem.selected) { $rootScope.$broadcast( 'personSelected' , $scope.gridOptions.selectedItems[ 0 ].id); } } }; // Refresh the grid, calling the appropriate rest method. $scope.refreshGrid = function () { var listPersonsArgs = { page: $scope.persons.currentPage, sortFields: $scope.sortInfo.fields[ 0 ], sortDirections: $scope.sortInfo.directions[ 0 ] }; personService.get(listPersonsArgs, function (data) { $scope.persons = data; }) }; // Broadcast an event when an element in the grid is deleted. No real deletion is perfomed at this point. $scope.deleteRow = function (row) { $rootScope.$broadcast( 'deletePerson' , row.entity.id); }; // Watch the sortInfo variable. If changes are detected than we need to refresh the grid. // This also works for the first page access, since we assign the initial sorting in the initialize section. $scope.$watch( 'sortInfo.fields[0]' , function () { $scope.refreshGrid(); }, true ); // Do something when the grid is sorted. // The grid throws the ngGridEventSorted that gets picked up here and assigns the sortInfo to the scope. // This will allow to watch the sortInfo in the scope for changed and refresh the grid. $scope.$on( 'ngGridEventSorted' , function (event, sortInfo) { $scope.sortInfo = sortInfo; }); // Picks the event broadcasted when a person is saved or deleted to refresh the grid elements with the most // updated information. $scope.$on( 'refreshGrid' , function () { $scope.refreshGrid(); }); // Picks the event broadcasted when the form is cleared to also clear the grid selection. $scope.$on( 'clear' , function () { $scope.gridOptions.selectAll( false ); }); }); |
Для настройки поведения сетки требуется еще несколько атрибутов. Важными битами являются data: 'persons.list'
который привязывает данные сетки к значению угловой модели $scope.persons
, columnDefs
которое позволяет нам моделировать сетку по своему усмотрению. Поскольку я хотел добавить опцию для удаления каждой строки, мне нужно было добавить новую ячейку, которая вызывает функцию deleteRow
когда вы щелкаете по крестовому значку. Функция afterSelectionChanges
требуется для обновления данных формы с человеком, выбранным в сетке. Вы можете проверить другие параметры сетки здесь.
Остальная часть кода не требует пояснений, и там также есть несколько комментариев. Специальное примечание о $rootScope.$broadcast
: используется для отправки события всем остальным контроллерам. Это способ связи между контроллерами, поскольку сетка, форма и сообщения обратной связи имеют отдельные контроллеры. Если бы все было только в одном контроллере, это не требовалось, и простого вызова функции было бы достаточно. Другим возможным решением, если мы хотим сохранить несколько контроллеров, было бы использование сервисов Angular. Используемый подход кажется намного чище, поскольку он разделяет проблемы приложения и не требует от вас реализации дополнительных сервисов Angular, но в случае необходимости его отладка может оказаться немного сложнее.
Форма HTML
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
< div class = "form" ng-controller = "personsFormController" > <!-- Verify person, if there is no id present, that we are Adding a Person --> < div ng-if = "person.id == null" > < h3 >Add Person</ h3 > </ div > <!-- Otherwise it's an Edit --> < div ng-if = "person.id != null" > < h3 >Edit Person</ h3 > </ div > < div > <!-- Specify the function to be called on submit and disable HTML5 validation, since we're using Angular validation--> < form name = "personForm" ng-submit = "updatePerson()" novalidate> <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) --> < div class = "form-group" ng-class = "{'has-error' : personForm.name.$invalid && personForm.name.$dirty}" > < label for = "name" >Name:</ label > <!-- Display a check when the field is valid and was modified --> < span ng-class = "{'glyphicon glyphicon-ok' : personForm.name.$valid && personForm.name.$dirty}" ></ span > < input id = "name" name = "name" type = "text" class = "form-control" maxlength = "50" ng-model = "person.name" required ng-minlength = "2" ng-maxlength = "50" /> <!-- Validation messages to be displayed on required, minlength and maxlength --> < p class = "help-block" ng-show = "personForm.name.$error.required" >Add Name.</ p > < p class = "help-block" ng-show = "personForm.name.$error.minlength" >Name must be at least 2 characters long.</ p > < p class = "help-block" ng-show = "personForm.name.$error.maxlength" >Name cannot be longer than 50 characters.</ p > </ div > <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) --> < div class = "form-group" ng-class = "{'has-error' : personForm.description.$invalid && personForm.description.$dirty}" > < label for = "description" >Description:</ label > <!-- Display a check when the field is valid and was modified --> < span ng-class = "{'glyphicon glyphicon-ok' : personForm.description.$valid && personForm.description.$dirty}" ></ span > < input id = "description" name = "description" type = "text" class = "form-control" maxlength = "100" ng-model = "person.description" required ng-minlength = "5" ng-maxlength = "100" /> <!-- Validation messages to be displayed on required, minlength and maxlength --> < p class = "help-block" ng-show = "personForm.description.$error.required" >Add Description.</ p > < p class = "help-block" ng-show = "personForm.description.$error.minlength" >Description must be at least 5 characters long.</ p > < p class = "help-block" ng-show = "personForm.description.$error.maxlength" >Description cannot be longer than 100 characters.</ p > </ div > <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) --> < div class = "form-group" ng-class = "{'has-error' : personForm.imageUrl.$invalid && personForm.imageUrl.$dirty}" > < label for = "imageUrl" >Image URL:</ label > <!-- Display a check when the field is valid and was modified --> < span ng-class = "{'glyphicon glyphicon-ok' : personForm.imageUrl.$valid && personForm.imageUrl.$dirty}" ></ span > < input id = "imageUrl" name = "imageUrl" type = "url" class = "form-control" maxlength = "500" ng-model = "person.imageUrl" required/> <!-- Validation messages to be displayed on required and invalid. Type 'url' makes checks to a proper url format. --> < p class = "help-block" ng-show = "personForm.imageUrl.$error.required" >Add Image URL.</ p > < p class = "help-block" ng-show = "personForm.imageUrl.$invalid && personForm.imageUrl.$dirty" >Invalid Image URL.</ p > </ div > < div class = "avatar" ng-if = "person.imageUrl" > < img ng-src = "{{person.imageUrl}}" width = "400" height = "250" /> </ div > <!-- Form buttons. The 'Save' button is only enabled when the form is valid. --> < div class = "buttons" > < button type = "button" class = "btn btn-primary" ng-click = "clearForm()" >Clear</ button > < button type = "submit" class = "btn btn-primary" ng-disabled = "personForm.$invalid" >Save</ button > </ div > </ form > </ div > </ div > |
Вот выглядит:
Много кода для целей проверки, но давайте рассмотрим это немного подробнее: каждый элемент input
связывает свое значение с person.something
. Это позволяет моделировать данные между HTML и контроллером Javascript, поэтому мы можем написать $scope.person.name
в нашем контроллере, чтобы получить значение, заполненное в форме ввода с именем name
. Для доступа к данным внутри HTML-формы мы используем имя формы personForm
плюс имя поля ввода.
HTML5 имеет собственный набор проверок в полях ввода, но мы хотим использовать угловые. В этом случае нам нужно отключить проверку формы, используя novalidate
в элементе form
. Теперь, чтобы использовать угловые проверки, мы можем использовать несколько угловых директив во input
элементах. Для этой очень простой формы мы используем только required
ng-minlength
и ng-maxlength
, но вы можете использовать и другие. Просто загляните в документацию .
Angular назначает CSS-классы на основе состояния проверки ввода. Чтобы иметь представление, это возможные значения:
государство | CSS | На |
---|---|---|
valid |
нг-Валид | Когда поле действительно. |
invalid |
нг-инвалид | Когда поле недействительно. |
pristine |
нг-нетронутые | Когда поле никогда не трогали раньше. |
dirty |
нг-грязная | Когда поле изменилось. |
Эти CSS-классы пусты. Вам необходимо создать их и назначить их стили на прилагаемом листе CSS для приложения. Вместо этого мы собираемся использовать стили из Bootstrap, которые очень хороши. Чтобы они работали, к элементам необходимо применить несколько дополнительных классов. div
заключающему в себя вход, требуется группа form-group
класса CSS, а элементу input
нужен элемент управления form-control
CSS.
Чтобы отобразить недопустимое поле ввода, мы добавляем ng-class="{'has-error' : personForm.name.$invalid && personForm.name.$dirty}"
в содержащий входной div. Этот код оценивает, является ли имя в personForm недействительным и является ли оно грязным. Если условие проверяется, то ввод отображается как недействительный.
Наконец, для сообщений проверки формы нам нужно проверить директиву $error
для каждого из входных данных и типов выполняемых проверок. Просто добавьте ng-show="personForm.name.$error.minlength"
в элемент отображения HTML с сообщением, чтобы предупредить пользователя о том, что поле ввода имени слишком короткое.
Форма угловой контроллер
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
// Create a controller with name personsFormController to bind to the form section. app.controller( 'personsFormController' , function ($scope, $rootScope, personService) { // Clears the form. Either by clicking the 'Clear' button in the form, or when a successfull save is performed. $scope.clearForm = function () { $scope.person = null ; // For some reason, I was unable to clear field values with type 'url' if the value is invalid. // This is a workaroud. Needs proper investigation. document.getElementById( 'imageUrl' ).value = null ; // Resets the form validation state. $scope.personForm.$setPristine(); // Broadcast the event to also clear the grid selection. $rootScope.$broadcast( 'clear' ); }; // Calls the rest method to save a person. $scope.updatePerson = function () { personService.save($scope.person).$promise.then( function () { // Broadcast the event to refresh the grid. $rootScope.$broadcast( 'refreshGrid' ); // Broadcast the event to display a save message. $rootScope.$broadcast( 'personSaved' ); $scope.clearForm(); }, function () { // Broadcast the event for a server error. $rootScope.$broadcast( 'error' ); }); }; // Picks up the event broadcasted when the person is selected from the grid and perform the person load by calling // the appropiate rest service. $scope.$on( 'personSelected' , function (event, id) { $scope.person = personService.get({id: id}); }); // Picks us the event broadcasted when the person is deleted from the grid and perform the actual person delete by // calling the appropiate rest service. $scope.$on( 'deletePerson' , function (event, id) { personService.delete({id: id}).$promise.then( function () { // Broadcast the event to refresh the grid. $rootScope.$broadcast( 'refreshGrid' ); // Broadcast the event to display a delete message. $rootScope.$broadcast( 'personDeleted' ); $scope.clearForm(); }, function () { // Broadcast the event for a server error. $rootScope.$broadcast( 'error' ); }); }); }); |
Для контроллера формы нам нужны две функции, которые выполняют операции, связанные с кнопкой «Очистить» и кнопкой «Сохранить», которые не требуют пояснений. Небольшое примечание: по какой-то причине Angular не очищает поля ввода, которые находятся в недопустимом состоянии. Я нашел несколько человек, жалующихся на ту же проблему, но мне нужно исследовать это дальше. Может быть, я что-то не так делаю.
Службы REST вызываются с помощью save
и delete
из объекта $resource
который уже реализует соответствующие методы HTTP. Проверьте документацию . Вы можете получить $resource
со следующей фабрикой:
ОТДЫХ Сервис
1
2
3
4
|
// Service that provides persons operations app.factory( 'personService' , function ($resource) { return $resource( 'resources/persons/:id' ); }); |
Остальная часть кода контроллера — это функции для сбора событий, созданных сеткой, для загрузки данных о человеке в форму и удаления человека. Этот контроллер также создает несколько событий. Если мы добавляем или удаляем лиц, необходимо обновить сетку, чтобы сгенерировалось событие, запрашивающее обновление сетки.
Обратная связь Сообщения HTML
1
2
3
4
|
<!-- Specify a Angular controller script that binds Javascript variables to the feedback messages.--> < div class = "message" ng-controller = "alertMessagesController" > < alert ng-repeat = "alert in alerts" type = "{{alert.type}}" close = "closeAlert($index)" >{{alert.msg}}</ alert > </ div > |
Это только верхняя часть приложения, отображающая сообщения об успехах или ошибках, основанные на сохранении, удалении или ошибке сервера.
Сообщения обратной связи Угловой контроллер
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// Create a controller with name alertMessagesController to bind to the feedback messages section. app.controller( 'alertMessagesController' , function ($scope) { // Picks up the event to display a saved message. $scope.$on( 'personSaved' , function () { $scope.alerts = [ { type: 'success' , msg: 'Record saved successfully!' } ]; }); // Picks up the event to display a deleted message. $scope.$on( 'personDeleted' , function () { $scope.alerts = [ { type: 'success' , msg: 'Record deleted successfully!' } ]; }); // Picks up the event to display a server error message. $scope.$on( 'error' , function () { $scope.alerts = [ { type: 'danger' , msg: 'There was a problem in the server!' } ]; }); $scope.closeAlert = function (index) { $scope.alerts.splice(index, 1 ); }; }); |
Это контроллер, который выдвигает сообщения к представлению. Прослушивает события, созданные сеткой и контроллерами форм.
Конечный результат
Уф … это было много кода и новой информации. Давайте посмотрим на конечный результат:
Существует также живая версия, запущенная на http://javaee7-angular.radcortez.cloudbees.net , благодаря Cloudbees . Открытие облачного хранилища может занять некоторое время (из-за отсутствия использования).
Ресурсы
Вы можете клонировать полную рабочую копию из моего репозитория github и развернуть ее в Wildfly. Вы можете найти там инструкции по его развертыванию. Также должен работать на Glassfish.
Поскольку я могу изменить код в будущем, вы можете скачать исходный код этого поста с версии 3.0 . В качестве альтернативы, клонируйте репозиторий и извлеките тег из версии 3.0 с помощью следующей команды: git checkout 3.0
.
Проверьте также:
Последние мысли
- Проверка формы запускается сразу после того, как вы начнете печатать. У Angular 1.3 будет свойство on blur, которое можно проверить только после потери фокуса, но я все еще использую Angular 1.2.x.
- Я должен признаться, что я нашел код проверки слишком многословным. Я не знаю, есть ли способ упростить его, но вам не нужно добавлять проверку каждого сообщения к каждому входу.
- Здесь все еще не хватает некоторых вещей, таких как очистка параметров или проверка на стороне сервера. Я расскажу об этом в следующем посте.
Это был очень длинный пост, самый длинный, который я написал в своем блоге. Если вы дошли до этого места, большое спасибо за ваше время, прочитав этот пост . Надеюсь, вам понравилось! Дайте мне знать, если у вас есть какие-либо комментарии.
Ссылка: | Java EE 7 с Angular JS — CRUD, REST, валидации — часть 2 от нашего партнера по JCG Роберто Кортеса в блоге |