单一页面应用模式的页面导航

页面导航是指在一个应用程序内「多个页面之间切换」的议题。当页面越来越多的时候,就需要一个方法将页面组织串连起来,好让使用者可以容易在多个页面之间游走,不致迷失。

整页切换

页面数量不多的时候,可以就用以往整切换的方式,例如用 <a> 直接指向目的页面:

<a href="mypath/page.zul"/>

也可以在按钮的 onClick listener 中呼叫页面重导向:

Executions.getCurrent().sendRedirect("mypath/mypage.zul");
  • 在 ZK 中 Execution 是一个封装 Http request 的物件,你可以把它当作 request 来看。
  • Executions.getCurrent() 回传当前的 Execution,要在 Servlet thread 中呼叫才能取得,例如 event listener 中

部分页面切换

但是整页切换就会有过往 JSP 的缺点:换页速度较慢、整页重传会有许多重复的内容。因此 ZK 中比较建议采用部分页面切换的方法,页面网址维持不变,只有抽换内容,也类似单页式应用程序 (Single Page Application)的做法。以最常见的排版为例,左边为「选单侧边栏」,右边为「内容区」。使用者点选侧边栏後,只有内容区会切换。

范本插入

右边内容区不是写死的固定内容,而是放入一个 <apply> 元素,用来动态插入页面。

https://ithelp.ithome.com.tw/upload/images/20220207/20050621LOj9U3eqRB.png

侧边栏

先做侧边栏,我预计是每个选单项目有一个 key 值,可以用该值来推算出对应的页面名称:

nav-template.zul

<navbar orient="vertical" width="200px" >
    <navitem label="财务" iconSclass="z-icon-book" selected="true">
        <custom-attributes name="finance"/>
    </navitem>
    <navitem label="管理" iconSclass="z-icon-user">
        <custom-attributes name="management"/>
    </navitem>
    <navitem label="研究" iconSclass="z-icon-lightbulb-o">
        <custom-attributes name="research"/>
    </navitem>
</navbar>
  • <cutom-attributes> 是用来存资料的元素,放在元件内就等同於呼叫该元件 setAttribute(key, value),因此我用来存入一个 key 为 name, value 为 finance 的资料。为了简单化,这个值就等於该选单的对应页面名称,因此对应的页面就是 finance.zul

内容区

<hlayout height="100%" apply="quickstart.nav.TemplateNavComposer">
    <navbar orient="vertical" width="200px" >
        ...
    </navbar>
    <div sclass="content" hflex="1" vflex="1" ...>
        <apply id="content" templateURI="finance.zul" dynamicValue="true"/>
    </div>
</hlayout>
  • 因为未来要动态插入不同的页面,因此 dynamicValue="true"

控制器

public class TemplateNavComposer extends SelectorComposer<Component> {

    @Wire("::shadow#content")
    private Apply contentTemplate;
  • @Wire 取得 shadow 元素的参照
@Listen(Events.ON_SELECT+ "= navbar")
public void navigate(SelectEvent event){
    //取得页面名称
    String pageName = ((Navitem)event.getSelectedItems().iterator().next()).getAttribute("name").toString();
    // 换页面
    contentTemplate.setTemplateURI(pageName + ".zul");
    contentTemplate.recreate();
}
  • 使用者点选选单时,会发出 onSelect 事件。点选已经选的选单就不会再发了,因此不会重复载入同一页面。
  • getAttribute("name") 来取得每个选单的存的 key 值,透过该值推算出页面名称
  • setTemplateURI() 切换内容区页面名称,并重建该部分内容

tab 导航

当使用者点选一个项目就产生一个新分页的导航方式也是很常见的一种。

https://ithelp.ithome.com.tw/upload/images/20211005/20050621Rshhe0CR1j.jpg

介面规划

假设仍是分成左右两区,左边侧边栏跟前一个例子相同。右边换成一个空的 <tabbox>

nav-tab.zul

<hlayout height="100%" apply="quickstart.nav.TabNavComposer">
    <navbar orient="vertical" width="200px" >
        ...
    </navbar>
    <div sclass="content" hflex="1" vflex="1">
        <tabbox vflex="1">
            ...
        </tabbox>
    </div>
</hlayout>

Tabbox 也是一个支援 Model-driven rendering 的元件,因此我可以赋予一个 ListModelList,动态的增减 tab 数量:

public class TabNavComposer extends SelectorComposer {
    @Wire("tabbox")
    private Tabbox tabbox;
    private ListModelList<TabState> tabModel = new ListModelList();

