|
|
Line 6: |
Line 6: |
| | | |
| = Overview = | | = Overview = |
− | In the recently released ZK9, we provide a new way to integrate applications with ZK and front-end frameworks - [https://blog.zkoss.org/2019/12/17/zk-9-highlight-embedding-your-zk-app-into-a-3rd-party-app/ Embedded ZK]. For instance, [https://vuejs.org/ VueJS] is a progressive framework for building user interfaces. With the embedded ZK in ZK9, we could easily combine VueJS and ZK together. For more detail, we will demo how it works with modern front-end frameworks. We wil use ZK client side binding, embedded ZK, [https://getbootstrap.com/ Bootstrap] and [https://vuejs.org/ VueJS] in this demo project. | + | In the recently released ZK9, we provide a new way to integrate applications with ZK and front-end frameworks - [https://blog.zkoss.org/2019/12/17/zk-9-highlight-embedding-your-zk-app-into-a-3rd-party-app/ Embedded ZK]. For instance, [https://vuejs.org/ VueJS] is a progressive framework for building user interfaces. With the embedded ZK in ZK9, we could easily combine VueJS and ZK together. For more detail, we will demo how it works with modern front-end frameworks. We will use ZK client-side binding, embedded ZK, [https://getbootstrap.com/ Bootstrap] and [https://vuejs.org/ VueJS] in this demo project. |
| | | |
| = Demo = | | = Demo = |
− | [[File:ZK9_Embedded_With_VueJS01.png | 680px | center]] | + | [[File:ZK9_Embedded_With_VueJS01.png | 800px | center]] |
| | | |
| == Settings In ZK == | | == Settings In ZK == |
Line 25: |
Line 25: |
| === Prepare view models for client-side binding === | | === Prepare view models for client-side binding === |
| In the view model, we need to define the properties that would be loaded on the client-side. | | In the view model, we need to define the properties that would be loaded on the client-side. |
| + | (in org.zkoss.demo.forum.viewmodel.ArticleListVM) |
| <source lang="java"> | | <source lang="java"> |
− | | + | public class ArticleListVM { |
− | | + | private List<Article> articles; |
− | </source> | + | public List<Article> getArticles() { |
− | | + | return articles; |
− | <source lang="xml">var binder = zkbind.$('$id');</source>
| + | } |
− | | + | //... omitted |
− | We have to get the client binder first in order to use the client side methods. To get the binder, simply use
| |
− | <source lang="js">var binder = zkbind.$('$id');</source> | |
− | in the scripts. After we have our client binder, we could use the ''command'' and ''after'' method to interact with the view model back to our server.
| |
− | | |
− | === binder.command(commandName, data) ===
| |
− | This method is used to trigger a command we have at server by giving the command name as the first parameter. The second parameter is a JavaScript object, which is used to pass any information you want with the command.
| |
− | | |
− | '''Note:''' You could also pass ZK widgets in the data object and use ''@BindingParam'' to get the corresponding ZK component at the server.
| |
− | | |
− | === binder.after(commandName, callback) ===
| |
− | This method is used to place a callback at the client after a command gets executed at the server.
| |
− | | |
− | == At server ==
| |
− | Here, we are going to introduce two new annotations at the server side for the new client side binding. They should be placed at the beginning of the class declaration of our View Model.
| |
− | | |
− | === @NotifyCommand(value="commandName", onChange="_vm_.expression") ===
| |
− | The notify command annotation allows us to trigger a command whenever the given expression changes at the server. Notice the command which gets triggered is a command in our view model. The ''_vm_'' here means the current view model.
| |
− | | |
− | === @ToClientCommand(commandNames) ===
| |
− | The client command annotation allows us to put which commands we want to notify the client after execution has been done. Notice only the commands we put inside this annotation will trigger the callback we put in ''binder.after'' at client.
| |
− | | |
− | = Demo =
| |
− | In this demo, we will use full calendar, which is a JavaScript calendar library under MIT license. We are going to wire up the full calendar with some ZK components back to the server with a View Model and a Data Model. The goal here is to bind the events coming out from full calendar to the commands we have in our View Model so that we can sync whatever changes in the calendar directly to our data model.
| |
− | | |
− | <gflash width="900" height="730">Fullcalendar_demo.swf</gflash>
| |
− | | |
− | == Implementation ==
| |
− | We will have four main parts in this demo.
| |
− | * zul page, which is the view of our demo
| |
− | * js file, which will contain our event handlers for full calendar and our client side bindings.
| |
− | * view model, where we handle all the commands that come from the client.
| |
− | * data model, which act as a data accessing object to our data source.
| |
− | | |
− | === zul page ===
| |
− | Our zul page is relatively simple. It has three parts, a container for full calendar and two popups.
| |
− | The container will be just a native div element with an ID:
| |
− | <source lang="xml"><n:div id="cal" /></source>
| |
− | | |
− | The popups will be ZK popups and they will be used to create and modify events. The modify event popup looks like:
| |
− | | |
− | <source lang="xml">
| |
− | <popup id="modifyEventPop">
| |
− | <window title="Modify Event: " width="500px">
| |
− | <grid form="@id('fx') @load(vm.tempEvent) @save(vm.tempEvent, before='modEvent')">
| |
− | <rows>
| |
− | <row>
| |
− | <label value="Event ID: " />
| |
− | <label id="modId" value="@load(fx.id)" />
| |
− | </row>
| |
− | <row>
| |
− | <label value="Title:" />
| |
− | <textbox id="modTitle" value="@bind(fx.title)" />
| |
− | </row>
| |
− | <row>
| |
− | <label value="Start Date:" />
| |
− | <datebox id="modStart" value="@bind(fx.start)" format="long+medium" />
| |
− | </row>
| |
− | <row>
| |
− | <label value="End Date:" />
| |
− | <datebox id="modEnd" value="@bind(fx.end)" format="long+medium" />
| |
− | </row>
| |
− | </rows>
| |
− | </grid>
| |
− | <button id="modify" label="Modify"
| |
− | onClick="@command('modEvent', pop=modifyEventPop)" />
| |
− | <button label="Cancel" onClick="modifyEventPop.close()" />
| |
− | </window>
| |
− | </popup>
| |
− | </source>
| |
− | | |
− | The popup shows when an ''eventClicked'' event is triggered. The data of the event will be pre-loaded before the popup shows. When the modify button gets clicked, a ''modEvent'' command will be fired back to our View Model, and we will update the modified event to our data model from there.
| |
− | The popup for creating event is very similar with the one above, we will omit its implementation here just to keep things simple.
| |
− | | |
− | === js file ===
| |
− | Our script is where the new client side binding begins to play a role at. The complete js file looks like:
| |
− | <source lang="js" high="3,10,11,37,40">
| |
− | zk.afterMount(function() {
| |
− |
| |
− | var binder = zkbind.$('$cal'),
| |
− | calConfig = {};
| |
− |
| |
− | // day click handler
| |
− | calConfig.dayClick = function(data, jsEvent, view) {
| |
− | var popOffset = [jsEvent.clientX, jsEvent.clientY];
| |
− |
| |
− | binder.command('doDayClicked', {dateClicked: data.toDate().getTime()})
| |
− | .after(function() {
| |
− | var newPop = zk.$('$newEventPop');
| |
− | newPop.open(newPop, popOffset);
| |
− | });
| |
− | };
| |
− |
| |
− | // event click handler
| |
− | calConfig.eventClick = function(event, jsEvent, view) {
| |
− | var popOffset = [jsEvent.clientX, jsEvent.clientY];
| |
− | binder.command('doEventClicked', {evtId: event.id})
| |
− | .after(function() {
| |
− | var modPop = zk.$('$modifyEventPop');
| |
− | modPop.open(modPop, popOffset);
| |
− | });
| |
− | }
| |
− |
| |
− | // event drop handler and event resize handler
| |
− | calConfig.eventResize = calConfig.eventDrop =
| |
− | function(event, delta, revertFunc, jsEvent, ui, view) {
| |
− | var startTime = event.start ?
| |
− | event.start.toDate().getTime() : 0,
| |
− | endTime = event.end ? event.end.toDate().getTime() : 0;
| |
− |
| |
− | binder.command('doEventChanged', {evtId: event.id, startTime: startTime, endTime: endTime});
| |
− | }
| |
− |
| |
− | $('#cal').fullCalendar(calConfig);
| |
− |
| |
− | // the event handler of after 'doCommandChange' from server
| |
− | binder.after('doEventsChange', function(events) {
| |
− | $('#cal').fullCalendar('removeEvents');
| |
− | $('#cal').fullCalendar('addEventSource', events);
| |
− | $('#cal').fullCalendar('rerenderEvents');
| |
− | });
| |
− | | |
− | });
| |
− | </source>
| |
− | The first thing we do here at line 3 is to get our client binder. Then in our day clicked event handler, we use ''binder.command'' to trigger the ''doDayClicked'' command and pass the clicked date back to our view model at line 10. At line 11, we use ''binder.after'' to open our popup. Notice that when cascading ''binder.command'' and ''binder.after'', the first argument in ''binder.after'' can be omitted. ''eventClick'', ''eventDrop'', and ''eventResize'' handlers follow the similar concept as well. Line 37 is where we initialize our full calendar and finally, begins at line 40 is where our ''doEventsChange'' callback. We use this callback to ensure that every time when events change at the View Model, they will be updated in our view.
| |
− | | |
− | === View Model ===
| |
− | Our view model is where we put all of our commands at. The structure of our view model looks like:
| |
− | | |
− | <source lang="java" high="1,2">
| |
− | @NotifyCommand(value="doEventsChange", onChange="_vm_.events")
| |
− | @ToClientCommand({"doEventClicked", "doDayClicked", "doEventsChange"})
| |
− | public class DemoViewModel {
| |
− |
| |
− | private EventsDataModel dataModel;
| |
− | private Collection<EventObject> events;
| |
− | private EventObject tempEvent;
| |
− |
| |
− |
| |
− | @Init
| |
− | public void init() throws GeneralSecurityException, IOException {
| |
− | // init event data model
| |
− | dataModel = new DemoDataModel();
| |
− | events = dataModel.getEvents();
| |
− | }
| |
− |
| |
− | @Command
| |
− | @NotifyChange("tempEvent")
| |
− | public void doDayClicked(@BindingParam("dateClicked") long dateClicked);
| |
− |
| |
− | @Command
| |
− | @NotifyChange("tempEvent")
| |
− | public void doEventClicked(@BindingParam("evtId") String evtId);
| |
− |
| |
− | @Command
| |
− | @NotifyChange("events")
| |
− | public void doEventChanged(@BindingParam("evtId") String evtId,
| |
− | @BindingParam("startTime") long startTime, @BindingParam("endTime") long endTime);
| |
− |
| |
− | @Command
| |
− | @NotifyChange("events")
| |
− | public void modEvent(@BindingParam("pop") Popup pop);
| |
− |
| |
− | @Command
| |
− | @NotifyChange("events")
| |
− | public void createEvent(@BindingParam("pop") Popup pop);
| |
| } | | } |
| </source> | | </source> |
− | Notice we omit command implementations here just to focus on the new client side binding. First we put our ''@NotifyCommand'' and ''@ToClientCommand'' on top of our class declaration.
| |
| | | |
− | At line 1,
| + | (in org.zkoss.demo.forum.entity.Article) |
| <source lang="java"> | | <source lang="java"> |
− | @NotifyCommand(value="doEventsChange", onChange="_vm_.events")
| + | public class Article { |
| + | private String subject; |
| + | private String thumbnail; |
| + | private String content; |
| + | private Date lastEditedTime; |
| + | //... getter/setter omitted |
| + | } |
| </source> | | </source> |
− | We specify that our view model will trigger the ''doEventsChange'' command whenever events are changed. The ''_vm_'' here stands for the current view model.
| |
| | | |
− | At line 2,
| + | Then, we need to define the commands. |
| + | (in org.zkoss.demo.forum.viewmodel.ArticleListVM) |
| <source lang="java"> | | <source lang="java"> |
− | @ToClientCommand({"doEventClicked", "doDayClicked", "doEventsChange"}) | + | @NotifyCommand(value = "toC_Articles", onChange = "_vm_.articles") |
| + | @ToClientCommand({ "toC_Articles"}) |
| + | @ToServerCommand({ "loadArticles", "loadArticleById"}) |
| + | public class ArticleListVM { |
| + | //... omitted |
| + | } |
| </source> | | </source> |
− | We specify that every time these commands execute, ZK will notify our client, and if there is a ''binder.after'' callback at client, it will be invoked. Notice that we do not have a ''doEventsChange'' command in our view model, we put ''@NotifyCommand'' here just because we want to trigger the callback function at the client.
| + | |
− | | |
− | === data model ===
| |
− | Data model is used to assess our data source. Here's the class diagram of our data model:
| |
− | | |
− | | |
− | [[File: Fullcalendar_datamodel.png]]
| |
− | | |
− | Since we can have different data sources, our data model will have different implementations depending on the data source. As shown in the demo above, we use a mock data source, which is just a map object in memory. If you check out the [https://github.com/jumperchen/zk8-fullcalendar-demo source code] of this demo, we also have a implementation with Google Calendar's API.
| |
| | | |
| == Download == | | == Download == |
− | You can download the war file and all of the source code for this demo in [https://github.com/jumperchen/zk8-fullcalendar-demo/releases/tag/v0.8.0 Github] | + | You can download all of the source code for this demo in [https://github.com/zkoss-demo/client-binding-demo-vuejs Github] |
| | | |
| {{Template:CommentedSmalltalk_Footer_new| | | {{Template:CommentedSmalltalk_Footer_new| |