Smalltalk-draft"
m (correct highlight (via JWB)) |
|||
(8 intermediate revisions by one other user not shown) | |||
Line 7: | Line 7: | ||
= Overview = | = Overview = | ||
− | [https://reactjs.org/ React] is a JavaScript library for building user interfaces | + | ZK already provides many out-of-the-box components to fit your needs. But in some cases, you might need to integrate with other web technology like [https://reactjs.org/ React], which is a JavaScript library for building user interfaces. In this small talk, we will show you how to integrate React with ZK using the [https://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html client binding API] step by step. By using this approach, the front-end can use anything written in JavaScript. That makes React integrating with an existing ZK project easy. |
− | For convenience, | + | Here are some benefits why you would like to use this approach: |
+ | * Use React components (for example, Material-UI) in your ZK applications. | ||
+ | * Lower memory consumption since ZK doesn't need to maintain the state of components in the server. | ||
+ | |||
+ | For convenience, we use an MIT licensed React demo project [https://github.com/jeffersonRibeiro/react-shopping-cart/ react-shopping-cart] to demonstrate. | ||
[[File:React-Shopping-mall.png|540px|thumb|center]] | [[File:React-Shopping-mall.png|540px|thumb|center]] | ||
+ | |||
+ | We created an example project containing both client-side and server-side resources. For client-side resources, we put them into the <code>frontend</code> folder. | ||
+ | |||
+ | [[File:Client-binding-demo-react-project-structore.png]] | ||
= Render React in a Zul File = | = Render React in a Zul File = | ||
Line 18: | Line 26: | ||
'''index.zul''' | '''index.zul''' | ||
− | <source lang="xml" | + | <source lang="xml" highlight="1,3,4"> |
<?script src="/bundle.js"?> | <?script src="/bundle.js"?> | ||
<zk xmlns:n="native"> | <zk xmlns:n="native"> | ||
Line 26: | Line 34: | ||
</zk> | </zk> | ||
</source> | </source> | ||
− | * Line 1: Load <code>bundle.js</code> packed by Webpack. You must configure Webpack to build a single bundle.js. See the demo source for details. | + | * Line 1: Load <code>bundle.js</code> packed by Webpack. You must configure Webpack to build a single bundle.js. See the [https://github.com/zkoss-demo/client-binding-demo-react/blob/master/frontend/config-overrides.js demo source] for details. |
* Line 3: The <code>id</code> property is needed for client binding to get the correct VM. | * Line 3: The <code>id</code> property is needed for client binding to get the correct VM. | ||
* Line 4: We have created a native DIV element for React to render. | * Line 4: We have created a native DIV element for React to render. | ||
Line 35: | Line 43: | ||
'''frontend/src/index.js''' | '''frontend/src/index.js''' | ||
− | <source lang="js" | + | <source lang="js" highlight="12"> |
import 'babel-polyfill' | import 'babel-polyfill' | ||
import React from 'react'; | import React from 'react'; | ||
Line 65: | Line 73: | ||
== Create a ViewModel (VM) in ZK == | == Create a ViewModel (VM) in ZK == | ||
− | We created an <code>IndexVM.java</code> and defined a property "products". The <code>products</code> property | + | We created an <code>IndexVM.java</code> and defined a property "products". The <code>products</code> property can be loaded from a database. |
'''IndexVM.java''' | '''IndexVM.java''' | ||
− | <source lang="java" | + | <source lang="java" highlight="4,6"> |
− | + | // omitted | |
public class IndexVM { | public class IndexVM { | ||
// omitted | // omitted | ||
− | |||
− | |||
− | |||
private List<ProductDto> products; | private List<ProductDto> products; | ||
// omitted | // omitted | ||
− | |||
− | |||
− | |||
− | |||
− | |||
public List<ProductDto> getProducts() { | public List<ProductDto> getProducts() { | ||
return products; | return products; | ||
Line 87: | Line 87: | ||
// omitted | // omitted | ||
</source> | </source> | ||
− | * Line | + | * Line 4: Declare a property |
− | * Line | + | * Line 6: Declare a getter only here so ZK knows <code>products</code> is a readonly property of a VM. |
== Add Client Binding Annotations == | == Add Client Binding Annotations == | ||
Line 95: | Line 95: | ||
'''IndexVM.java''' | '''IndexVM.java''' | ||
− | <source lang="java" | + | <source lang="java" highlight="2,4,5,11"> |
@NotifyCommands({ | @NotifyCommands({ | ||
− | @NotifyCommand(value = " | + | @NotifyCommand(value = "loadProductsDone", onChange = "_vm_.products") |
}) | }) | ||
− | @ToServerCommand({" | + | @ToServerCommand({"loadProducts", "placeOrder"}) |
− | @ToClientCommand({" | + | @ToClientCommand({"loadProductsDone"}) |
@VariableResolver(DelegatingVariableResolver.class) | @VariableResolver(DelegatingVariableResolver.class) | ||
public class IndexVM { | public class IndexVM { | ||
Line 106: | Line 106: | ||
@Command | @Command | ||
@NotifyChange("products") | @NotifyChange("products") | ||
− | public void | + | public void loadProducts() { |
− | + | products = productService.getProducts(); | |
} | } | ||
// omitted | // omitted | ||
Line 113: | Line 113: | ||
* LIne 2: Add a <code>@NotifyCommand</code> and listen to <code>_vm_.products</code> changes (<em>_vm_</em> means the VM object) | * LIne 2: Add a <code>@NotifyCommand</code> and listen to <code>_vm_.products</code> changes (<em>_vm_</em> means the VM object) | ||
* Line 4 and 11: We added a simple command to trigger <code>products</code> changes for later use. Since it's called from the client, we need to register it by <code>@ToServerCommand</code>. | * Line 4 and 11: We added a simple command to trigger <code>products</code> changes for later use. Since it's called from the client, we need to register it by <code>@ToServerCommand</code>. | ||
− | * Line 5: | + | * Line 5: Expose the 'loadProductDone'-command to the client side. ZK's server side binding uses a white list so you must declare it explicitly (or expose all commands to the client side using the wildcard "*"). |
Once we set, we can get the property from the client. Let's see how we get the <code>products</code> from ZK. | Once we set, we can get the property from the client. Let's see how we get the <code>products</code> from ZK. | ||
Line 119: | Line 119: | ||
== Use Client Binding API in React == | == Use Client Binding API in React == | ||
− | We can use [ | + | 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. |
<source lang="js"> | <source lang="js"> | ||
− | zkbind.$('$main').after(' | + | zkbind.$('$main').after('loadProductsDone', function (data) { |
// the data is vm.products | // the data is vm.products | ||
// and this function will be called each time vm.products is changed | // and this function will be called each time vm.products is changed | ||
Line 141: | Line 141: | ||
</source> | </source> | ||
− | We can use client binding API to request with ZK instead. | + | We can use client binding API to request with ZK instead. We wrote a utility class to handle it for convenience (the full source is available in the demo project) |
'''frontend/src/util/zkBinder.js''' | '''frontend/src/util/zkBinder.js''' | ||
− | <source lang="js" | + | <source lang="js" highlight="16,20"> |
+ | import { EventEmitter } from 'events'; | ||
+ | |||
function getBinder(name) { | function getBinder(name) { | ||
return window.zkbind && window.zkbind.$(name); | return window.zkbind && window.zkbind.$(name); | ||
Line 151: | Line 153: | ||
export default { | export default { | ||
// omitted | // omitted | ||
− | init(name, command, event) { | + | init(name, command, commandArguments, event) { |
+ | this.init.emitters = this.init.emitters || {}; | ||
let binder = getBinder(name); | let binder = getBinder(name); | ||
if (binder) { | if (binder) { | ||
− | + | let emitter = this.init.emitters[event]; | |
− | + | if (!emitter) { | |
− | + | this.init.emitters[event] = emitter = new EventEmitter(); | |
− | + | binder.after(event, data => { if (data) emitter.emit('data', data) }); | |
− | + | } | |
− | + | return new Promise(resolve => { | |
− | + | emitter.once('data', resolve); | |
− | + | binder.command(command, commandArguments); | |
− | + | }); | |
− | }) | ||
} | } | ||
return Promise.reject(new Error('Binder not found')); | return Promise.reject(new Error('Binder not found')); | ||
Line 169: | Line 171: | ||
}; | }; | ||
</source> | </source> | ||
− | Line | + | * Line 16: Listen to <code>loadProductsDone</code>, and only process the data if it is not null or undefined. |
+ | * Line 20: To simulate a request, we need to trigger command from the client first. We can call <code>loadProducts</code> and wait for <code>loadProductsDone</code> to be triggered. | ||
'''frontend/src/services/shelf/actions.js''' | '''frontend/src/services/shelf/actions.js''' | ||
− | <source lang="js" | + | <source lang="js" highlight="4"> |
import zkBinder from "../../util/zkBinder"; | import zkBinder from "../../util/zkBinder"; | ||
// omitted | // omitted | ||
export const fetchProducts = (filters, sortBy, callback) => dispatch => { | export const fetchProducts = (filters, sortBy, callback) => dispatch => { | ||
− | return zkBinder.init('$main', ' | + | return zkBinder.init('$main', 'loadProducts', {filterSizes: filters}, 'loadProductsDone') |
.then(products => { | .then(products => { | ||
// omitted | // omitted | ||
Line 186: | Line 189: | ||
The checkout feature of react-shopping-cart isn't complete. It just shows an alert dialog to display the subtotal. We can complete it by using client binding API to send the cart data back to ZK. | The checkout feature of react-shopping-cart isn't complete. It just shows an alert dialog to display the subtotal. We can complete it by using client binding API to send the cart data back to ZK. | ||
− | We can use [ | + | 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"> | <source lang="js"> | ||
zkbind.$('$main').command('toServerCommand', {key1: value1, key2: value2}); | zkbind.$('$main').command('toServerCommand', {key1: value1, key2: value2}); | ||
Line 192: | Line 195: | ||
'''frontend/src/components/FloatCart/index.js''' | '''frontend/src/components/FloatCart/index.js''' | ||
− | <source lang="js" | + | <source lang="js" highlight="18"> |
// omitted | // omitted | ||
class FloatCart extends Component { | class FloatCart extends Component { | ||
Line 221: | Line 224: | ||
'''IndexVM.java''' | '''IndexVM.java''' | ||
− | <source lang="java" | + | <source lang="java" highlight="3,7"> |
// omitted | // omitted | ||
@Command | @Command | ||
Line 238: | Line 241: | ||
All done. Once the user clicks the checkout button, ZK will receive the cart result. | All done. Once the user clicks the checkout button, ZK will receive the cart result. | ||
+ | |||
+ | = Conclusion = | ||
+ | This article shows how to integrate React with ZK. With the help of MVVM Client-Binding API, ZK can pass data to React and get the data from React. | ||
= Download the Source= | = Download the Source= |
Latest revision as of 04:15, 20 January 2022
Rudy Huang, Engineer, Potix Corporation
January, 2020
ZK 9.0.0
Overview
ZK already provides many out-of-the-box components to fit your needs. But in some cases, you might need to integrate with other web technology like React, which is a JavaScript library for building user interfaces. In this small talk, we will show you how to integrate React with ZK using the client binding API step by step. By using this approach, the front-end can use anything written in JavaScript. That makes React integrating with an existing ZK project easy.
Here are some benefits why you would like to use this approach:
- Use React components (for example, Material-UI) in your ZK applications.
- Lower memory consumption since ZK doesn't need to maintain the state of components in the server.
For convenience, we use an MIT licensed React demo project react-shopping-cart to demonstrate.
We created an example project containing both client-side and server-side resources. For client-side resources, we put them into the frontend
folder.
Render React in a Zul File
We have created an index.zul
, an entry page for the whole application.
index.zul
<?script src="/bundle.js"?>
<zk xmlns:n="native">
<div id="main" viewModel="@id('vm') @init('react.vm.IndexVM')">
<n:div id="root"/>
</div>
</zk>
- Line 1: Load
bundle.js
packed by Webpack. You must configure Webpack to build a single bundle.js. See the demo source for details. - Line 3: The
id
property is needed for client binding to get the correct VM. - Line 4: We have created a native DIV element for React to render.
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 React. We need to modify index.js a bit.
frontend/src/index.js
import 'babel-polyfill'
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import Root from './Root';
import './index.scss';
let zk = window.zk;
if (zk) {
zk.afterMount(function () {
ReactDOM.render(
<Root>
<App />
</Root>,
document.getElementById('root')
);
});
}
- Line 12: We use the
zk.afterMount
API to executeReactDOM.render
, ensuring ZK is ready.
Load Data from ZK
We need to create a View Model to use the client binding feature in React.
Create a ViewModel (VM) in ZK
We created an IndexVM.java
and defined a property "products". The products
property can be loaded from a database.
IndexVM.java
// omitted
public class IndexVM {
// omitted
private List<ProductDto> products;
// omitted
public List<ProductDto> getProducts() {
return products;
}
// omitted
- Line 4: Declare a property
- Line 6: Declare a getter only here so ZK knows
products
is a readonly property of a VM.
Add Client Binding Annotations
To get the vm.products
from the client, we need to add some annotation to the IndexVM.
IndexVM.java
@NotifyCommands({
@NotifyCommand(value = "loadProductsDone", onChange = "_vm_.products")
})
@ToServerCommand({"loadProducts", "placeOrder"})
@ToClientCommand({"loadProductsDone"})
@VariableResolver(DelegatingVariableResolver.class)
public class IndexVM {
// omitted
@Command
@NotifyChange("products")
public void loadProducts() {
products = productService.getProducts();
}
// omitted
- LIne 2: Add a
@NotifyCommand
and listen to_vm_.products
changes (_vm_ means the VM object) - Line 4 and 11: We added a simple command to trigger
products
changes for later use. Since it's called from the client, we need to register it by@ToServerCommand
. - Line 5: Expose the 'loadProductDone'-command to the client side. ZK's server side binding uses a white list so you must declare it explicitly (or expose all commands to the client side using the wildcard "*").
Once we set, we can get the property from the client. Let's see how we get the products
from ZK.
Use Client Binding API in React
We can use binder.after to get properties of ViewModel from ZK.
zkbind.$('$main').after('loadProductsDone', function (data) {
// the data is vm.products
// and this function will be called each time vm.products is changed
// one-way binding: vm.products -> this function
});
The react-shopping-cart loads JSON data from a server. A method fetchProducts
initiates axios.get
to make an Ajax get request.
frontend/src/services/shelf/actions.js
export const fetchProducts = (filters, sortBy, callback) => dispatch => {
return axios
.get(productsAPI)
.then(res => {
let { products } = res.data;
// omitted
We can use client binding API to request with ZK instead. We wrote a utility class to handle it for convenience (the full source is available in the demo project)
frontend/src/util/zkBinder.js
import { EventEmitter } from 'events';
function getBinder(name) {
return window.zkbind && window.zkbind.$(name);
}
export default {
// omitted
init(name, command, commandArguments, event) {
this.init.emitters = this.init.emitters || {};
let binder = getBinder(name);
if (binder) {
let emitter = this.init.emitters[event];
if (!emitter) {
this.init.emitters[event] = emitter = new EventEmitter();
binder.after(event, data => { if (data) emitter.emit('data', data) });
}
return new Promise(resolve => {
emitter.once('data', resolve);
binder.command(command, commandArguments);
});
}
return Promise.reject(new Error('Binder not found'));
}
};
- Line 16: Listen to
loadProductsDone
, and only process the data if it is not null or undefined. - Line 20: To simulate a request, we need to trigger command from the client first. We can call
loadProducts
and wait forloadProductsDone
to be triggered.
frontend/src/services/shelf/actions.js
import zkBinder from "../../util/zkBinder";
// omitted
export const fetchProducts = (filters, sortBy, callback) => dispatch => {
return zkBinder.init('$main', 'loadProducts', {filterSizes: filters}, 'loadProductsDone')
.then(products => {
// omitted
- Line 4: Replace the axios call with ZK.
Send Data Back to ZK
The checkout feature of react-shopping-cart isn't complete. It just shows an alert dialog to display the subtotal. We can complete it by using client binding API to send the cart data back to ZK.
We can use binder.command to send data to the ViewModel.
zkbind.$('$main').command('toServerCommand', {key1: value1, key2: value2});
frontend/src/components/FloatCart/index.js
// omitted
class FloatCart extends Component {
// omitted
proceedToCheckout = () => {
const {
totalPrice,
productQuantity,
currencyFormat,
currencyId
} = this.props.cartTotal;
if (!productQuantity) {
alert('Add some product in the cart!');
} else {
let products = this.props.cartProducts
.map(p => ({[p.id]: p.quantity}))
.reduce((acc, curr) => Object.assign(acc, curr), {});
zkBinder.command('$main', 'placeOrder',
{format: currencyFormat, price: totalPrice, id: currencyId, products: products});
}
};
// omitted
- Line 18: Calling zkbind.command with arguments.
Then we need to add placeOrder
command in the ViewModel. Don't forget to add it to @ToServerCommand
whitelist.
IndexVM.java
// omitted
@Command
public void placeOrder(@BindingParam("format") String format,
@BindingParam("price") double price,
@BindingParam("id") String id,
@BindingParam("products") Map<String, Integer> cartProducts) {
Clients.showNotification(String.format("Checkout - Subtotal: %s %.2f", format, price));
this.cartProducts = cartProducts;
this.cartProducts.forEach((p, q) -> LOG.info("Order Product#{}, Quantity: {}", p, q));
// Save to DB...
}
- Line 3: Use
@BindingParam
to map each argument. - Line 7: We use ZK notification to show the result.
All done. Once the user clicks the checkout button, ZK will receive the cart result.
Conclusion
This article shows how to integrate React with ZK. With the help of MVVM Client-Binding API, ZK can pass data to React and get the data from React.
Download the Source
You can access the complete source at GitHub. Follow the instructions to start a server and open it in a browser.
Other Front-End Frameworks Integration
- Angular
- Vue.js
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |