iT邦幫忙

2021 iThome 鐵人賽

DAY 20
1
Modern Web

ZK 30天速成系列 第 20

單一頁面應用模式的頁面導航

  • 分享至 

  • xImage
  •  

頁面導航是指在一個應用程式內「多個頁面之間切換」的議題。當頁面越來越多的時候,就需要一個方法將頁面組織串連起來,好讓使用者可以容易在多個頁面之間遊走,不致迷失。

整頁切換

頁面數量不多的時候,可以就用以往整切換的方式,例如用 <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

Yes


上一篇
元件內建拖放(drag & drop)效果
下一篇
上傳與下載
系列文
ZK 30天速成30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
peterlee_shulin
iT邦新手 5 級 ‧ 2022-01-19 15:04:08

template.jpg 無法顯示?

Hawk iT邦新手 4 級 ‧ 2022-02-07 23:23:10 檢舉

謝謝告知,已修正

我要留言

立即登入留言