Integrating ZK with Angular JS"

From Documentation
m (correct highlight (via JWB))
 
Line 67: Line 67:
 
</source>
 
</source>
  
Because we need to send data to the server-side, we need to add a zul <tt>Div</tt> component with a ViewModel on this page.
+
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' high='1, 9'>
+
<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 102: Line 102:
 
</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 <tt>Div</tt> component. Notice its namespace is <tt>z:</tt>.
+
* 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 139: Line 139:
  
 
== Client-side ==
 
== Client-side ==
The controller initializes <tt>todoList.todos</tt> 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.
+
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' high='6, 8, 9'>
+
<source lang='javascript' highlight='6, 8, 9'>
 
angular.module('todoApp', [])
 
angular.module('todoApp', [])
 
.controller('TodoListController', function($scope, $element) {
 
.controller('TodoListController', function($scope, $element) {
Line 159: Line 159:
 
* 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 <tt>@NotifyCommand</tt>. We will mention it in the next section.
+
* 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 <tt>Todo</tt>. Then, we trigger a command execution with [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/notifycommand.html <tt>@NotifyCommand</tt>] when <tt>todoList</tt> is loaded (by the binder). The underlying implementation of <tt>@NotifyCommand</tt> is to add a property load binding on a root component's internal attribute; hence <tt>todoList</tt> will be loaded at 2 moments as a normal load binding:
+
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 <tt>todoList</tt>, e.g. <tt>@NotifyChange("todoList")</tt>
+
# notify a change for <code>todoList</code>, e.g. <code>@NotifyChange("todoList")</code>
 
<!-- self.attributes['$BINDER$'].dynamicAttrs['updateTodo'] -->
 
<!-- self.attributes['$BINDER$'].dynamicAttrs['updateTodo'] -->
  
Combine <tt>@NotifyCommand</tt> with [http://books.zkoss.org/zk-mvvm-book/8.0/syntax/toclientcommand.html <tt>@ToClientCommand</tt>], ZK will invoke the client-side callback at 2 moments above and send <tt>todoList</tt> as javascript array as a parameter.
+
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' high='14, 15'>
+
<source lang='java' highlight='14, 15'>
 
package org.zkoss.zkangular;
 
package org.zkoss.zkangular;
  
Line 213: Line 213:
 
</source>
 
</source>
  
We need to synchronize the todoList with the server-side by invoking a command <tt>addTodo</tt> 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 <tt>angular.toJson(newTodo)</tt>].
+
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' high='6'>
+
<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 224: Line 224:
 
};
 
};
 
</source>
 
</source>
* Line 6: AngularJS adds a property <tt>$$hashKey</tt> in every <tt>newTodo</tt> 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 <tt>angular.toJson(newTodo)</tt>] before passing to the server. Because of this extra property, $$hashKey, will prevent ZK from converting newTodo (JSON) into its Java object (Todo) correctly.
+
* 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 <tt>Todo</tt>, but you should declare a no-argument constructor in <tt>Todo</tt>.
+
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' high='8'>
+
<source lang='java' highlight='8'>
 
public class TodoVM {
 
public class TodoVM {
 
...
 
...
Line 242: Line 242:
 
}
 
}
 
</source>
 
</source>
* Line 8: <tt>Todo</tt> requires a no-argument constructor for the automatic JSON conversion.
+
* Line 8: <code>Todo</code> requires a no-argument constructor for the automatic JSON conversion.
  
 
= Update "Done" Status =
 
= Update "Done" Status =
Line 249: Line 249:
 
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 <tt>ng-click</tt> attribute:
+
First, we need to add a listener on the checkbox at <code>ng-click</code> attribute:
<source lang='html' high='2'>
+
<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 258: Line 258:
  
 
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' high='3'>
+
<source lang='javascript' highlight='3'>
 
$scope.todoList.updateStatus = function(todo) {
 
$scope.todoList.updateStatus = function(todo) {
 
//send to ZK VM
 
//send to ZK VM
Line 269: Line 269:
  
 
This time the command requires receiving 2 arguments.  
 
This time the command requires receiving 2 arguments.  
<source lang='java' high='2'>
+
<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 293: Line 293:
  
 
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' high='3'>
+
<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 302: Line 302:
  
 
==Server-side==
 
==Server-side==
We implement "archive" logic (remove those done todo items) in the command method. Since the whole <tt>todoList</tt> changes and needs to be rendered again on the client-side, we put <tt>@NotifyChange("todoList")</tt> to calling the callback at the client-side with <tt>todoList</tt> as a parameter.
+
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' high='1'>
+
<source lang='java' highlight='1'>
  
 
@Command @NotifyChange("todoList")
 
@Command @NotifyChange("todoList")

Latest revision as of 04:25, 20 January 2022

DocumentationSmall Talks2016MayIntegrating ZK with Angular JS
Integrating ZK with Angular JS

Author
Hawk Chen, Engineer, Potix Corporation
Date
May 3, 2016
Version
ZK 8.0

Stop.png 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:

Zkangular-clientSideBindingApi.png


You can also integrate with other client-side frameworks with the same approach. Please refer to:

Example Application

We will build a Todo application which is demonstrated at AngularJS official website as an example.

Zk-angular-todo.png

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 is z:.

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:

  1. the page creation phase
  2. 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 every newTodo to keep track of its changes, so that it knows when it needs to update the DOM. We should remove that hash key with angular.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

Example source code.



Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.