Smalltalk-draft"

From Documentation
m (correct highlight (via JWB))
 
(49 intermediate revisions by one other user not shown)
Line 6: Line 6:
  
 
= Overview =
 
= Overview =
[https://angular.io/ Angular] is a front-end framework for building user interfaces. We can use Angular as a front-end UI and use ZK as a pure backend. In this small talk, I will show you how to integrate Angular with ZK using the [https://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html client binding API].
+
[https://angular.io/ Angular] is a well-known client-side MVW framework. In the previous article, [https://www.zkoss.org/wiki/Small_Talks/2017/June/Using_Angular_with_ZK Using Angular with ZK], we have introduced how to integrate with Angular2.0.
 +
Except that Angular has changed drastically since then, what is different this time is that we will talk about how to integrate an existed(or new) Angular project with ZK, and we do not have to modify the components in the Angular project.
  
For convenience, I use a MIT licensed Angular demo project [https://github.com/Ismaestro/angular8-example-app] to demonstrate.
+
In this demo, we [https://www.zkoss.org/wiki/ZK_Installation_Guide/Quick_Start/Create_and_Run_Your_First_ZK_Application_with_Spring_Boot start a ZK application with Spring Boot] then place an Angular project in the frontend folder.
 +
 
 +
We will show you how to integrate Angular(6+) with ZK using the [https://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html client binding API].
 +
 
 +
For convenience, I use a MIT licensed Angular demo project [https://github.com/Ismaestro/angular8-example-app angular8-example-app] to demonstrate.
 +
 
 +
[[File:Zkng-demo.png|540px|thumb|center]]
 +
 
 +
= Load Data from ZK =
 +
 
 +
We need to create a View Model to use the client binding feature in Angular.
 +
 
 +
== Create a ViewModel (VM) in ZK ==
 +
 
 +
We created an <code>HeroVM.java</code> and defined a property "heroes".
 +
 
 +
'''HeroVM.java'''
 +
<source lang="java">
 +
public class HeroVM {
 +
// omitted
 +
private List<Hero> heroes;
 +
 
 +
// omitted
 +
 
 +
public List<Hero> getHeroes() {
 +
return heroes;
 +
}
 +
 
 +
public void setHeroes(ArrayList<Hero> heroes) {
 +
this.heroes = heroes;
 +
}
 +
}
 +
</source>
 +
* Line 7: Declare getter only here so ZK knows <code>heroes</code> is a readonly property of a VM.
 +
 
 +
== Add Client Binding Annotations ==
 +
 
 +
To get the <code>vm.heroes</code> from the client, we need to add some annotation to the HeroVM.
 +
 
 +
'''HeroVM.java'''
 +
<source lang="java" highlight="1,2,3,8">
 +
@NotifyCommand(value = "updateHero", onChange = "_vm_.heroes")
 +
@ToClientCommand({"updateHero"})
 +
@ToServerCommand({"reload", "delete", "add", "update"})
 +
public class HeroVM {
 +
// omitted
 +
@Command
 +
@NotifyChange("heroes")
 +
public void reload(@BindingParam("id") String id) {
 +
if (id == null) {
 +
heroes = dao.queryAll();
 +
} else { //query one
 +
heroes.clear();
 +
heroes.add(dao.queryById(id));
 +
}
 +
}
 +
// omitted
 +
}
 +
</source>
 +
* LIne 1: Add a <code>@NotifyCommand</code> and listen to <code>_vm_.heroes</code> changes (<em>_vm_</em> means the VM object)
 +
* Line 2: Register a client command so the client can call <code>getHeroes</code>. ZK client binding uses whitelist so you must declare it explicitly. To allow all, simply use an asterisk symbol "*".
 +
* Line 3 and 8: We added a simple command to trigger <code>heroes</code> changes for later use. Since it's called from the client, we need to register it by <code>@ToServerCommand</code>.
 +
 
 +
Once we set, we can get the property from the client. Let's see how we get the <code>heroes</code> from ZK.
 +
 
 +
== Use Client Binding API in Angular ==
 +
 
 +
We can use [https://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html#on-clientside binder.after] to get properties of ViewModel from ZK.
 +
 
 +
Let's see how to use client binding API in an Angular service, and coexist with [https://angular.io/guide/observables observables], so we don't have to modify other Angular components.
 +
'''frontend/src/app/modules/heroes/shared/hero.service.ts'''
 +
<source lang="js" highlight="16,22">
 +
// omitted
 +
declare var zkbind: any;
 +
 
 +
@Injectable({
 +
  providedIn: 'root'
 +
})
 +
export class HeroService {
 +
  private binder = zkbind.$('$heroes');
 +
  private heroes: EventEmitter<Hero[]> = new EventEmitter();
 +
  // omitted
 +
 
 +
  constructor(private snackBar: MatSnackBar,
 +
              private i18n: I18n,
 +
              private cookieService: CookieService) {
 +
                // omitted
 +
    this.binder.after('updateHero', (data) => { // the data is vm.heroes
 +
      this.heroes.emit(data); // emits the event to update angular components.
 +
    });
 +
  }
 +
  // omitted
 +
  getHeroes(): Observable<Hero[]> {
 +
    this.binder.command('reload');
 +
    return this.heroes.pipe(
 +
      map((actions) => {
 +
        return actions.map((action: any) => {
 +
          return new Hero({...action});
 +
        });
 +
      })
 +
    );
 +
  }
 +
// omitted
 +
}
 +
</source>
 +
* Line 16: register a callback function which will be called once vm.heroes is changed.
 +
* Line 22: trigger the command from the client, the command just notifies ZK in order to trigger <code>@NotifyCommand</code>.
 +
 
 +
= Send Data Back to ZK =
 +
 
 +
We can use [https://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html#on-clientside binder.command] to send data to the ViewModel.
 +
 
 +
For Example, getHero() method send a hero id to the HeroVM.
 +
 
 +
'''frontend/src/app/modules/heroes/shared/hero.service.ts'''
 +
<source lang="js" highlight="3">
 +
  // omitted
 +
  getHero(id: string): Observable<any> {
 +
    this.binder.command('reload', {'id': id});
 +
    return this.heroes.pipe(
 +
      map((hero) => {
 +
        return new Hero({id, ...hero[0]});
 +
      }),
 +
      first()
 +
    );
 +
  }
 +
// omitted
 +
</source>
 +
* Line 3: Calling zkbind.command with arguments.
 +
 
 +
Then we can use the same command <code>reload</code> as getHeroes() used.
 +
 
 +
'''HeroVM.java'''
 +
<source lang="java" highlight="8">
 +
@NotifyCommand(value = "updateHero", onChange = "_vm_.heroes")
 +
@ToClientCommand({"updateHero"})
 +
@ToServerCommand({"reload", "delete", "add", "update"})
 +
public class HeroVM {
 +
// omitted
 +
@Command
 +
@NotifyChange("heroes")
 +
public void reload(@BindingParam("id") String id) {
 +
if (id == null) {
 +
heroes = dao.queryAll();
 +
} else { //query one
 +
heroes.clear();
 +
heroes.add(dao.queryById(id));
 +
}
 +
}
 +
// omitted
 +
}
 +
</source>
 +
* Line 8: Use @BindingParam to map each argument.
  
 
= Render Angular in a Zul File =
 
= Render Angular in a Zul File =
== With Angular Element  ==
+
Since [https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements/ Custom Element] provides a way to create web components i.e new DOM elements that behave like standard HTML elements, and the Angular Elements package (Angular 6+) allows you to create native custom elements and author web components with Angular.  
Custom Element provides a way to create web components i.e new DOM elements that behave like standard HTML elements.
+
So it looks and behaves like any other HTML element, and does not require any special knowledge of Angular terms or usage conventions.
 +
 
 
To render the Angular app in a zul file, we could use [https://angular.io/guide/elements/ Angular Elements].
 
To render the Angular app in a zul file, we could use [https://angular.io/guide/elements/ Angular Elements].
  
The Angular Elements package (Angular 6+) allows you to create native custom elements and author web components with Angular.
+
We can still render angular in a zhtml file to be processed by ZK (see the previous article [https://www.zkoss.org/wiki/Small_Talks/2017/June/Using_Angular_with_ZK Using Angular with ZK]).  
The @angular/elements package provides a createCustomElement() API that can be used to transform Angular Components to native Custom Elements.
 
  
We need to follow some steps in order to transform the Angular component to a web component.
+
== Prepare a Zul File ==
 +
We have created a <code>hello.zul</code>, an entry page for the whole application.
 +
 
 +
'''hello.zul'''
 +
<source lang="xml" highlight="2">
 +
<zk>
 +
<div id="heroes"
 +
viewModel="@id('vm')@init('org.zkoss.zkangular8.hero.HeroVM')" vflex="1">
 +
</div>
 +
</zk>
 +
</source>
 +
* Line 2: The <code>id</code> property is needed for client binding to get the correct VM.
 +
 
 +
 
 +
== With Angular Element  ==
 +
The @angular/elements package provides a [https://angular.io/api/elements/createCustomElement createCustomElement] API that can be used to transform Angular Components to native Custom Elements.
 +
 
 +
We need to follow some steps in order to transform the Angular root component to a web component.
  
 
'''frontend/src/app/app.module.ts'''
 
'''frontend/src/app/app.module.ts'''
<source lang="js">
+
<source lang="js" highlight="2,3,9,15,16">
 
// omitted
 
// omitted
import { Injector } from '@angular/core';
+
import { Injector } from '@angular/core';
 
import { createCustomElement } from '@angular/elements';
 
import { createCustomElement } from '@angular/elements';
 
// omitted
 
// omitted
Line 43: Line 213:
 
</source>
 
</source>
  
First import Injector from the @angular/core package and createCustomElement from the @angular/elements package. Next, we need to instruct Angular to compile it the component by adding it to the entryComponents array of the main module. Now we are ready to transform the component into a web component by calling the createCustomElement().
+
1.  Import Injector from the @angular/core package and createCustomElement from the @angular/elements package.
 +
 
 +
2.  We need to instruct Angular to compile it the component by adding it to the entryComponents array of the main module.  
 +
 
 +
3.  Transform the component into a web component by calling the createCustomElement(), and defined a custom element using the standard browser API [https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define customElements.define].
  
 
After these steps, we can build this Angular element as any normal Angular project.
 
After these steps, we can build this Angular element as any normal Angular project.
We'll find multiple generated files in the Angular app outputPath after build. Ideally, we must have only one file for using the web component but unfortunately the Angular CLI doesn't provide a way to produce on file for now.
+
We'll find multiple generated files in the Angular app outputPath after ng build.(I modifed the outputPath for this demo, you can see the config in angular.json file)
  
== Prepare a Zul File ==
+
Then place the Web Component into <code>hello.zul</code>.
We have created a <code>hello.zul</code>, an entry page for the whole application.
 
  
 
'''hello.zul'''
 
'''hello.zul'''
<source lang="xml" high="2,3,4,5,6,8,10">
+
<source lang="xml" highlight="1,2,3,4,5,6,7,10">
 
<zk xmlns:n="native">
 
<zk xmlns:n="native">
 
<style src="styles.css" />
 
<style src="styles.css" />
Line 66: Line 239:
 
</zk>
 
</zk>
 
</source>
 
</source>
* Line 2~6: Load the style & JS files which Angular element generated.
+
* Line 1: add the native name space.
* Line 8: The <code>id</code> property is needed for client binding to get the correct VM.
+
* Line 2~7: Load the style & JS files which Angular element generated by "ng build --prod".
 
* Line 10: Place the Angular element here.
 
* Line 10: Place the Angular element here.
  
Line 75: Line 248:
  
 
'''frontend/src/main.browser.ts'''
 
'''frontend/src/main.browser.ts'''
<source lang="js" high="11">
+
<source lang="js" highlight="11">
 
import {enableProdMode, MissingTranslationStrategy} from '@angular/core';
 
import {enableProdMode, MissingTranslationStrategy} from '@angular/core';
 
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
 
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
Line 94: Line 267:
 
</source>
 
</source>
 
* Line 11: We use the <code>zk.afterMount</code> API to ensuring ZK is ready.
 
* Line 11: We use the <code>zk.afterMount</code> API to ensuring ZK is ready.
* Note: I removed the serviceWorker stuff in the demo project since it is not able work with ZK.
+
* Note: We removed the serviceWorker stuff in the demo project since it is not able to work with ZK.
 
 
= Load Data from ZK =
 
 
 
We need to create a View Model to use the client binding feature in Angular.
 
 
 
== Create a ViewModel (VM) in ZK ==
 
 
 
We created an <code>HeroVM.java</code> and defined a property "heroes".
 
 
 
'''HeroVM.java'''
 
<source lang="java">
 
public class HeroVM {
 
// omitted
 
private List<Hero> heroes;
 
 
 
// omitted
 
 
 
public List<Hero> getHeroes() {
 
return heroes;
 
}
 
 
 
public void setHeroes(ArrayList<Hero> heroes) {
 
this.heroes = heroes;
 
}
 
}
 
</source>
 
* Line 7: Declare getter only here so ZK knows <code>heroes</code> is a readonly property of a VM.
 
 
 
== Add Client Binding Annotations ==
 
 
 
To get the <code>vm.heroes</code> from the client, we need to add some annotation to the HeroVM.
 
 
 
'''HeroVM.java'''
 
<source lang="java" high="1,2,3,8">
 
@NotifyCommand(value = "updateHero", onChange = "_vm_.heroes")
 
@ToClientCommand({"updateHero"})
 
@ToServerCommand({"reload", "delete", "add", "update"})
 
public class HeroVM {
 
// omitted
 
@Command
 
@NotifyChange("heroes")
 
public void reload(@BindingParam("id") String id){
 
// do findById(id)
 
}
 
// omitted
 
}
 
</source>
 
* LIne 1: Add a <code>@NotifyCommand</code> and listen to <code>_vm_.heroes</code> changes (<em>_vm_</em> means the VM object)
 
* Line 2: Register a client command so the client can call <code>getHeroes</code>. ZK client binding uses whitelist so you must declare it explicitly. To allow all, simply use an asterisk symbol "*".
 
* Line 3 and 8: We added a simple command to trigger <code>heroes</code> changes for later use. Since it's called from the client, we need to register it by <code>@ToServerCommand</code>.
 
 
 
Once we set, we can get the property from the client. Let's see how we get the <code>heroes</code> from ZK.
 
 
 
== Use Client Binding API in Angular ==
 
 
 
We can use [https://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html#on-clientside binder.after] to get properties of ViewModel from ZK.
 
 
 
......
 
 
 
= Send Data Back to ZK =
 
 
 
We can use [https://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html#on-clientside binder.command] to send data to the ViewModel.
 
<source lang="js">
 
zkbind.$('$main').command('toServerCommand', {key1: value1, key2: value2});
 
</source>
 
  
......
+
Now, we can use an Angular project as a Web Component.
  
 
= Download the Source=  
 
= Download the Source=  

Latest revision as of 03:27, 20 January 2022

eonlee

Author
Leon Lee, Engineer, Potix Corporation
Date
January, 2020
Version
ZK 9.0.0

Overview

Angular is a well-known client-side MVW framework. In the previous article, Using Angular with ZK, we have introduced how to integrate with Angular2.0. Except that Angular has changed drastically since then, what is different this time is that we will talk about how to integrate an existed(or new) Angular project with ZK, and we do not have to modify the components in the Angular project.

In this demo, we start a ZK application with Spring Boot then place an Angular project in the frontend folder.

We will show you how to integrate Angular(6+) with ZK using the client binding API.

For convenience, I use a MIT licensed Angular demo project angular8-example-app to demonstrate.

Zkng-demo.png

Load Data from ZK

We need to create a View Model to use the client binding feature in Angular.

Create a ViewModel (VM) in ZK

We created an HeroVM.java and defined a property "heroes".

HeroVM.java

public class HeroVM {
	// omitted
	private List<Hero> heroes;

	// omitted

	public List<Hero> getHeroes() {
		return heroes;
	}

	public void setHeroes(ArrayList<Hero> heroes) {
		this.heroes = heroes;
	}
}
  • Line 7: Declare getter only here so ZK knows heroes is a readonly property of a VM.

Add Client Binding Annotations

To get the vm.heroes from the client, we need to add some annotation to the HeroVM.

HeroVM.java

@NotifyCommand(value = "updateHero", onChange = "_vm_.heroes")
@ToClientCommand({"updateHero"})
@ToServerCommand({"reload", "delete", "add", "update"})
public class HeroVM {
	// omitted
	@Command
	@NotifyChange("heroes")
	public void reload(@BindingParam("id") String id) {
		if (id == null) {
			heroes = dao.queryAll();
		} else { //query one
			heroes.clear();
			heroes.add(dao.queryById(id));
		}
	}
	// omitted
}
  • LIne 1: Add a @NotifyCommand and listen to _vm_.heroes changes (_vm_ means the VM object)
  • Line 2: Register a client command so the client can call getHeroes. ZK client binding uses whitelist so you must declare it explicitly. To allow all, simply use an asterisk symbol "*".
  • Line 3 and 8: We added a simple command to trigger heroes changes for later use. Since it's called from the client, we need to register it by @ToServerCommand.

Once we set, we can get the property from the client. Let's see how we get the heroes from ZK.

Use Client Binding API in Angular

We can use binder.after to get properties of ViewModel from ZK.

Let's see how to use client binding API in an Angular service, and coexist with observables, so we don't have to modify other Angular components. frontend/src/app/modules/heroes/shared/hero.service.ts

// omitted
declare var zkbind: any;

@Injectable({
  providedIn: 'root'
})
export class HeroService {
  private binder = zkbind.$('$heroes');
  private heroes: EventEmitter<Hero[]> = new EventEmitter();
  // omitted

  constructor(private snackBar: MatSnackBar,
              private i18n: I18n,
              private cookieService: CookieService) {
                // omitted
    this.binder.after('updateHero', (data) => { // the data is vm.heroes
      this.heroes.emit(data); // emits the event to update angular components.
    });
  }
  // omitted
  getHeroes(): Observable<Hero[]> {
    this.binder.command('reload');
    return this.heroes.pipe(
      map((actions) => {
        return actions.map((action: any) => {
          return new Hero({...action});
        });
      })
    );
  }
 // omitted
}
  • Line 16: register a callback function which will be called once vm.heroes is changed.
  • Line 22: trigger the command from the client, the command just notifies ZK in order to trigger @NotifyCommand.

Send Data Back to ZK

We can use binder.command to send data to the ViewModel.

For Example, getHero() method send a hero id to the HeroVM.

frontend/src/app/modules/heroes/shared/hero.service.ts

  // omitted
  getHero(id: string): Observable<any> {
    this.binder.command('reload', {'id': id});
    return this.heroes.pipe(
      map((hero) => {
        return new Hero({id, ...hero[0]});
      }),
      first()
    );
  }
// omitted
  • Line 3: Calling zkbind.command with arguments.

Then we can use the same command reload as getHeroes() used.

HeroVM.java

@NotifyCommand(value = "updateHero", onChange = "_vm_.heroes")
@ToClientCommand({"updateHero"})
@ToServerCommand({"reload", "delete", "add", "update"})
public class HeroVM {
	// omitted
	@Command
	@NotifyChange("heroes")
	public void reload(@BindingParam("id") String id) {
		if (id == null) {
			heroes = dao.queryAll();
		} else { //query one
			heroes.clear();
			heroes.add(dao.queryById(id));
		}
	}
	// omitted
}
  • Line 8: Use @BindingParam to map each argument.

Render Angular in a Zul File

Since Custom Element provides a way to create web components i.e new DOM elements that behave like standard HTML elements, and the Angular Elements package (Angular 6+) allows you to create native custom elements and author web components with Angular. So it looks and behaves like any other HTML element, and does not require any special knowledge of Angular terms or usage conventions.

To render the Angular app in a zul file, we could use Angular Elements.

We can still render angular in a zhtml file to be processed by ZK (see the previous article Using Angular with ZK).

Prepare a Zul File

We have created a hello.zul, an entry page for the whole application.

hello.zul

<zk>
	<div id="heroes"
			 viewModel="@id('vm')@init('org.zkoss.zkangular8.hero.HeroVM')" vflex="1">
	</div>
</zk>
  • Line 2: The id property is needed for client binding to get the correct VM.


With Angular Element

The @angular/elements package provides a createCustomElement API that can be used to transform Angular Components to native Custom Elements.

We need to follow some steps in order to transform the Angular root component to a web component.

frontend/src/app/app.module.ts

// omitted
import { Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
// omitted

@NgModule({
  // omitted
  entryComponents: [
    AppComponent
  ]
})

export class AppModule {
  constructor(private injector: Injector) {
    const appComponent = createCustomElement(AppComponent, { injector : this.injector });
    customElements.define('app-root', appComponent);
}
ngDoBootstrap() {}
}

1. Import Injector from the @angular/core package and createCustomElement from the @angular/elements package.

2. We need to instruct Angular to compile it the component by adding it to the entryComponents array of the main module.

3. Transform the component into a web component by calling the createCustomElement(), and defined a custom element using the standard browser API customElements.define.

After these steps, we can build this Angular element as any normal Angular project. We'll find multiple generated files in the Angular app outputPath after ng build.(I modifed the outputPath for this demo, you can see the config in angular.json file)

Then place the Web Component into hello.zul.

hello.zul

<zk xmlns:n="native">
	<style src="styles.css" />
	<script src="polyfills.js"/>
	<script src="runtime.js"/>
	<script src="scripts.js"/>
	<script src="main.js"/>
	<style src="https://fonts.googleapis.com/icon?family=Material+Icons" />
	<div id="heroes"
			 viewModel="@id('vm')@init('org.zkoss.zkangular8.hero.HeroVM')" vflex="1">
			<n:app-root></n:app-root>
	</div>
</zk>
  • Line 1: add the native name space.
  • Line 2~7: Load the style & JS files which Angular element generated by "ng build --prod".
  • Line 10: Place the Angular element here.

Wait for ZK to Be Mounted

Since we want to use ZK client binding API, it's safe to wait for ZK to finish mounting in Angular. We need to modify main.browser.ts a bit.

frontend/src/main.browser.ts

import {enableProdMode, MissingTranslationStrategy} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

import {AppBrowserModule} from './app/app.browser.module';
import {environment} from './environments/environment';

if (environment.production) {
  enableProdMode();
}
// zk afterMount
window['zk'].afterMount( () => {
  platformBrowserDynamic().bootstrapModule(AppBrowserModule, {
    missingTranslation: MissingTranslationStrategy.Error,
  }).catch(err => console.log(err));
});
  • Line 11: We use the zk.afterMount API to ensuring ZK is ready.
  • Note: We removed the serviceWorker stuff in the demo project since it is not able to work with ZK.

Now, we can use an Angular project as a Web Component.

Download the Source

You can access the complete source at GitHub. Follow the instructions to start a server and open it in a browser.


Comments



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