Integrating ZK with Angular JS
Hawk Chen, Engineer, Potix Corporation
May 3, 2016
ZK 8.0
This article is out of date, please refer to https://www.zkoss.org/wiki/Small_Talks/2017/June/Using_Angular_with_ZK for more up to date information.
Introduction
AngularJS (1.x) 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
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. |