Integrating ZK with AngularJS"
m (correct highlight (via JWB)) |
|||
Line 71: | Line 71: | ||
</source> | </source> | ||
− | Because we need to send data to the server-side, we need to add a zul < | + | Because we need to send data to the server-side, we need to add a zul <code>Div</code> component with a ViewModel on this page. |
'''todo.zhtml''' | '''todo.zhtml''' | ||
− | <source lang='xml' | + | <source lang='xml' highlight='1, 9'> |
<html xmlns="native" xmlns:z="zul" xmlns:ca="client/attribute" ca:ng-app="todoApp"> | <html xmlns="native" xmlns:z="zul" xmlns:ca="client/attribute" ca:ng-app="todoApp"> | ||
Line 106: | Line 106: | ||
</source> | </source> | ||
* Line 1: Because most elements are native elements, we make native components as the default namespace. | * Line 1: Because most elements are native elements, we make native components as the default namespace. | ||
− | * Line 9: Apply a ViewModel on a ZK < | + | * Line 9: Apply a ViewModel on a ZK <code>Div</code> component. Notice its namespace is <code>z:</code>. |
= Initialize Todo List = | = Initialize Todo List = | ||
Line 143: | Line 143: | ||
== Client-side == | == Client-side == | ||
− | The controller initializes < | + | The controller initializes <code>todoList.todos</code> with a static JavaScript array, but we want to initialize it from the server-side. So we empty the list and [http://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html register a callback for a command] to receive a todo list from the server-side. |
− | <source lang='javascript' | + | <source lang='javascript' highlight='6, 8, 9'> |
angular.module('todoApp', []) | angular.module('todoApp', []) | ||
.controller('TodoListController', function($scope, $element) { | .controller('TodoListController', function($scope, $element) { | ||
Line 163: | Line 163: | ||
* Line 6: Get a binder object for we should call client-side binding API through it. | * Line 6: Get a binder object for we should call client-side binding API through it. | ||
* Line 8: Register a calllback function. The 1st parameter is the command name, and the 2nd is the callback function. | * Line 8: Register a calllback function. The 1st parameter is the command name, and the 2nd is the callback function. | ||
− | * Line 9: The parameter of the callback comes from the property specified in < | + | * Line 9: The parameter of the callback comes from the property specified in <code>@NotifyCommand</code>. We will mention it in the next section. |
== Server-side == | == Server-side == | ||
− | We declare a list object to contain all todo items and its getter in the ViewModel and create a domain class < | + | We declare a list object to contain all todo items and its getter in the ViewModel and create a domain class <code>Todo</code>. Then, we trigger a command execution with [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/notifycommand.html <code>@NotifyCommand</code>] when <code>todoList</code> is loaded (by the binder). The underlying implementation of <code>@NotifyCommand</code> is to add a property load binding on a root component's internal attribute; hence <code>todoList</code> will be loaded at 2 moments as a normal load binding: |
# the page creation phase | # the page creation phase | ||
− | # notify a change for < | + | # notify a change for <code>todoList</code>, e.g. <code>@NotifyChange("todoList")</code> |
<!-- self.attributes['$BINDER$'].dynamicAttrs['updateTodo'] --> | <!-- self.attributes['$BINDER$'].dynamicAttrs['updateTodo'] --> | ||
− | Combine < | + | Combine <code>@NotifyCommand</code> with [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/toclientcommand.html <code>@ToClientCommand</code>], ZK will invoke the client-side callback at 2 moments above and send <code>todoList</code> as javascript array as a parameter. |
− | <source lang='java' | + | <source lang='java' highlight='14, 15'> |
package org.zkoss.zkangular; | package org.zkoss.zkangular; | ||
Line 217: | Line 217: | ||
</source> | </source> | ||
− | We need to synchronize the todoList with the server-side by invoking a command < | + | We need to synchronize the todoList with the server-side by invoking a command <code>addTodo</code> with the newly-created todo as a parameter. We can usually count on ZK to convert the parameter into JSON object, but AngularJS adds some internal properties in our todo object. Therefore, we have to convert it by Angular's [https://docs.angularjs.org/api/ng/function/angular.toJson <code>angular.toJson(newTodo)</code>]. |
− | <source lang='javascript' | + | <source lang='javascript' highlight='6'> |
$scope.todoList.addTodo = function() { | $scope.todoList.addTodo = function() { | ||
var newTodo = {text:$scope.todoList.todoText, done:false}; | var newTodo = {text:$scope.todoList.todoText, done:false}; | ||
Line 228: | Line 228: | ||
}; | }; | ||
</source> | </source> | ||
− | * Line 6: AngularJS adds a property < | + | * Line 6: AngularJS adds a property <code>$$hashKey</code> in every <code>newTodo</code> to keep track of its changes, so that it knows when it needs to update the DOM. We should remove that hash key with [https://docs.angularjs.org/api/ng/function/angular.toJson <code>angular.toJson(newTodo)</code>] before passing to the server. Because of this extra property, $$hashKey, will prevent ZK from converting newTodo (JSON) into its Java object (Todo) correctly. |
== Server-side == | == Server-side == | ||
− | The command method requires a [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/bindingparam.html @BindingParam] with a corresponding key in its parameter to receive the Todo object from the client. ZK can automatically convert a JSON object sent from the client into our domain object < | + | The command method requires a [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/bindingparam.html @BindingParam] with a corresponding key in its parameter to receive the Todo object from the client. ZK can automatically convert a JSON object sent from the client into our domain object <code>Todo</code>, but you should declare a no-argument constructor in <code>Todo</code>. |
− | <source lang='java' | + | <source lang='java' highlight='8'> |
public class TodoVM { | public class TodoVM { | ||
... | ... | ||
Line 246: | Line 246: | ||
} | } | ||
</source> | </source> | ||
− | * Line 8: < | + | * Line 8: <code>Todo</code> requires a no-argument constructor for the automatic JSON conversion. |
= Update "Done" Status = | = Update "Done" Status = | ||
Line 253: | Line 253: | ||
In order to implement "archive" at the server side, we need to synchronize "done" status with the server. In a pure client-side application, there is no such need, so there is no corresponding method in the original AngularJS controller. | In order to implement "archive" at the server side, we need to synchronize "done" status with the server. In a pure client-side application, there is no such need, so there is no corresponding method in the original AngularJS controller. | ||
− | First, we need to add a listener on the checkbox at < | + | First, we need to add a listener on the checkbox at <code>ng-click</code> attribute: |
− | <source lang='html' | + | <source lang='html' highlight='2'> |
... | ... | ||
<input type="checkbox" ng-model="todo.done" ng-click="todoList.updateStatus(todo)"> | <input type="checkbox" ng-model="todo.done" ng-click="todoList.updateStatus(todo)"> | ||
Line 262: | Line 262: | ||
Then we add a listener to invoke a command in the ViewModel with related data. | Then we add a listener to invoke a command in the ViewModel with related data. | ||
− | <source lang='javascript' | + | <source lang='javascript' highlight='3'> |
$scope.todoList.updateStatus = function(todo) { | $scope.todoList.updateStatus = function(todo) { | ||
//send to ZK VM | //send to ZK VM | ||
Line 273: | Line 273: | ||
This time the command requires receiving 2 arguments. | This time the command requires receiving 2 arguments. | ||
− | <source lang='java' | + | <source lang='java' highlight='2'> |
@Command | @Command | ||
public void updateStatus(@BindingParam("index") int index, @BindingParam("done") boolean done){ | public void updateStatus(@BindingParam("index") int index, @BindingParam("done") boolean done){ | ||
Line 297: | Line 297: | ||
Since we don't want to synchronize each todo status one by one between the client and the server, we decided to move the "archive" function implementation to the server side. On the client side, we just invoke the corresponding command. | Since we don't want to synchronize each todo status one by one between the client and the server, we decided to move the "archive" function implementation to the server side. On the client side, we just invoke the corresponding command. | ||
− | <source lang='javascript' | + | <source lang='javascript' highlight='3'> |
$scope.todoList.archive = function() { | $scope.todoList.archive = function() { | ||
//archive todo list at the server side | //archive todo list at the server side | ||
Line 306: | Line 306: | ||
==Server-side== | ==Server-side== | ||
− | We implement "archive" logic (remove those done todo items) in the command method. Since the whole < | + | We implement "archive" logic (remove those done todo items) in the command method. Since the whole <code>todoList</code> changes and needs to be rendered again on the client-side, we put <code>@NotifyChange("todoList")</code> to calling the callback at the client-side with <code>todoList</code> as a parameter. |
− | <source lang='java' | + | <source lang='java' highlight='1'> |
@Command @NotifyChange("todoList") | @Command @NotifyChange("todoList") |
Latest revision as of 04:26, 20 January 2022
Hawk Chen, Engineer, Potix Corporation
May 3, 2016
ZK 8.0
Introduction
AngularJS (1.x) [1] is a well-known client-side UI framework, and its two-way data binding on HTML is the most notable feature. You could build a custom view with it when there is no suitable ZK component available. Or you might want to integrate an existing AngularJS widget with ZK. I assume the readers had read its tutorial, so I won't introduce AngularJS in detail here.
This article will talk about how to integrate AngularJS in a simple way with ZK 8's new feature, client-side binding API. The API provides a server-client communication channel so that you can easily communicate with ZK’s ViewModel without knowing AJAX details. This channel can simplify the complexity to integrate a 3rd party JavaScript library and widget with ZK. It mainly provides 2 methods to communicate with a ViewModel. One is to invoke a command method, and another is to register a callback function to be invoked after a ViewModel executes a command method.
The overall concept of the communication between AngularJS and ViewModel is:
You can also integrate with other client-side frameworks with the same approach. Please refer to:
- ZK8: Chat Room with React.js
- ZK8: Work with Polymer Components using ZK’s new client-side binding API
- ↑ According to Angular naming guideline, I should use "AngularJS" to describe versions 1.x or earlier
Example Application
We will build a Todo application which is demonstrated at AngularJS official website as an example.
The functions are simple:
- add todo items
- check or uncheck a todo
- archive those done todo items
WebJars
In this example project, we rely on WebJars to include 3rd party front-end resources like AngularJS and Bootstrap theme. It lets you explicitly and easily manage the client-side dependencies with Maven, and it also supports other dependency management tools like ivy or Gradle. When I include Bootstrap, maven will also include Jquery web JAR for the transitive dependency.
Since servlet 3 is easier to expose the web static resources, we specify our web.xml with servlet 3. Please run the project with an application server that supports servlet 3 like Tomcat 7 or Jetty 8.
Building the UI
For easy understanding, we build the UI mainly with ZK native components and replace some of them with zul components if necessary; therefore, you can just copy the HTML including CSS and Javascript from AngularJS's website. Although a purely client-side application is working, it still doesn't synchronize its data to the server-side.
<!doctype html>
<html ng-app="todoApp">
<head>
<script src="webjars/angularjs/1.4.8/angular.min.js"></script>
<script src="todo.js"></script>
...
</head>
<body>
<h2>Todo</h2>
<div ng-controller="TodoListController as todoList">
<span>{{todoList.remaining()}} of {{todoList.todos.length}} remaining</span>
[ <a href="" ng-click="todoList.archive()">archive</a> ]
<ul class="unstyled">
<li ng-repeat="todo in todoList.todos">
<label class="checkbox">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</label>
</li>
</ul>
<form ng-submit="todoList.addTodo()">
<input type="text" ng-model="todoList.todoText" size="30"
placeholder="add new todo here">
<input class="btn-primary" type="submit" value="add">
</form>
</div>
</body>
</html>
Because we need to send data to the server-side, we need to add a zul Div
component with a ViewModel on this page.
todo.zhtml
<html xmlns="native" xmlns:z="zul" xmlns:ca="client/attribute" ca:ng-app="todoApp">
<head>
<script src="webjars/angularjs/1.4.8/angular.min.js"></script>
<script src="todo-zk.js"></script>
...
</head>
<body>
<h2>Todo</h2>
<z:div id="content" viewModel="@id('vm') @init('org.zkoss.zkangular.TodoVM')" >
<div ng-controller="TodoListController as todoList">
<span>{{todoList.remaining()}} of {{todoList.todos.length}} remaining</span>
[ <a href="" ng-click="todoList.archive()">archive</a> ]
<ul class="unstyled">
<li ng-repeat="todo in todoList.todos">
<input type="checkbox" ng-model="todo.done" ng-click="todoList.updateStatus(todo)">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>
<form ng-submit="todoList.addTodo()">
<input type="text" ng-model="todoList.todoText" size="30"
placeholder="add new todo here">
<input class="btn btn-primary" type="submit" value="add">
</form>
</div>
</z:div>
</body>
</html>
- Line 1: Because most elements are native elements, we make native components as the default namespace.
- Line 9: Apply a ViewModel on a ZK
Div
component. Notice its namespace isz:
.
Initialize Todo List
We can start from AngularJS's controller:
angular.module('todoApp', [])
.controller('TodoListController', function() {
var todoList = this;
todoList.todos = [
{text:'learn angular', done:true},
{text:'build an angular app', done:false}];
todoList.addTodo = function() {
todoList.todos.push({text:todoList.todoText, done:false});
todoList.todoText = '';
};
todoList.remaining = function() {
var count = 0;
angular.forEach(todoList.todos, function(todo) {
count += todo.done ? 0 : 1;
});
return count;
};
todoList.archive = function() {
var oldTodos = todoList.todos;
todoList.todos = [];
angular.forEach(oldTodos, function(todo) {
if (!todo.done) todoList.todos.push(todo);
});
};
});
Client-side
The controller initializes todoList.todos
with a static JavaScript array, but we want to initialize it from the server-side. So we empty the list and register a callback for a command to receive a todo list from the server-side.
angular.module('todoApp', [])
.controller('TodoListController', function($scope, $element) {
$scope.todoList.todos = []; //data model
//communicate with ZK VM
var binder = zkbind.$('$content'); //the binder is used to invoke a command, register a command callback
//register a command callback
binder.after('updateTodo',
function (updatedTodoList) {
$scope.$apply(function() {
$scope.todoList.todos = updatedTodoList;
});
});
...
- Line 6: Get a binder object for we should call client-side binding API through it.
- Line 8: Register a calllback function. The 1st parameter is the command name, and the 2nd is the callback function.
- Line 9: The parameter of the callback comes from the property specified in
@NotifyCommand
. We will mention it in the next section.
Server-side
We declare a list object to contain all todo items and its getter in the ViewModel and create a domain class Todo
. Then, we trigger a command execution with @NotifyCommand
when todoList
is loaded (by the binder). The underlying implementation of @NotifyCommand
is to add a property load binding on a root component's internal attribute; hence todoList
will be loaded at 2 moments as a normal load binding:
- the page creation phase
- notify a change for
todoList
, e.g.@NotifyChange("todoList")
Combine @NotifyCommand
with @ToClientCommand
, ZK will invoke the client-side callback at 2 moments above and send todoList
as javascript array as a parameter.
package org.zkoss.zkangular;
import java.util.ArrayList;
import java.util.Iterator;
import org.zkoss.bind.annotation.BindingParam;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.bind.annotation.NotifyCommand;
import org.zkoss.bind.annotation.ToClientCommand;
import org.zkoss.bind.annotation.ToServerCommand;
@NotifyCommand(value="updateTodo", onChange="_vm_.todoList")
@ToClientCommand({"updateTodo"})
public class TodoVM {
private ArrayList<Todo> todoList = new ArrayList<Todo>();
@Init
public void init() {
Todo todo = new Todo("learn ZK");
todo.setDone(true);
todoList.add(todo);
todoList.add(new Todo("build a ZK application"));
}
//getter and setter
...
Adding a Todo
Client-side
In Angular controller, it adds a todo object into the list:
todoList.addTodo = function() {
todoList.todos.push({text:todoList.todoText, done:false});
todoList.todoText = '';
};
We need to synchronize the todoList with the server-side by invoking a command addTodo
with the newly-created todo as a parameter. We can usually count on ZK to convert the parameter into JSON object, but AngularJS adds some internal properties in our todo object. Therefore, we have to convert it by Angular's angular.toJson(newTodo)
.
$scope.todoList.addTodo = function() {
var newTodo = {text:$scope.todoList.todoText, done:false};
$scope.todoList.todos.push(newTodo);
$scope.todoList.todoText = '';
//send to ZK VM
binder.command('addTodo', {todo:angular.toJson(newTodo)});
};
- Line 6: AngularJS adds a property
$$hashKey
in everynewTodo
to keep track of its changes, so that it knows when it needs to update the DOM. We should remove that hash key withangular.toJson(newTodo)
before passing to the server. Because of this extra property, $$hashKey, will prevent ZK from converting newTodo (JSON) into its Java object (Todo) correctly.
Server-side
The command method requires a @BindingParam with a corresponding key in its parameter to receive the Todo object from the client. ZK can automatically convert a JSON object sent from the client into our domain object Todo
, but you should declare a no-argument constructor in Todo
.
public class TodoVM {
...
/**
* ZK can automatically convert a JSON object into your domain object.
* @param todo
*/
@Command
public void addTodo(@BindingParam("todo") Todo todo){
todoList.add(todo);
}
- Line 8:
Todo
requires a no-argument constructor for the automatic JSON conversion.
Update "Done" Status
Client-side
In order to implement "archive" at the server side, we need to synchronize "done" status with the server. In a pure client-side application, there is no such need, so there is no corresponding method in the original AngularJS controller.
First, we need to add a listener on the checkbox at ng-click
attribute:
...
<input type="checkbox" ng-model="todo.done" ng-click="todoList.updateStatus(todo)">
<span class="done-{{todo.done}}">{{todo.text}}</span>
...
Then we add a listener to invoke a command in the ViewModel with related data.
$scope.todoList.updateStatus = function(todo) {
//send to ZK VM
binder.command('updateStatus', {index:$scope.todoList.todos.indexOf(todo), done:todo.done});
};
Server-side
This time the command requires receiving 2 arguments.
@Command
public void updateStatus(@BindingParam("index") int index, @BindingParam("done") boolean done){
todoList.get(index).setDone(done);
}
Archiving Todo
In the original AngularJS controller, it implements archive logic in it:
todoList.archive = function() {
var oldTodos = todoList.todos;
todoList.todos = [];
angular.forEach(oldTodos, function(todo) {
if (!todo.done) todoList.todos.push(todo);
});
};
Client-side
Since we don't want to synchronize each todo status one by one between the client and the server, we decided to move the "archive" function implementation to the server side. On the client side, we just invoke the corresponding command.
$scope.todoList.archive = function() {
//archive todo list at the server side
binder.command('archive');
};
Server-side
We implement "archive" logic (remove those done todo items) in the command method. Since the whole todoList
changes and needs to be rendered again on the client-side, we put @NotifyChange("todoList")
to calling the callback at the client-side with todoList
as a parameter.
@Command @NotifyChange("todoList")
public void archive(){
Iterator<Todo> iterator = todoList.iterator();
while (iterator.hasNext()){
Todo todo = iterator.next();
if (todo.isDone()){
iterator.remove();
}
}
}
Client or Server Implementation?
For each function like add or archive todo, you need to determine whether to implement it on the client or server-side. Both ways have its pros and cons. In general, we recommend implementing on the server-side, the reasons are:
- An application logic might involve a lot of data from different parts of a system; it is, therefore, easier to access them on the server-side. To implement such functions on the client-side, you would have to push all the data to the client-side first.
- Avoid exposing business logic to users.
- For a complicated business logic, Java has better compiler checking.
- If a page is accidentally closed or reloaded, the data that have not been synchronized with a server will be lost.
Meanwhile, the server-side implementation will increase the network traffic and requires more execution time (at least a round-trip from a client to server). Therefore, if it's critical to have a fast response time for your application, you can choose a client-side implementation.
Take, for example, "archive" function. The potential problem might be the re-rendering performance when the number of items is very large. You can implement it with Javascript and invoke a command with those removed todos' index as a parameter to avoid re-rendering the whole list. Then you would just have to implement logic removal on the server side.
Download
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |