Building Stateless UI"

From Documentation
 
(41 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
{{ZKDevelopersReferencePageHeader}}
 
{{ZKDevelopersReferencePageHeader}}
 +
 +
{{versionSince| 10.0.0}} {{ZK_EE}}
 +
 
__TOC__
 
__TOC__
  
Line 31: Line 34:
  
 
= Example Application=
 
= Example Application=
We will use the simple shopping cart application as an example to introduce basic features of stateless components:
+
We will use the simple shopping cart application as an example to introduce the basic features of stateless components ([https://github.com/zkoss-demo/zk10-shopping-cart-demo download the shopping cart example project]):
 
[[File:Shoppingcart.png|center]]
 
[[File:Shoppingcart.png|center]]
  
 
= Building UI with Richlet=
 
= Building UI with Richlet=
Building user interfaces (UI) with stateless components requires creating a StatelessRichlet and mapping a URL to that richlet.
+
Building user interfaces (UI) with stateless components requires creating a [https://www.zkoss.org/javadoc/latest/zk/org/zkoss/stateless/ui/StatelessRichlet.html StatelessRichlet] and mapping a URL to that richlet.
  
 
= URL Mapping =
 
= URL Mapping =
We use <code>@RichletMapping</code> to compose a URL. When users visit that URL, ZK will invoke the corresponding method.
+
We use <code>@RichletMapping</code> to compose a URL. When users visit that URL, ZK will invoke the corresponding method.
  
 
For example below, the <code>index()</code> URL will be '''<protocal>:// <host name: port> /shoppingCart'''.
 
For example below, the <code>index()</code> URL will be '''<protocal>:// <host name: port> /shoppingCart'''.
Line 58: Line 61:
 
== Method-Level Mapping==
 
== Method-Level Mapping==
 
Within the <code>StatelessRichlet</code>, each method can specify further URL mapping. By applying <code>@RichletMapping("")</code> to a method, the specified path appends to the class-level path.
 
Within the <code>StatelessRichlet</code>, each method can specify further URL mapping. By applying <code>@RichletMapping("")</code> to a method, the specified path appends to the class-level path.
 +
 +
Hence, the final URL is the combination of the class-level path and method-level mapping one, for example:
 +
 +
<syntaxhighlight lang="XML">
 +
http://localhost:8080/[CLASS_LEVEL PATH]/[METHOD_LEVEL PATH]
 +
</syntaxhighlight>
  
 
= Composing the UI with Stateless Components =
 
= Composing the UI with Stateless Components =
Before ZK 10, ZK components are stateful, meaning that the server holds the state. Starting from ZK 10, we provide a set of stateless components as '''Immutable objects'''. Immutable objects are constructed once and can not be changed after they are constructed. After Immutable objects are rendered, they will be destroyed. Since the component state will not be saved on the server, they consume less memory.  
+
Before ZK 10, ZK components are stateful, meaning that the server holds the state. Starting from ZK 10, we provide a set of stateless components as '''Immutable objects'''. Immutable objects are constructed once and can not be changed after they are constructed. After Immutable objects are rendered, they will be destroyed. Since the stateless component states will not be saved on the server, they consume less memory.  
  
== UI Composing ==
+
With stateless components, ZK offers a streamlined, fluent API for building user interfaces.
  
With ZK 10 stateless components, you will no longer write a zul file. You will be composing your view using the stateless components and their APIs in Java.
+
* Every classic component has a corresponding stateless version, identified by an "I" prefix, denoting "immutable."
 +
* Stateless components employ a builder pattern, using methods like <code>of()</code> for initializing properties and <code>withSclass()</code> for setting classes.
 +
Here's a comparison of UI composition between classic and stateless components:
  
* ZK component having a prefix letter "I" represents an immutable component.
+
'''Classic Component in ZK 9'''
* We offer <code>of()</code> API for commonly used properties.
+
<source lang='java'>
* <code>withSclass()</code> means the setter of sclass.
+
Button button = new Button("add items");
 +
button.setSclass("add-items");
 +
</source>
  
 +
'''Equivalent Stateless Component in ZK 10'''
 
<source lang='java'>
 
<source lang='java'>
    // ZK 10
+
IButton.of("add items")
    IButton.of("add items")
+
.withSclass("add-items");
          .withSclass("add-items");
 
 
</source>
 
</source>
 +
 +
 +
Therefor, for the method with URL mapping, we should return a list of components like:
 
<source lang='java'>
 
<source lang='java'>
    // ZK 9
+
@RichletMapping("")
    // equivalent idea as above.
+
public List<IComponent> index() {
    Button button = new Button("add items");
+
return asList(
    button.setSclass("add-items");
+
IStyle.ofSrc(DEMO_CSS),
 +
IVlayout.of(
 +
renderShoppingCart(),
 +
Boilerplate.ORDER_TEMPLATE
 +
)
 +
);
 +
}
 
</source>
 
</source>
  
 
= Event Wiring =
 
= Event Wiring =
To wire an action handler method for an event, you need to apply the annotation <code>@Action</code> on a method:
+
To wire an action handler method for an event, you need to call <code>withAction(ActionHandler action)</code> with a '''public method''' reference:
* The method should be public.
 
* The parameter of <code>@Action</code> should be one of  [https://www.zkoss.org/javadoc/zk/org/zkoss/zk/ui/event/Events.html event types].
 
* Wire the action handler with the target component by <code>withAction(ActionHandler action)</code>.
 
  
<source lang='java' highlight='2, 9'>
+
<source lang='java' highlight='3'>
     // ActionHandler method
+
IButton.of("add item +")
     @Action(type = Events.ON_CLICK) // Wiring event
+
     .withSclass("add-items")
    public void addItem() {
+
     .withAction(ActionType.onClick(this::addItem))  
        ...
+
</source>
    }
+
* Line 3: it means register <code>addItem()</code> as an action handler for <code>onClick</code> event on <code>IButton</code>. ActionType supports all types of component events.
  
    public IComponent demo() {
+
In stateless components, we use the term '''action handler''', which distinctly separates it from the event listener associated with classic components.
        return IButton.of("add items").withSclass("add-items")
 
                .withAction(this::addItem);
 
    }
 
</source>
 
  
Here, we use '''action handler''' to differentiate an event listener of a classic component.
+
Hence, when a user clicks the button above, ZK will invoke <code>addItem()</code> declared in the Richlet.
  
= Obtain Widget State =
+
= Obtain Component State =
Since a server no longer holds a component's state (it's at client), we provide <code>@ActionVariable</code> to access a UI component's state sent from the client.
+
Since a server no longer holds a component's state (it's on the client side), we provide <code>@ActionVariable</code> to access a UI component's state sent from the client. When ZK invokes an action handler for an event, it will pass the corresponding parameters you specified.
  
 
* <code>@ActionVariable(targetId = ActionTarget.SELF, field = "id")</code> retrieves the value from the <code>field</code> of a component with the <code>targetId</code> on the client.</li>
 
* <code>@ActionVariable(targetId = ActionTarget.SELF, field = "id")</code> retrieves the value from the <code>field</code> of a component with the <code>targetId</code> on the client.</li>
* <code>ActionTarget.SELF</code> means it targets the component associated with the event itself.
+
* <code>ActionTarget.SELF</code> represents the component associated with the event which is a button. Please see [https://www.zkoss.org/javadoc/latest/zk/org/zkoss/stateless/action/ActionTarget.html ActionTarget] for other targets.
  
<source lang='java' highlight='2, 6'>
+
<source lang='java'>
    @Action(type = Events.ON_CLICK)
 
 
     public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String orderId) {
 
     public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String orderId) {
 
     }
 
     }
 
</source>
 
</source>
 +
* in this case, we will get a button's id
 +
 +
== Get User Input ==
 +
If you register an action handler on an input component like ICombobox:
 +
<syntaxhighlight lang='java' highlight='3'>
 +
ICombobox.of(initProductSize)
 +
.withReadonly(true)
 +
.withAction(ActionType.onChange(this::doSizeChange))
 +
</syntaxhighlight>
 +
 +
Declare <code>InputData</code> in the handler's signature, ZK will pass user input to you:
 +
<syntaxhighlight lang='java' highlight='1'>
 +
public void doSizeChange(InputData data,
 +
                        @ActionVariable(targetId = ActionTarget.PARENT, field = "id") String uuid){
 +
    String value = data.getValue();
 +
}
 +
</syntaxhighlight>
 +
* in this example, value is what a user input in a Combobox
 +
 +
= Update Component State =
 +
ZK provides various APIs on <code>UiAgent</code> to update a component's state. You need to call its method in an action handler method to implement your UI logic. Then those commands to update component states will be sent to the client after executing the method.
 +
 +
== Locator ==
 +
When you manipulate stateless components with <code>UiAgent</code> API, you need to pass a <code>Locator</code>. Why? Because your Richlet doesn't have any reference to a stateless component on the server side, no setter method to call. Instead, you need to tell ZK client engine the target component you want to manipulate by describing its location with <code>Locator</code>.
 +
 +
===By Component ID ===
 +
 +
<syntaxhighlight lang='java'>
 +
Locator.ofId("myId")
 +
</syntaxhighlight>
 +
 +
=== Self ===
 +
If you declare <code>Self</code>, the event target component's Locator,  on an action handler method signature, ZK will pass it to the method. For example, if I wire the method below with the spinner above for quantity change:
 +
 +
<syntaxhighlight lang="java">
 +
public void doQuantityChange(Self self,...)
 +
</syntaxhighlight>
 +
* <code>Self</code> is the Locator of the spinner.
 +
 +
=== By Relative Position ===
 +
If you have a Locator, you can find another component based on it like
 +
 +
<syntaxhighlight lang="java">
 +
self.nextSibling();
 +
self.closest() //find its parent component
 +
self.firstChild()
 +
</syntaxhighlight>
 +
 +
 +
== Add Child Components==
 +
 +
<syntaxhighlight lang='java' highlight='3'>
 +
public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String id) {
 +
    UiAgent.getCurrent()
 +
          .appendChild(Locator.ofId(SHOPPING_CART_ROWS),
 +
                        renderShoppingCartOneItem(parseOrderId(id)));
 +
}
 +
</syntaxhighlight>
 +
 +
== Change a Component's Property ==
  
= Update Widget State =
+
<syntaxhighlight lang='java' highlight='3'>
To update a widget's state, we provide several APIs in <code>UiAgent</code>.  The state will be updated to the client after executing the method.
+
UiAgent.getCurrent()
 +
    .smartUpdate(Helper.getPriceLocator(self),
 +
                new ILabel.Updater().value(String.valueOf(price)))
 +
</syntaxhighlight>
 +
* It changes a label'a value with a <code>price</code>
  
The following code adds the specified child component as the last child to the component found by <code>Locator</code>.
+
== Remove Components==
 +
The following code removes a component specified by <code>Locator</code>.
  
 
<source lang='java' highlight='3'>
 
<source lang='java' highlight='3'>
@Action(type = Events.ON_CLICK)
+
public void doDelete(@ActionVariable(targetId = ActionTarget.PARENT, field = "id") String id) {
public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String uuid) {
+
...
    UiAgent.getCurrent().appendChild(Locator.ofId(SHOPPING_CART_ROWS),
+
UiAgent.getCurrent().remove(Locator.ofId(id));
            renderShoppingCartOneItem(parseOrderId(uuid)));
+
}
}
 
 
</source>
 
