Smalltalk-draft"
Line 9: | Line 9: | ||
[https://reactjs.org/ React] is a JavaScript library for building user interfaces. We can use React as a front-end UI and use ZK as a pure backend. In this small talk, I 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. | [https://reactjs.org/ React] is a JavaScript library for building user interfaces. We can use React as a front-end UI and use ZK as a pure backend. In this small talk, I 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. | ||
− | For convenience, I use an | + | For convenience, I use an MIT licensed React demo project [https://github.com/jeffersonRibeiro/react-shopping-cart/ react-shopping-cart] to demonstrate. |
− | = Load | + | [[File:React-Shopping-mall.png|540px|thumb|center]] |
+ | |||
+ | = Render React in a Zul File = | ||
+ | |||
+ | We have created an <code>index.zul</code>, an entry page for the whole application. | ||
+ | |||
+ | '''index.zul''' | ||
+ | <source lang="xml" high="1,3,4"> | ||
+ | <?script src="/bundle.js"?> | ||
+ | <zk xmlns:n="native"> | ||
+ | <div id="main" viewModel="@id('vm') @init('react.vm.IndexVM')"> | ||
+ | <n:div id="root"/> | ||
+ | </div> | ||
+ | </zk> | ||
+ | </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 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. | ||
+ | |||
+ | == 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''' | ||
+ | <source lang="js" high="12"> | ||
+ | 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') | ||
+ | ); | ||
+ | }); | ||
+ | } | ||
+ | </source> | ||
+ | * Line 12: We use the <code>zk.afterMount</code> API to execute <code>ReactDOM.render</code>, ensuring ZK is ready. | ||
+ | |||
+ | = Load Data from ZK = | ||
We need to create a View Model to use the client binding feature in React. | We need to create a View Model to use the client binding feature in React. | ||
Line 42: | Line 90: | ||
* Line 14: Declare getter only here so ZK knows <code>products</code> is a readonly property of a VM. | * Line 14: Declare getter only here so ZK knows <code>products</code> is a readonly property of a VM. | ||
− | == | + | == Add Client Binding Annotations == |
In order to get the <code>vm.products</code> from the client, we need to add some annotation to the IndexVM. | In order to get the <code>vm.products</code> from the client, we need to add some annotation to the IndexVM. | ||
'''IndexVM.java''' | '''IndexVM.java''' | ||
− | <source lang="java" high="2,5"> | + | <source lang="java" high="2,4,5,11"> |
@NotifyCommands({ | @NotifyCommands({ | ||
@NotifyCommand(value = "getProducts", onChange = "_vm_.products") | @NotifyCommand(value = "getProducts", onChange = "_vm_.products") | ||
Line 55: | Line 103: | ||
@VariableResolver(DelegatingVariableResolver.class) | @VariableResolver(DelegatingVariableResolver.class) | ||
public class IndexVM { | public class IndexVM { | ||
− | + | // omitted | |
+ | @Command | ||
+ | @NotifyChange("products") | ||
+ | public void tipProducts() { | ||
+ | // just triggering getProducts | ||
+ | } | ||
+ | // omitted | ||
</source> | </source> | ||
* 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 5: Register a client command so the client can call <code>getProducts</code>. ZK client binding uses whitelist so no command is allowed to be called from the client by default. | * Line 5: Register a client command so the client can call <code>getProducts</code>. ZK client binding uses whitelist so no command is allowed to be called from the client by default. | ||
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. | ||
− | == Use Client | + | == Use Client Binding API in React == |
− | + | We can use [http://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. | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | We can use [http://books.zkoss.org/zk-mvvm-book/8.0/data_binding/client_binding_api.html#on-clientside binder.after] to get | ||
<source lang="js"> | <source lang="js"> | ||
Line 83: | Line 125: | ||
// 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 | ||
+ | // one-way binding: vm.products -> this function | ||
}); | }); | ||
</source> | </source> | ||
− | The react-shopping-cart loads JSON data a server. A method <code>fetchProducts</code> initiates <code>axios.get</code> to make an | + | The react-shopping-cart loads JSON data from a server. A method <code>fetchProducts</code> initiates <code>axios.get</code> to make an Ajax get request. |
− | ''' | + | '''frontend/src/services/shelf/actions.js''' |
<source lang="js"> | <source lang="js"> | ||
export const fetchProducts = (filters, sortBy, callback) => dispatch => { | export const fetchProducts = (filters, sortBy, callback) => dispatch => { | ||
Line 98: | Line 141: | ||
</source> | </source> | ||
− | We can use client binding API to request with ZK instead. I wrote a utility class to handle it for convenience | + | We can use client binding API to request with ZK instead. I wrote a utility class to handle it for convenience (the full source is available in the demo project) |
− | '''zkBinder.js''' | + | '''frontend/src/util/zkBinder.js''' |
− | <source lang="js"> | + | <source lang="js" high="7"> |
function getBinder(name) { | function getBinder(name) { | ||
return window.zkbind && window.zkbind.$(name); | return window.zkbind && window.zkbind.$(name); | ||
Line 107: | Line 150: | ||
export default { | export default { | ||
− | + | // omitted | |
+ | init(name, command, event) { | ||
let binder = getBinder(name); | let binder = getBinder(name); | ||
if (binder) { | if (binder) { | ||
− | + | return new Promise((resolve, reject) => { | |
− | + | let fn = res => { | |
+ | this.unAfter(name, event, fn); | ||
+ | resolve(res); | ||
+ | }; | ||
+ | let result = this.after(name, event, fn); | ||
+ | if (!result) | ||
+ | reject(new Error('Binder not found')); | ||
+ | this.command(name, command); | ||
+ | }) | ||
} | } | ||
− | return | + | return Promise.reject(new Error('Binder not found')); |
− | } | + | } |
− | + | }; | |
</source> | </source> | ||
+ | Line 7: To simulate a request, I need to trigger a command from the client first, the command just notifies ZK in order to trigger <code>@NotifyCommand</code>. Then we'll remove the function binding by calling <code>unAfter</code> method. | ||
− | + | '''frontend/src/services/shelf/actions.js''' | |
+ | <source lang="js" high="4"> | ||
+ | import zkBinder from "../../util/zkBinder"; | ||
+ | // omitted | ||
+ | export const fetchProducts = (filters, sortBy, callback) => dispatch => { | ||
+ | return zkBinder.init('$main', 'tipProducts', 'getProducts') | ||
+ | .then(products => { | ||
+ | // omitted | ||
+ | </source> | ||
+ | * Line 4: Replace the axios call with ZK. | ||
+ | = Submit Data 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 [http://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}); | |
− | + | </source> | |
− | . | + | |
− | . | + | '''frontend/src/components/FloatCart/index.js''' |
− | + | <source lang="js" high="18"> | |
+ | // 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 | ||
+ | </source> | ||
+ | * Line 18: Calling zkbind.command with arguments. | ||
+ | |||
+ | Then we need to add <code>placeOrder</code> command in the ViewModel. Don't forget to add it to <code>@ToServerCommand</code> whitelist. | ||
+ | |||
+ | '''IndexVM.java''' | ||
+ | <source lang="java" high="3,7"> | ||
// omitted | // 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... | ||
+ | } | ||
</source> | </source> | ||
+ | * Line 3: Use <code>@BindingParam</code> 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. | |
= Download the Source= | = Download the Source= | ||
− | You can access the complete source at [https://github.com/zkoss-demo/ | + | You can access the complete source at [https://github.com/zkoss-demo/client-binding-demo-react GitHub]. |
+ | |||
+ | Follow the instructions to start a server and open it in a browser. | ||
− | = Other Front-End Frameworks | + | = Other Front-End Frameworks Integration = |
* Angular | * Angular | ||
* Vue.js | * Vue.js |
Revision as of 09:27, 7 January 2020
Rudy Huang, Engineer, Potix Corporation
January, 2020
ZK 9.0.0
Overview
React is a JavaScript library for building user interfaces. We can use React as a front-end UI and use ZK as a pure backend. In this small talk, I will show you how to integrate React with ZK using the client binding API step by step.
For convenience, I use an MIT licensed React demo project react-shopping-cart to demonstrate.
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 is loaded from a database.
IndexVM.java
@VariableResolver(DelegatingVariableResolver.class)
public class IndexVM {
// omitted
@WireVariable
private ProductService productService;
private List<ProductDto> products;
// omitted
@Init
public void init() {
products = productService.getProducts();
}
public List<ProductDto> getProducts() {
return products;
}
// omitted
- Line 11: We use Spring + Hibernate to communicate with the database. A service instance is needed to be injected in the VM by adding a
@WireVariable
annotation. - Line 14: Declare getter only here so ZK knows
products
is a readonly property of a VM.
Add Client Binding Annotations
In order to get the vm.products
from the client, we need to add some annotation to the IndexVM.
IndexVM.java
@NotifyCommands({
@NotifyCommand(value = "getProducts", onChange = "_vm_.products")
})
@ToServerCommand({"tipProducts", "placeOrder"})
@ToClientCommand({"getProducts"})
@VariableResolver(DelegatingVariableResolver.class)
public class IndexVM {
// omitted
@Command
@NotifyChange("products")
public void tipProducts() {
// just triggering 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: Register a client command so the client can call
getProducts
. ZK client binding uses whitelist so no command is allowed to be called from the client by default.
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('getProducts', 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. I wrote a utility class to handle it for convenience (the full source is available in the demo project)
frontend/src/util/zkBinder.js
function getBinder(name) {
return window.zkbind && window.zkbind.$(name);
}
export default {
// omitted
init(name, command, event) {
let binder = getBinder(name);
if (binder) {
return new Promise((resolve, reject) => {
let fn = res => {
this.unAfter(name, event, fn);
resolve(res);
};
let result = this.after(name, event, fn);
if (!result)
reject(new Error('Binder not found'));
this.command(name, command);
})
}
return Promise.reject(new Error('Binder not found'));
}
};
Line 7: To simulate a request, I need to trigger a command from the client first, the command just notifies ZK in order to trigger @NotifyCommand
. Then we'll remove the function binding by calling unAfter
method.
frontend/src/services/shelf/actions.js
import zkBinder from "../../util/zkBinder";
// omitted
export const fetchProducts = (filters, sortBy, callback) => dispatch => {
return zkBinder.init('$main', 'tipProducts', 'getProducts')
.then(products => {
// omitted
- Line 4: Replace the axios call with ZK.
Submit Data 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.
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. |