SlideShare a Scribd company logo
#JCConf
當 ZK 遇⾒見 Front-End
朱祁源 (James Chu)
marchuqq@gmail.com
GitHub: DevChu
Demo Code: DevChu/Jcconf2015-ZKDemo
About ZK
About ZK
• Open source Java/AJAX framework
About ZK
• Open source Java/AJAX framework
• 200+ Components, Event driven
About ZK
• Open source Java/AJAX framework
• 200+ Components, Event driven
• Multiple browsers/platforms support
Concept of ZK ?
當ZK遇見Front-End
<html>
<head>
<meta ... />
<title></title>
</head>
<body>
<div>
...
</div>
<span>
...
</span>
...
</body>
</html>
<html>
<head>
<meta ... />
<title></title>
</head>
<body>
<div>
...
</div>
<span>
...
</span>
...
</body>
</html>
HTML DOM
Tree
Browser Server
HTML Java
Browser Server
HTML
JSP/Servlet
Java
Browser Server
HTML
JSP/ServletJavaScript
Java
<html>
<head>
<meta ... />
<title></title>
</head>
<body>
<div>
...
</div>
<span>
...
</span>
...
</body>
</html>
HTML DOM
Tree
<html>
<head>
<meta ... />
<title></title>
</head>
<body>
<div>
...
</div>
<span>
...
</span>
...
</body>
</html>
<html>
<head>
<meta ... />
<title></title>
</head>
<body>
<div>
...
</div>
<span>
...
</span>
...
</body>
</html>
Grid
Form
Browser Server
HTML Java
Browser Server
HTML Java
ZK Component
Browser Server
HTML
ZK
Widget
Java
ZK Component
Browser Server
HTML
ZK
Widget
Java
ZK Component
AJAX
Browser Server
HTML
ZK
Widget
Java
ZK Component
AJAX
Start ZK
• Manually
• Download Jar (SourceForge, Github)