</source>
  
= Example Application =
+
===Clear Child Components===
You can [https://github.com/zkoss-demo/zk10-shopping-cart-demo download the shopping cart demo project].
+
<syntaxhighlight lang="java">
 +
UiAgent.getCurrent()
 +
// empty the shopping cart rows
 +
.replaceChildren(Locator.ofId(SHOPPING_CART_ROWS))
 +
</syntaxhighlight>
 +
 
 +
 
 +
= Building UI in a zul =
 +
Many users still prefer to build UI in a zul because it's more readable than java code. In a stateless richlet, zk also provides a way to build UI in a zul:
 +
 
 +
<syntaxhighlight lang="java" highlight='6'>
 +
@RichletMapping("/zul")
 +
public class ZulRichlet implements StatelessRichlet {
 +
    @RichletMapping("")
 +
    public List<IComponent> index() {
 +
        // build UI via a zul but render it in stateless components
 +
        return Immutables.createComponents("stateless-page.zul", null);
 +
    }
 +
</syntaxhighlight>
 +
 
 +
Remember that zk creates stateless components based on the zul in such a usage
  
  
 +
== Wire Action Handlers ==
 +
When using a zul, you need to apply <code>@Action</code> on a method instead of <code>withAction()</code> to wire an action hander:
 +
 +
<syntaxhighlight lang="java" highlight='1'>
 +
    @Action(from = "#calculate", type = Events.ON_CLICK)
 +
    public void calculate(@ActionVariable(targetId = "firstMember") int firstMemberValue,
 +
                          @ActionVariable(targetId = "secondMember") int secondMemberValue,
 +
                          @ActionVariable(targetId = "operation", field = "selectedIndex") int operation)
 +
</syntaxhighlight>
 +
* Line 1: this line wires the method as an action handler for onClick event on a component whose id is <code>calculate</code>. Specify a component selector in <code>from</code>, e.g. <code>#calculate</code> is ID selector. see [[ZK_Developer%27s_Reference/MVC/Controller/Wire_Components#CSS3-like_Selectors]]
  
 
{{ZKDevelopersReferencePageFooter}}
 
{{ZKDevelopersReferencePageFooter}}

Latest revision as of 06:45, 1 April 2024


Building Stateless UI


Since 10.0.0

  • Available for ZK:
  • http://www.zkoss.org/product/zkhttp://www.zkoss.org/whyzk/zkeeVersion ee.png

Setting up

To set up stateless components in a ZK 10 application, you need to include the stateless components module and define a Dispatcher Richlet Filter in your WEB-INF/web.xml file.

Including Required Jar

dependencies {
    implementation "org.zkoss.zk:stateless:${zkVersion}"
    ...
}

Dispatcher Richlet Filter

<filter>
    <filter-name>DispatcherRichletFilter</filter-name>
    <filter-class>org.zkoss.stateless.ui.http.DispatcherRichletFilter</filter-class>
    <init-param>
        <param-name>basePackages</param-name>
        <param-value><!-- your base packages --></param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>DispatcherRichletFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>


Example Application

We will use the simple shopping cart application as an example to introduce the basic features of stateless components (download the shopping cart example project):

Shoppingcart.png

Building UI with Richlet

Building user interfaces (UI) with stateless components requires creating a StatelessRichlet and mapping a URL to that richlet.

URL Mapping

We use @RichletMapping to compose a URL. When users visit that URL, ZK will invoke the corresponding method.

For example below, the index() URL will be <protocal>:// <host name: port> /shoppingCart.

    @RichletMapping("/shoppingCart")
    public class DemoRichlet implements StatelessRichlet {
        @RichletMapping("")
        public List<IComponent> index() {
            //return ...
        }
    }


Class-Level Mapping

At this level, @RichletMapping defines the base path for all methods in a StatelessRichlet. For example, assigning @RichletMapping("/shoppingCart") to the DemoRichlet class sets a foundational path for all UI components it manages.


Method-Level Mapping

Within the StatelessRichlet, each method can specify further URL mapping. By applying @RichletMapping("") to a method, the specified path appends to the class-level path.

Hence, the final URL is the combination of the class-level path and method-level mapping one, for example:

http://localhost:8080/[CLASS_LEVEL PATH]/[METHOD_LEVEL PATH]

Composing the UI with Stateless Components

Before ZK 10, ZK components are stateful, meaning that the server holds the state. Starting from ZK 10, we provide a set of stateless components as Immutable objects. Immutable objects are constructed once and can not be changed after they are constructed. After Immutable objects are rendered, they will be destroyed. Since the stateless component states will not be saved on the server, they consume less memory.

With stateless components, ZK offers a streamlined, fluent API for building user interfaces.

  • Every classic component has a corresponding stateless version, identified by an "I" prefix, denoting "immutable."
  • Stateless components employ a builder pattern, using methods like of() for initializing properties and withSclass() for setting classes.

Here's a comparison of UI composition between classic and stateless components:

Classic Component in ZK 9

Button button = new Button("add items");
button.setSclass("add-items");

Equivalent Stateless Component in ZK 10

IButton.of("add items")
.withSclass("add-items");


Therefor, for the method with URL mapping, we should return a list of components like:

	@RichletMapping("")
	public List<IComponent> index() {
		return asList(
			IStyle.ofSrc(DEMO_CSS),
			IVlayout.of(
				renderShoppingCart(),
				Boilerplate.ORDER_TEMPLATE
			)
		);
	}

Event Wiring

To wire an action handler method for an event, you need to call withAction(ActionHandler action) with a public method reference:

IButton.of("add item +")
    .withSclass("add-items")
    .withAction(ActionType.onClick(this::addItem))
  • Line 3: it means register addItem() as an action handler for onClick event on IButton. ActionType supports all types of component events.

In stateless components, we use the term action handler, which distinctly separates it from the event listener associated with classic components.

Hence, when a user clicks the button above, ZK will invoke addItem() declared in the Richlet.

Obtain Component State

Since a server no longer holds a component's state (it's on the client side), we provide @ActionVariable to access a UI component's state sent from the client. When ZK invokes an action handler for an event, it will pass the corresponding parameters you specified.

  • @ActionVariable(targetId = ActionTarget.SELF, field = "id") retrieves the value from the field of a component with the targetId on the client.
  • ActionTarget.SELF represents the component associated with the event which is a button. Please see ActionTarget for other targets.
    public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String orderId) {
    }
  • in this case, we will get a button's id

Get User Input

If you register an action handler on an input component like ICombobox:

ICombobox.of(initProductSize)
	.withReadonly(true)
	.withAction(ActionType.onChange(this::doSizeChange))

Declare InputData in the handler's signature, ZK will pass user input to you:

public void doSizeChange(InputData data, 
                         @ActionVariable(targetId = ActionTarget.PARENT, field = "id") String uuid){
    String value = data.getValue();
}
  • in this example, value is what a user input in a Combobox

Update Component State

ZK provides various APIs on UiAgent to update a component's state. You need to call its method in an action handler method to implement your UI logic. Then those commands to update component states will be sent to the client after executing the method.

Locator

When you manipulate stateless components with UiAgent API, you need to pass a Locator. Why? Because your Richlet doesn't have any reference to a stateless component on the server side, no setter method to call. Instead, you need to tell ZK client engine the target component you want to manipulate by describing its location with Locator.

By Component ID

Locator.ofId("myId")

Self

If you declare Self, the event target component's Locator, on an action handler method signature, ZK will pass it to the method. For example, if I wire the method below with the spinner above for quantity change:

public void doQuantityChange(Self self,...)
  • Self is the Locator of the spinner.

By Relative Position

If you have a Locator, you can find another component based on it like

self.nextSibling();
self.closest() //find its parent component
self.firstChild()