    @Override
    public void doAfterCompose(Component comp) throws Exception {
        super.doAfterCompose(comp);
        tabbox.setModel(tabModel);
    }

增加页签

我假定每个 tab 上的图示、名称各有一套设定,并存在 TabState 中。我先制作了3 组样本设定:

private static Map<String, TabState>tabStates
= Map.of("finance", new TabState("财务", "z-icon-book"),
         "management", new TabState("管理", "z-icon-user"),
         "research", new TabState("研究", "z-lightbulb-o"));

定义范本

需要定义两个范本,分别给 <tab><tabpanel>

<tabbox id="box" vflex="1">
    <template name="model:tab">
        <tab iconSclass="${each.iconClass}">
            ${each.name}
        </tab>
    </template>
    <template name="model:tabpanel">
        <tabpanel>
            ${each.name}
        </tabpanel>
    </template>
</tabbox>
  • ${each} 参照到 TabState 物件,因为我的资料模型中是存放 TabState

控制器

@Listen(Events.ON_CLICK+ "= navitem")
public void navigate(MouseEvent event){
    //取得页面名称
    String pageName = ((Navitem)event.getTarget()).getAttribute("name").toString();
    addTab(pageName);
}
  • 因为每次点选单就要增加一个 tab,因此要听 onClick 而不是前例的 onSelect
  • 取得选单上的 attribute 来作为 key 值找出对应的 tab 设定
public void addTab(String pageName){
    try {
        tabModel.add((TabState)tabStates.get(pageName).clone());
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
}
  • 复制一份tab设定 (TabState) 加入 tabModel 中,ZK 就会帮我画出新的 tab

关闭 Tab

删除 tab 也必须要透过操作 ListModelList

<tabbox id="box" vflex="1">
    <template name="model:tab">
        <tab iconSclass="${each.iconClass}" closable="true" forward="onClose=box.onClose(${each})">
            ${each.name}
        </tab>
    </template>
...
</tabbox>
  • closable 能在 tab 上启动关闭功能
  • 因为是动态产生的 tab,我用转发事件转到父元件上去,并把 ${each} 作为参数传入,这样就可得知 Tab 所对应 TabState
@Listen("onClose = #box")
public void closeTab(ForwardEvent event){
    TabState tabState = (TabState) event.getData();
    tabModel.remove(tabState);
}
  • event.getData() 取得转发事件时传入的 ${each}
  • 呼叫 remove() 移除对应的 tabState 之後,tabbox 就会自动帮我们把浏览器上的 tab 移除

进阶:同一个页面不同状态的导航

如果要在同一个页面设定不同状态例如

http://myapp/page.zul#step1

http://myapp/page.zul#step2

请参考 Browser History Management

进阶:页面内不同位置的导航

如果是要让使用者可在页面内跳至不同的位置,请使用元件 Anchornav


<<:  [DAY20] Domain 间的依赖关系

>>:  【Day22】导航元件 - Tabs

Logger: Code Stream Logger

指令的部分终於完结了! 今天就来做 Logger 吧, 目标是要有一致性和一定程度的可读性, 让之後...

iOS工程师面试深入浅出(OC)- @property 使用方法?Copy 什麽时候用?

iOS工程师面试深入浅出(OC)- @property 使用方法?Copy 什麽时候用? 如果本来是...

周末雨会(一):变数的两种状态 val vs var

台湾的特殊位置,使她在夏秋之间常遭受台风袭击,但偶尔也会有搞错季节的晚台。 诗忆望着窗外灰暗的天空,...

Day7 CSV档处理

在经历上一部函数与类别的摧残後,这两天就来教一些比较温和的程序吧~ 今天的影片内容为介绍常见的档案格...

23 搞半天终於在网页上启动游戏了

发牌员 我们的短期目标是,在网页上用纯文字的方式直接显示游戏状态 并会随着游戏更新的时候更新 原本想...