• Maven Archetype (recommended!)
• Setting and Choose an ZK archetype
Concepts of ZK
• MVC and MVVM
Concepts of ZK
• MVC and MVVM
• MVC
• Model - View - Controller
當ZK遇見Front-End
當ZK遇見Front-End
當ZK遇見Front-End
當ZK遇見Front-End
MVC in ZK
• View -> ZUL (XML-Base), ZHTML (XHTML)
• *.zul, *.zhtml
MVC in ZK
• View -> ZUL (XML-Base), ZHTML (XHTML)
• *.zul, *.zhtml
• Controller -> Composer
• *.java
MVC in ZK
• View -> ZUL (XML-Base), ZHTML (XHTML)
• *.zul, *.zhtml
• Controller -> Composer
• *.java
• Model -> Business logic
• Entity, DB Access…
MVC in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVC" apply="org.zkoss.DemoComposer">
<vbox>
<div>
Input: <textbox id="input" />
</div>
<div>
Output: <label id="output" />
</div>
<div>
<button id="ok" label="Submit" />
<button id="clear" label="Clear" />
</div>
</vbox>
</window>
MVC in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVC" apply="org.zkoss.DemoComposer">
<vbox>
<div>
Input: <textbox id="input" />
</div>
<div>
Output: <label id="output" />
</div>
<div>
<button id="ok" label="Submit" />
<button id="clear" label="Clear" />
</div>
</vbox>
</window>
MVC in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVC" apply="org.zkoss.DemoComposer">
<vbox>
<div>
Input: <textbox id="input" />
</div>
<div>
Output: <label id="output" />
</div>
<div>
<button id="ok" label="Submit" />
<button id="clear" label="Clear" />
</div>
</vbox>
</window>
MVC in ZK
MVC in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVC" apply="org.zkoss.DemoComposer">
<vbox>
<div>
Input: <textbox id="input" />
</div>
<div>
Output: <label id="output" />
</div>
<div>
<button id="ok" label="Submit" />
<button id="clear" label="Clear" />
</div>
</vbox>
</window>
MVC in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVC" apply="org.zkoss.DemoComposer">
<vbox>
<div>
Input: <textbox id="input" />
</div>
<div>
Output: <label id="output" />
</div>
<div>
<button id="ok" label="Submit" />
<button id="clear" label="Clear" />
</div>
</vbox>
</window>
MVC in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVC" apply="org.zkoss.DemoComposer">
<vbox>
<div>
Input: <textbox id="input" />
</div>
<div>
Output: <label id="output" />
</div>
<div>
<button id="ok" label="Submit" />
<button id="clear" label="Clear" />
</div>
</vbox>
</window>
public class DemoComposer extends SelectorComposer<Window> {
@Wire
Textbox input;
@Wire
Label output;
@Listen("onClick=#ok")
public void submit() {
output.setValue(input.getValue());
//Business logic, access DB…
}
@Listen("onClick=#clear")
public void clear() {
input.setValue("");
output.setValue("");
}
}
MVC in ZK
public class DemoComposer extends SelectorComposer<Window> {
@Wire
Textbox input;
@Wire
Label output;
@Listen("onClick=#ok")
public void submit() {
output.setValue(input.getValue());
//Business logic, access DB…
}
@Listen("onClick=#clear")
public void clear() {
input.setValue("");
output.setValue("");
}
}
MVC in ZK
public class DemoComposer extends SelectorComposer<Window> {
@Wire
Textbox input;
@Wire
Label output;
@Listen("onClick=#ok")
public void submit() {
output.setValue(input.getValue());
//Business logic, access DB…
}
@Listen("onClick=#clear")
public void clear() {
input.setValue("");
output.setValue("");
}
}
MVC in ZK
public class DemoComposer extends SelectorComposer<Window> {
@Wire
Textbox input;
@Wire
Label output;
@Listen("onClick=#ok")
public void submit() {
output.setValue(input.getValue());
//Business logic, access DB…
}
@Listen("onClick=#clear")
public void clear() {
input.setValue("");
output.setValue("");
}
}
MVC in ZK
MVC in ZK
Concepts of ZK
• MVC and MVVM
Concepts of ZK
• MVC and MVVM
• MVVM
• Model - View - ViewModel
當ZK遇見Front-End
當ZK遇見Front-End
MVVM in ZK
• View -> ZUL (with state)
• @init, @load, @save, @command…
MVVM in ZK
• View -> ZUL (with state)
• @init, @load, @save, @command…
• ViewModel -> ViewModel Class
• Java Bean
• Command
MVVM in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVVM" apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm')@init('org.zkoss.DemoViewModel')">
<vbox>
<div>
Input: <textbox value="@bind(vm.input)" />
</div>
<div>
Output: <label value="@load(vm.output)" />
</div>
<div>
<button label="Submit" onClick="@command('save')"/>
<button label="Clear" onClick="@command('clear')"/>
</div>
</vbox>
</window>
MVVM in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVVM" apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm')@init('org.zkoss.DemoViewModel')">
<vbox>
<div>
Input: <textbox value="@bind(vm.input)" />
</div>
<div>
Output: <label value="@load(vm.output)" />
</div>
<div>
<button label="Submit" onClick="@command('save')"/>
<button label="Clear" onClick="@command('clear')"/>
</div>
</vbox>
</window>
MVVM in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVVM" apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm')@init('org.zkoss.DemoViewModel')">
<vbox>
<div>
Input: <textbox value="@bind(vm.input)" />
</div>
<div>
Output: <label value="@load(vm.output)" />
</div>
<div>
<button label="Submit" onClick="@command('save')"/>
<button label="Clear" onClick="@command('clear')"/>
</div>
</vbox>
</window>
MVVM in ZK
<window border="normal" width="300px" height="300px"
title="ZK MVVM" apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm')@init('org.zkoss.DemoViewModel')">
<vbox>
<div>
Input: <textbox value="@bind(vm.input)" />
</div>
<div>
Output: <label value="@load(vm.output)" />
</div>
<div>
<button label="Submit" onClick="@command('save')"/>
<button label="Clear" onClick="@command('clear')"/>
</div>
</vbox>
</window>
MVVM in ZK
public class DemoViewModel {
private String input;
private String output;
public String getInput() {
return input;
}
public void setInput(String input) {
this.input = input;
}
public String getOutput() {
return output;
}
@Init
public void init() {
input = "init";
}
//commands...
}
Bean
MVVM in ZK
public class DemoViewModel {
//getter, setter..
@Command
@NotifyChange("output")
public void save() {
output = input;
}
@Command
@NotifyChange({"input", "output"})
public void clear() {
input = "";
output = "";
}
}
Command
MVVM in ZK
public class DemoViewModel {
//getter, setter..
@Command
@NotifyChange("output")
public void save() {
output = input;
}
@Command
@NotifyChange({"input", "output"})
public void clear() {
input = "";
output = "";
}
}
Command
MVVM in ZK
public class DemoViewModel {
//getter, setter..
@Command
@NotifyChange("output")
public void save() {
output = input;
}
@Command
@NotifyChange({"input", "output"})
public void clear() {
input = "";
output = "";
}
}
Command
Why MVVM?
MVC MVVM
Why MVVM?
MVC MVVM
Component ID
Data binding
expression
View
Why MVVM?
MVC MVVM
Component ID
Data binding
expression
Extend
Composer
POJO