Add Child Components

public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String id) {
    UiAgent.getCurrent()
           .appendChild(Locator.ofId(SHOPPING_CART_ROWS),
                        renderShoppingCartOneItem(parseOrderId(id)));
}

Change a Component's Property

UiAgent.getCurrent()
    .smartUpdate(Helper.getPriceLocator(self), 
                 new ILabel.Updater().value(String.valueOf(price)))
  • It changes a label'a value with a price

Remove Components

The following code removes a component specified by Locator.

	public void doDelete(@ActionVariable(targetId = ActionTarget.PARENT, field = "id") String id) {
		...
		UiAgent.getCurrent().remove(Locator.ofId(id));
	}

Clear Child Components

UiAgent.getCurrent()
	// empty the shopping cart rows
	.replaceChildren(Locator.ofId(SHOPPING_CART_ROWS))


Building UI in a zul

Many users still prefer to build UI in a zul because it's more readable than java code. In a stateless richlet, zk also provides a way to build UI in a zul:

@RichletMapping("/zul")
public class ZulRichlet implements StatelessRichlet {
    @RichletMapping("")
    public List<IComponent> index() {
        // build UI via a zul but render it in stateless components
        return Immutables.createComponents("stateless-page.zul", null);
    }

Remember that zk creates stateless components based on the zul in such a usage


Wire Action Handlers

When using a zul, you need to apply @Action on a method instead of withAction() to wire an action hander:

    @Action(from = "#calculate", type = Events.ON_CLICK)
    public void calculate(@ActionVariable(targetId = "firstMember") int firstMemberValue,
                          @ActionVariable(targetId = "secondMember") int secondMemberValue,
                          @ActionVariable(targetId = "operation", field = "selectedIndex") int operation)



Last Update : 2024/04/01

Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.