Integrating ZK with Angular JS

From Documentation
Revision as of 04:25, 20 January 2022 by Hawk (talk | contribs) (correct highlight (via JWB))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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.