CommandController
View
Why MVVM?
Designer Developer
Why MVVM?
Designer Developer
UI/UX
Why MVVM?
Designer Developer
UI/UX
Flow
Model
Extensions of ZK
• ZK Addons
• ZK Charts
• ZK SpreadSheet
• …etc.
ZK Charts
• Line Chart
ZK Charts
• Bar Chart
ZK Charts
• Pie Chart
ZK Charts
• 3D Chart
ZK SpreadSheet
Extensions of ZK
• ZK Addons
• ZK Charts, ZK SpreadSheet
Extensions of ZK
• ZK Addons
• ZK Charts, ZK SpreadSheet
• ZK + Spring, Hibernate …
• ZK x SpringMVC
Nowadays..
ZK in Front-End
• Responsive design (ZK6.5)
ZK in Front-End
• Responsive design (ZK6.5)
• CSS3, LESS, Flat Design (ZK7)
Flat Design in ZK
Flat Design in ZK
ZK in Front-End
• Responsive design (ZK6.5)
• CSS3, LESS, Flat Design (ZK7)
• More Components and Addons (All versions)
ZK in Front-End
• Responsive design (ZK6.5)
• CSS3, LESS, Flat Design (ZK7)
• More Components and Addons (All versions)
• ZK 8?
當ZK遇見Front-End
About ZK 8
• UI Template Injection
About ZK 8
• UI Template Injection
• Shadow Elements
About ZK 8
• UI Template Injection
• Shadow Elements
• Concept -> Shadow DOM + JSTL Tag
About ZK 8
• UI Template Injection
• Shadow Elements
• Concept -> Shadow DOM + JSTL Tag


<input type="range">
About ZK 8
• UI Template Injection
• Shadow Elements
• Concept -> Shadow DOM + JSTL Tag


<input type="range">
About ZK 8
• UI Template Injection
• Shadow Elements
• Concept -> Shadow DOM + JSTL Tag


<input type="range">
About ZK 8
• JSTL Tag
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title> Tag Example</title>
</head>
<body>
<c:set var="salary" scope="session" value="${2000*2}"/>
<c:if test="${salary > 2000}">
<p>My salary is: <c:out value="${salary}"/><p>
</c:if>
<c:forEach var="i" begin="1" end="5">
Item <c:out value="${i}"/><p>
</c:forEach>
</body>
</html>
About ZK 8
• JSTL Tag
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title> Tag Example</title>
</head>
<body>
<c:set var="salary" scope="session" value="${2000*2}"/>
<c:if test="${salary > 2000}">
<p>My salary is: <c:out value="${salary}"/><p>
</c:if>
<c:forEach var="i" begin="1" end="5">
Item <c:out value="${i}"/><p>
</c:forEach>
</body>
</html>
c:set
c:if
c:forEach
Shadow Elements in ZK 8
• If
• Choose/When/Otherwise
• ForEach
• Apply
Shadow Elements in ZK 8
<div>
<if test="${user.editable}">
User Name: <textbox value="${user.name}"/>
<forEach items="${user.phones}">
<label value="${each.number}"/>
</forEach>
</if>
</div>
Shadow Elements in ZK 8
<div>
<if test="${user.editable}">
User Name: <textbox value="${user.name}"/>
<forEach items="${user.phones}">
<label value="${each.number}"/>
</forEach>
</if>
</div>
當ZK遇見Front-End
當ZK遇見Front-End
當ZK遇見Front-End
Apply + Template
• with Template name
Apply + Template
• with Template name
<template name="default">
<div style="color:blue">Blue</div>
</template>
<template name="normal">
<div style="color:red">Red</div>
</template>
<apply template="default" />
Apply + Template
• with Template name
<template name="default">
<div style="color:blue">Blue</div>
</template>
<template name="normal">
<div style="color:red">Red</div>
</template>
<apply template="default" />
Apply + Template
• with Template URI
Apply + Template
<apply templateURI="inc/apply.zul"/>
• with Template URI
UI Template Injection
• Pure Data (Catalog)
UI Template Injection
• Pure Data (Catalog)
Product Price Seller
Briefcase 159.99 Buzzphilip
BlueFolder 1.50 Buzzphilip
USB 20.00 Olliejeffery
…More
UI Template Injection
• View?
UI Template Injection
• View?
UI Template Injection
• View?
UI Template Injection
• View?
UI Template Injection
• main.zul
UI Template Injection
<apply templateURI="@load(vm.tmpURI)"
catalog="@load(vm.catalog)"/>
• main.zul
UI Template Injection
<apply templateURI="@load(vm.tmpURI)"
catalog="@load(vm.catalog)"/>
• main.zul
<button label="Change template"
onClick="@command('changeTemplate')">
UI Template Injection
• template_list.zul
UI Template Injection
• template_list.zul
<forEach items="@init(catalog.items)" var="item">
<label value="@load(item.title)" />
...
</forEach>
DEMO
Catalog x ZK 8
About ZK 8
• Client-Side binding
• Easier to work with 3rd JS library
About ZK 8
• Client-Side binding
• Easier to work with 3rd JS library
• Trigger Command in Javascript
Browser Server
HTML
ZK
Widget
Java
ZK Component
AJAX
Browser Server
HTML
ZK
Widget
Java
ZK Component
JS
Composer /
ViewModel
AJAX
Browser Server
JS
Composer /
ViewModel
Browser Server
binder.command
JS
Composer /
ViewModel
Browser Server
binder.command @ToServerCommand
JS
Composer /
ViewModel
Browser Server
binder.command @ToServerCommand
@ToClientCommand
JS
Composer /
ViewModel
Browser Server
binder.command
binder.after
@ToServerCommand
@ToClientCommand
JS
Composer /
ViewModel
Browser Server
binder.command
binder.after
@ToServerCommand
@ToClientCommand
@NotifyCommand
JS
Composer /
ViewModel
Client-Side binding API
• Client:
var binder = zkbind.$('$id');
binder.command(commandName, data);
binder.after(commandName, callback);
Client-Side binding API
• Client:
var binder = zkbind.$('$id');
binder.command(commandName, data);
binder.after(commandName, callback);
@ToServerCommand("commandName")
@ToClientCommand("commandName")
@NotifyCommand(value="commandName",
onChange="_vm_.expression")
• Server:
ZK 8 x React
• Work with React
Browser Server
HTML
ZK
Widget
Java
ZK Component
AJAX
Browser Server
HTML
ZK
Widget
Java
ZK Component
Composer/
ViewModel
AJAX
ZK 8 x React
• A Simple Chat Room with React x ZK 8
ZK 8 x React
• A Simple Chat Room with React x ZK 8
Comment
ZK 8 x React
• A Simple Chat Room with React x ZK 8
Comment
Author
Text
ZK 8 x React
• A Simple Chat Room with React x ZK 8
Comment
Author
Text
doAddComment doCommentsChange
var CommentBox = React.createClass({
handleCommentSubmit: function(comment) {
var comments = this.state.data;
comments.push(comment);
this.setState({data: comments}, function() {
zkbinder.command('doAddComment', comment);
});
},
loadCommentsFromServer: function() {
var self = this;
zkbinder.after('doCommentsChange', function (evt) {
self.setState({data: evt});
});
},
//other code, render…
});
var CommentBox = React.createClass({
handleCommentSubmit: function(comment) {
var comments = this.state.data;
comments.push(comment);
this.setState({data: comments}, function() {
zkbinder.command('doAddComment', comment);
});
},
loadCommentsFromServer: function() {
var self = this;
zkbinder.after('doCommentsChange', function (evt) {
self.setState({data: evt});
});
},
//other code, render…
});
handleCommentSubmit
var CommentBox = React.createClass({
handleCommentSubmit: function(comment) {
var comments = this.state.data;
comments.push(comment);
this.setState({data: comments}, function() {
zkbinder.command('doAddComment', comment);
});
},
loadCommentsFromServer: function() {
var self = this;
zkbinder.after('doCommentsChange', function (evt) {
self.setState({data: evt});
});
},
//other code, render…
});
loadCommentsFromServer
handleCommentSubmit
@ToServerCommand({"doAddComment"})


