Smalltalk-draft
Leon Lee, Engineer, Potix Corporation
January, 2020
ZK 9.0.0
Overview
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(6+) with ZK using the client binding API.
We’ll assume that you have some familiarity with TypeScript and rxjs since Angular using them a lot.
For convenience, I use a MIT licensed Angular demo project angular8-example-app to demonstrate.
Render Angular in a Zul File
With Angular Element
Custom Element provides a way to create web components i.e new DOM elements that behave like standard HTML elements. To render the Angular app in a zul file, we could use Angular Elements.
The Angular Elements package (Angular 6+) allows you to create native custom elements and author web components with Angular. 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.
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)
Prepare a Zul File
We have created a hello.zul
, an entry page for the whole application.
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 2~6: Load the style & JS files which Angular element generated by "ng build --prod".
- Line 8: The
id
property is needed for client binding to get the correct VM. - 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: I 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 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) {
//find all
} else {
//find by 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.
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) {
//find all
} else {
//find by id
}
}
// omitted
}
- Line 8: Use @BindingParam to map each argument.
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. |