@NotifyCommand(value="doCommentsChange",
onChange="_vm_.comments")
@ToClientCommand({"doCommentsChange"})
public class ReactVM {
//event queue, other command…
}
doAddComment doCommentsChange
@ToServerCommand({"doAddComment"})


@NotifyCommand(value="doCommentsChange",
onChange="_vm_.comments")
@ToClientCommand({"doCommentsChange"})
public class ReactVM {
//event queue, other command…
}
doAddComment doCommentsChange
@ToServerCommand({"doAddComment"})


@NotifyCommand(value="doCommentsChange",
onChange="_vm_.comments")
@ToClientCommand({"doCommentsChange"})
public class ReactVM {
//event queue, other command…
}
doAddComment doCommentsChange
//@ToServerCommand, @NotifyCommand,@ToClientCommand
public class ReactVM {
private Collection<Comment> comments;
@Command
@NotifyChange("comments")
public void doAddComment(
@BindingParam("author") String author,
@BindingParam("text") String text) {
Comment c = new Comment(author, text);
comments.add(c);
}
//event queue, other command…
}
//@ToServerCommand, @NotifyCommand,@ToClientCommand
public class ReactVM {
private Collection<Comment> comments;
@Command
@NotifyChange("comments")
public void doAddComment(
@BindingParam("author") String author,
@BindingParam("text") String text) {
Comment c = new Comment(author, text);
comments.add(c);
}
//event queue, other command…
}
comments
//@ToServerCommand, @NotifyCommand,@ToClientCommand
public class ReactVM {
private Collection<Comment> comments;
@Command
@NotifyChange("comments")
public void doAddComment(
@BindingParam("author") String author,
@BindingParam("text") String text) {
Comment c = new Comment(author, text);
comments.add(c);
}
//event queue, other command…
}
comments
doAddComment
DEMO
React x ZK 8
About ZK 8
• Furthermore, wrapped into a Component
About ZK 8
• Furthermore, wrapped into a Component
• Data-Attribute handler
About ZK 8
• Furthermore, wrapped into a Component
• Data-Attribute handler
• HTML5 Custom Data Attribute (data-*)
<div id="test" data-content="something" />
About ZK 8
• Furthermore, wrapped into a Component
• Data-Attribute handler
• HTML5 Custom Data Attribute (data-*)
<div id="test" data-content="something" />
var testDiv = document.getElementById('test');
var content = plant.getAttribute('data-content');
//Do something...
Data-Attribute Handler in ZK 8
• Required 3 Parts:
Data-Attribute Handler in ZK 8
• Required 3 Parts:
• Javascript library
Data-Attribute Handler in ZK 8
• Required 3 Parts:
• Javascript library
• ZK Component (Widget)
Data-Attribute Handler in ZK 8
• Required 3 Parts:
• Javascript library
• ZK Component (Widget)
• Integration (in zk.xml)
Data-Attribute Handler in ZK 8
• Required 3 Parts:
• Javascript library
• ZK Component (Widget)
• Integration (in zk.xml)
data-labelauty
• in WEB-INF/zk.xml
<client-config>
<data-handler>
<name>labelauty</name>
<link href="/scripts/auty/jquery-labelauty.css" />
<script src="/scripts/auty/jquery-labelauty.js" />
<script>
function (wgt, dataValue) {
jq(wgt._subnodes.real).labelauty(dataValue);
}
</script>
</data-handler>
</client-config>
data-labelauty
• in WEB-INF/zk.xml
<client-config>
<data-handler>
<name>labelauty</name>
<link href="/scripts/auty/jquery-labelauty.css" />
<script src="/scripts/auty/jquery-labelauty.js" />
<script>
function (wgt, dataValue) {
jq(wgt._subnodes.real).labelauty(dataValue);
}
</script>
</data-handler>
</client-config>
name link script
data-labelauty
• in WEB-INF/zk.xml
<client-config>
<data-handler>
<name>labelauty</name>
<link href="/scripts/auty/jquery-labelauty.css" />
<script src="/scripts/auty/jquery-labelauty.js" />
<script>
function (wgt, dataValue) {
jq(wgt._subnodes.real).labelauty(dataValue);
}
</script>
</data-handler>
</client-config>
name link script
data-labelauty
• in WEB-INF/zk.xml
<client-config>
<data-handler>
<name>labelauty</name>
<link href="/scripts/auty/jquery-labelauty.css" />
<script src="/scripts/auty/jquery-labelauty.js" />
<script>
function (wgt, dataValue) {
jq(wgt._subnodes.real).labelauty(dataValue);
}
</script>
</data-handler>
</client-config>
name link script
Widget Data
data-labelauty
• Usage:
<checkbox xmlns:ca="client/attribute"
checked="@bind(vm.like)"
ca:data-labelauty="{'class': 'icon',…}" />
data-labelauty
• Usage:
<checkbox xmlns:ca="client/attribute"
checked="@bind(vm.like)"
ca:data-labelauty="{'class': 'icon',…}" />
DEMO
Labelauty x ZK 8
Data-Attribute Handler in ZK 8
• Another example: markdown converter
var converter = new showdown.Converter(),
text = '#hello, markdown!',
html = converter.makeHtml(text);
Data-Attribute Handler in ZK 8
• Another example: markdown converter
var converter = new showdown.Converter(),
text = '#hello, markdown!',
html = converter.makeHtml(text);
Data-Attribute Handler in ZK 8
• Another example: markdown converter
var converter = new showdown.Converter(),
text = '#hello, markdown!',
html = converter.makeHtml(text);
input outputconverter
Markdown editor by Data-handler
Markdown editor by Data-handler
<textbox value="@bind(vm.markdown)" />
<label ca:data-markdown="true"
value="@load(vm.markdown)"
multiline="true" />
function (wgt, dataValue) {
var converter = new showdown.Converter();
//Convert current value
wgt.$n().innerHTML = converter.makeHtml(wgt.getValue());
//After value get updated, convert to markdown
wgt.setOverride("setValue", function(value) {
this.$setValue(value);
this.$n().innerHTML = converter.makeHtml(value);
});
}
• data-markdown script
Markdown editor by Data-handler
function (wgt, dataValue) {
var converter = new showdown.Converter();
//Convert current value
wgt.$n().innerHTML = converter.makeHtml(wgt.getValue());
//After value get updated, convert to markdown
wgt.setOverride("setValue", function(value) {
this.$setValue(value);
this.$n().innerHTML = converter.makeHtml(value);
});
}
• data-markdown script
Markdown editor by Data-handler
Current
function (wgt, dataValue) {
var converter = new showdown.Converter();
//Convert current value
wgt.$n().innerHTML = converter.makeHtml(wgt.getValue());
//After value get updated, convert to markdown
wgt.setOverride("setValue", function(value) {
this.$setValue(value);
this.$n().innerHTML = converter.makeHtml(value);
});
}
• data-markdown script
Markdown editor by Data-handler
Current
Updated
DEMO
Markdown editor x ZK 8
當ZK遇見Front-End
#JCConf
Thank you!

More Related Content

當ZK遇見Front-End