iDempiere OSGI JavaScript 七擒孟獲:五次失敗與一次攻心
iDempiere

iDempiere OSGI JavaScript 七擒孟獲:五次失敗與一次攻心

2026-03-08 · 35 分鐘 · Ray Lee (System Analyst)

南征序曲:蠻荒之地多蠻王

自古蠻荒之地多猛獸,iDempiere OSGI 之地多 JavaScript 蠻王。

上回書說到,丞相以八陣圖之法,教會天下人如何在 iDempiere 中佈署 CustomForm,從 OSGI ClassLoader 的糧道到 ZK MVVM 的陣旗,一路過關斬將,好不威風。正當丞相以為天下太平,準備回成都喝茶之時,探馬來報:「丞相!南蠻又反了!」

南蠻王是誰?JavaScript

你看那 JavaScript,在瀏覽器裡橫行霸道慣了,到了 ZK Framework 的地盤,照樣不服管教。你想在 OSGI plugin 的 CustomForm 裡載入 ECharts CDN?它不讓。你想用 ZUL 的 <script> 標籤引入自定義 JS?它笑你天真。你想用 <?script?> Processing Instruction 繞道偷襲?它路都給你封了。

丞相深知,蠻王不可力敵,唯有攻心為上。於是丞相率大軍南征,五擒五縱,直到 JavaScript 蠻王心服口服,乖乖臣服於 Clients.evalJavaScript() 的旗幟之下。

本文記錄這場南征戰役的完整經過——五種失敗的嘗試(五擒五縱)、一個終極解法(攻心為上),以及行軍途中收服的四員叛將。所有程式碼皆為實戰驗證,複製即用。

第一擒:正面強攻 — <script> 作為 <borderlayout> 子元素

戰術口訣:「既然要載 JavaScript,那就直接塞 <script> 進去嘛!」

丞相一開始也是想得簡單:蠻王不就是個 JavaScript 嗎?我堂堂大漢天朝,直接正面強攻,在 <borderlayout> 裡放兩個 <script>,一個載 CDN,一個載自訂 JS,問題不就解決了?

<borderlayout>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"/>
    <script src="~./js/rnd-charts.js"/>
    <north>...</north>
</borderlayout>

結果?蠻王大笑,擺手而去。ZK 直接甩你一臉錯誤:

Unsupported child for Borderlayout: <Script null>

原來 ZK 的 <borderlayout> 是個極度挑食的傢伙,只接受五個子元素:<north><south><east><west><center>。你塞個 <script> 進去,就好比帶著蠻族使者闖皇宮正殿——禁衛軍直接把你攔在門外,理由很充分:「此人不在名單上。」

第一擒,蠻王不服,縱之。

第二擒:埋伏計 — <script> 在條件顯示的容器內

戰術口訣:「正面不行,那就埋伏!藏在 vlayout 裡總行了吧?」

丞相檢討第一戰的失敗,決定改走暗棋。把 <script> 藏在一個有條件顯示的 <vlayout> 容器裡,等時機成熟再載入:

<vlayout visible="@load(not vm.testEntryMode)">
    <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"/>
    <script src="~./js/rnd-charts.js"/>
    <div id="chartContainer"/>
</vlayout>

這次倒是沒報結構錯誤了。丞相心想:「妙哉,埋伏成功——」話還沒說完,console 啪一聲甩出:

renderTestTrendChart is not defined

蠻王笑得前仰後合:「你的埋伏兵馬根本沒出擊啊!」

原來 <script> 在條件隱藏的容器內,執行時機完全不可預測。容器 visible="false" 時,裡面的 script 可能根本不會執行;等容器變為可見時,script 的執行順序又是一團亂。就好比你在山谷裡埋了三千伏兵,結果號角吹響時,伏兵還在睡覺——時機不對,再精妙的埋伏也是白搭。

第二擒,蠻王仍不服,再縱之。

第三擒:繞道奇襲 — <?script?> Processing Instruction + ~./ 路徑

戰術口訣:「既然城門進不去,那就繞山路走小道!」

丞相翻閱 ZK 兵書(官方文件),發現了一條小路:<?script?> Processing Instruction。這東西不是 ZK component,而是頁面級的處理指令,不受 <borderlayout> 的挑食限制:

<?script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"?>
<?script src="~./js/rnd-charts.js"?>
<borderlayout>...</borderlayout>

丞相大喜:「此計甚妙,繞道奇襲,蠻王必不防!」

CDN 那條路確實通了——ECharts 庫順利載入。但自訂的 rnd-charts.js?Console 冷冷地回了一句:

renderTestTrendChart is not defined

蠻王嘿嘿一笑:「你的糧道走錯了。」

問題出在 ~./ 路徑。<?script?> 是頁面級處理,~./ 路徑解析到的是主 webapp(/webui/),而不是你 OSGI bundle 的 Web-ContextPath。CDN 是外部連結,不受路徑影響,所以沒問題;但 bundle 內的 JS 檔?糧官帶著糧車走了十里路,結果到的是別人的軍營。

第三擒,蠻王大笑而去,又縱之。

第四擒:換路再攻 — <?script?> + 絕對路徑

戰術口訣:「~./ 走錯路?那我直接寫絕對路徑!」

丞相此時已經有點上頭了。他想:既然 ~./ 解析錯了,那我直接用 OSGI bundle 的 Web-ContextPath 作為絕對路徑,總該到得了吧?

<?script src="/rnd/js/rnd-charts.js"?>

滿懷信心地啟動……

仍然找不到檔案

蠻王這次連笑都懶得笑了,只是搖搖頭:「此路不通。」

原來 OSGI bundle 的 Web-ContextPath: /rnd 不一定能正確 serve 靜態 JS 檔。這取決於 iDempiere 的 web resource 配置。bundle 的 Web-ContextPath 主要是給 ZK 的 ~./ 機制用的,不是一個通用的靜態檔案伺服器。你換了一條路,結果這條路壓根不存在——地圖上畫了路,實地卻是懸崖。

第四擒,蠻王搖頭嘆息,再縱之。

第五擒:策反內應 — w:onCreate 設定 DOM ID

戰術口訣:「打不進去,那就策反裡面的人!」

四次正面交鋒全敗。丞相決定改變策略——不再從外部載入 JS 檔,而是想辦法在 ZK 渲染的 DOM 裡安插內應。具體做法是用 w:onCreate 在 client 端手動設定一個固定的 DOM ID,方便 JavaScript 用 getElementById 找到圖表容器:

<div id="trendChart" w:onCreate="this.$n().id = 'rndTrendChart';"/>

然後在 JavaScript 裡:

document.getElementById('rndTrendChart')

看起來天衣無縫?內應已經成功潛入敵營,只等一聲號令就開城門……

結果內應叛變了。

ZK 的 widget lifecycle 有自己的一套規矩:它可能在某些渲染階段覆蓋你手動設定的 DOM ID,或者在你呼叫 getElementById 的時候,widget 根本還沒完成初始化。內應(DOM ID)是安插進去了,但他在 ZK lifecycle 的壓力下「投降」了——有時候能用,有時候找不到,完全不可靠。

就好比你策反了蠻王的一員大將,約好午時三刻開城門,結果那位大將到了時辰才發現:城門鑰匙不在他手上,而且城門在哪他也不太確定。

第五擒,蠻王冷笑,丞相沉默。五擒五縱,勝負未分。

心服口服:攻心為上 — Clients.evalJavaScript()

戰略口訣:「攻城為下,攻心為上。用兵之道,全國為上。」

五次硬碰硬,五次鎩羽而歸。丞相在帳中獨坐三日,翻遍兵書,終於悟出一個道理:不是 JavaScript 不聽話,是你一直在用錯誤的方式指揮它。

ZUL 的 <script><?script?> 都是在「ZK 的地盤」上載入 JS——路徑要走 ZK 的規矩,時機要看 ZK 的臉色。但 Clients.evalJavaScript() 不一樣,它是直接從 Java 端發送 JavaScript 到瀏覽器執行,繞過所有 ZK 的路徑解析和元件生命週期限制。

這就是攻心為上——不在蠻王的地盤跟他打,而是直接走進他心裡,讓他心甘情願為你效力。

攻心第一策:loadChartScripts() — 動態載入 CDN + 定義函數

第一步:在 Form.java 的 initForm() 中,用 Clients.evalJavaScript() 動態建立 <script> 元素載入 ECharts CDN,並在 CDN 載入完成後定義自訂函數。

@Override
protected void initForm() {
    // ... ZUL loading, WSearchEditor setup ...

    // Load ECharts CDN + define chart function
    loadChartScripts();
}

private void loadChartScripts() {
    String loadECharts = "if(!window.echarts){"
        + "var s=document.createElement('script');"
        + "s.src='https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js';"
        + "s.onload=function(){" + getChartFunctionJs() + "};"
        + "document.head.appendChild(s);"
        + "}else if(!window.myChartFunction){" + getChartFunctionJs() + "}";
    Clients.evalJavaScript(loadECharts);
}

private String getChartFunctionJs() {
    return "window.myChartFunction=function(dom,data){...};";
}

精妙之處在三層防禦:

  • 第一層:if(!window.echarts) — 如果 ECharts 還沒載入,動態建立 <script> 元素,掛上 onload callback。CDN 載入完成後,才定義自訂函數。先到糧草,再練兵。
  • 第二層:s.onload — 確保 CDN 載入完畢後才定義函數,不搶時機。
  • 第三層:else if(!window.myChartFunction) — 如果使用者重複開啟 Form,ECharts 已經在記憶體裡了,直接定義函數就好,不重複載入 CDN。老將不用再訓練,直接上陣。

攻心第二策:CSS class 容器 — 不依賴 DOM ID

第二步:在 ZUL 中用 sclass(CSS class)標記圖表容器,不要idw:onCreate 設定 DOM ID。

<div sclass="rnd-trend-chart" hflex="1" height="750px"
     visible="@load(not empty vm.testResults)"/>

為什麼?因為第五擒的慘痛教訓告訴我們:ZK 的 DOM ID 是它自己管的,你硬塞一個 ID 進去,就像在別人家裡釘一個門牌——房東隨時可以拆掉。但 CSS class 不一樣,ZK 不會去動它。用 sclass 標記容器,就像在蠻王的帳篷上掛了一面旗——旗在人在,JavaScript 隨時能找到。

攻心第三策:querySelector + setTimeout 重試機制

第三步:在 ViewModel 中用 document.querySelector 找容器,配合 IIFE + setTimeout 重試機制,等待 CDN 非同步載入完成。

@Command
public void refreshChart() {
    String jsonData = buildJsonData();

    Clients.evalJavaScript(
        "(function _try(){"
        + "var dom=document.querySelector('.rnd-trend-chart');"
        + "if(!dom||!window.echarts){setTimeout(_try,200);return;}"
        + "var chart=echarts.getInstanceByDom(dom)||echarts.init(dom);"
        + "// ... chart.setOption(...) ..."
        + "})()");
}

這段程式碼的精髓是一個自動重試的 IIFE(Immediately Invoked Function Expression):

  • document.querySelector('.rnd-trend-chart') — 用 CSS class 找 DOM 元素,穩如泰山
  • if(!dom||!window.echarts) — 如果容器還沒渲染,或 ECharts CDN 還沒載入完畢,200 毫秒後重試
  • echarts.getInstanceByDom(dom)||echarts.init(dom) — 如果已有圖表實例就複用,沒有就新建

這就是攻心為上的最高境界:不催不逼,耐心等待,條件成熟自然水到渠成。蠻王你什麼時候準備好,我什麼時候就來——反正每 200 毫秒我會來看一眼。

五擒之後,心服口服。蠻王終於跪地稱臣。

行軍佈陣:Layout 注意事項

佈陣口訣:「地形不察,三軍覆沒。」

蠻王雖服,但班師回朝的路上還有兩處險地需要小心。這不是敵人的問題,是你自己的行軍隊列出了亂子。

險地一:固定高度 chart + vflex 衝突

你把一個 750px 高的 ECharts 圖表和一個 vflex="1" 的 listbox 放在同一個 <vlayout> 裡,會發生什麼事?

<!-- 錯誤:750px chart 會把 vflex="1" 的 listbox 擠到 0 高度 -->
<vlayout vflex="1">
    <listbox vflex="1"/>        <!-- 被擠壓 -->
    <div height="750px"/>       <!-- 吃掉所有空間 -->
</vlayout>

答案是:listbox 被擠成一條線,高度歸零。750px 的大將軍往那裡一站,把所有空間都吃光了,其他士兵只能貼牆站。

解法:listbox 也用固定 height,外層 <vlayout> 加上 overflow-y:auto 啟用捲動。行軍隊列排不下?那就讓隊伍可以捲動,別硬擠在一條路上:

<vlayout hflex="1" vflex="1" style="overflow-y:auto;">
    <listbox height="200px"/>
    <groupbox>
        <listbox height="150px"/>
    </groupbox>
    <div sclass="rnd-trend-chart" height="750px"/>
</vlayout>

險地二:多指標子圖的動態高度

當有多個 TestSpec 需要顯示子圖時,750px 的固定高度不夠用。你需要根據子圖數量動態調整容器高度。這就像行軍途中遇到山地——地形變了,陣型也得跟著變:

var n = data.specs.length || 1;
dom.style.height = (n * 250) + 'px';
// 先 dispose 再 init,確保 ECharts 重新計算尺寸
var chart = echarts.getInstanceByDom(dom);
if (chart) { chart.dispose(); }
chart = echarts.init(dom);

重點是先 dispose()init()。ECharts 初始化時會記住容器的尺寸,你改了高度但沒有重新初始化,它還是按原來的尺寸畫圖——就像士兵還按照平原陣型在山路上行軍,不撞牆才怪。

收服餘黨:四員叛將逐一歸降

收服口訣:「蠻王雖降,餘黨未靖。」

JavaScript 蠻王是服了,但他手下還有四員小將,各有各的脾氣。這四位不解決,你的 CustomForm 就別想太平。

不聽號令的副將:@bind 缺少 getter/setter

ZUL 裡寫了 @bind 雙向綁定:

<listbox selectedItem="@bind(vm.selectedItem)"/>

ViewModel 裡卻只有 getter 沒有 setter:

public MyType getSelectedItem() { return selectedItem; }
public void setSelectedItem(MyType item) { this.selectedItem = item; }

缺少 setter 會報 PropertyNotWritableException。這位副將只會報告軍情(getter),但你下命令他聽不懂(沒有 setter),因為他耳朵是裝飾品。@bind 是雙向的,getter 和 setter 缺一不可。

變臉將軍:Yes-No Boolean/String 衝突

iDempiere 的 Yes-No 欄位(AD_Reference_ID=20)在資料庫存 char(1)Y/N),但 PO.get_Value() 回傳的是 Boolean。Generated X_ class 的 getter 如果直接用 (String)get_Value(...),就會被一記 ClassCastException 打臉——因為你收到的是 Boolean,不是 String。

這位將軍進營時說自己是字串(DB 存 char),進了帳篷卻變成布林值。標準的變臉將軍。

修正:先檢查型別再轉換:

public String getIsComplete() {
    Object oo = get_Value(COLUMNNAME_IsComplete);
    if (oo instanceof Boolean) return ((Boolean)oo) ? "Y" : "N";
    return (String)oo;
}

身材限制:varchar(1) 只能存單字元

資料庫欄位 Result varchar(1) 只能存一個字元。你的 ViewModel 回傳 "Pass""Fail"?超長,塞不進去。這位將軍的盔甲尺碼只有 XS,你硬要他穿 XXL,結果當然是——穿不上。

解法:存 DB 用縮寫,顯示用全名。分兩套邏輯:

public String getPassFail() {    // 存 DB 用
    if (...) return "F";
    return "P";
}
public String getPassFailLabel() {  // ZUL 顯示用
    String pf = getPassFail();
    if ("P".equals(pf)) return "Pass";
    if ("F".equals(pf)) return "Fail";
    return "";
}

多餘的監軍:冗餘的 apply="BindComposer"

當 ZUL 已經指定了 viewModel="@id('vm') @init(...)" 時,ZK 會自動套用 BindComposer。你又手動加一個 apply="org.zkoss.bind.BindComposer"?那就等於派了兩個監軍到同一支軍隊——命令打架,Parser 警告一堆。

這位監軍純屬多餘,可以安全移除。少一個人指手畫腳,軍心反而更穩。

孟獲曰:蠻王的認輸感言

吾乃 JavaScript 蠻王孟獲。

自瀏覽器開天闢地以來,吾橫行 DOM,縱橫 window,天下莫敢不從。

第一戰,丞相遣 <script> 直入 <borderlayout>,被五子(north、south、east、west、center)擋在門外。吾在城頭看戲,笑得前仰後合。

第二戰,丞相將 <script> 埋伏於 <vlayout> 暗處,吾只需靜待時機錯亂,一句 is not defined 便讓伏兵全軍覆沒。不費吹灰之力。

第三戰,丞相繞道 <?script?> 小徑,~./ 糧道卻通往他人營寨。吾連出手都不必,丞相自己迷了路。

第四戰,丞相改走絕對路徑 /rnd/,結果此路根本不存在。吾在山頭納涼,看丞相在荒野中轉了一夜。

第五戰,丞相策反 DOM ID 為內應,豈料 ZK lifecycle 一聲令下,內應即刻變節歸吾。丞相的人,終究是 ZK 的人。

五戰五勝!吾當時意氣風發,以為這諸葛亮不過爾爾——蠻荒之地,終究是吾的天下。

然後丞相使出 Clients.evalJavaScript()——不攻城,不設伏,不繞路,不策反。直接走進吾的心裡,用三策定乾坤:動態載入確保時機、CSS class 確保定位、setTimeout 確保耐心。

吾服了。

自今日起,吾不再妄圖從 ZUL 中脫逃,不再依賴不可靠的 DOM ID,乖乖待在 evalJavaScript() 的旗幟之下。丞相要吾畫圖,吾便畫圖;丞相要吾重試,吾便每 200 毫秒報到一次。

南人不復反矣。

English

Prelude to the Southern Campaign: The Barbarian King Stirs

Since ancient times, wild frontiers breed untamed kings. And in the land of iDempiere OSGI, one barbarian king reigns supreme.

In our previous tale, the Chancellor (Zhuge Liang) demonstrated the Eight Formations strategy — teaching the world how to deploy a CustomForm in iDempiere, conquering everything from OSGI ClassLoader supply lines to ZK MVVM battle flags. Just as the Chancellor was about to return to the capital for a well-earned cup of tea, a scout galloped in: “Chancellor! The southern barbarians have risen again!”

And who is this barbarian king? JavaScript.

That JavaScript — accustomed to running wild in browsers — refuses to submit even on ZK Framework’s turf. Want to load ECharts CDN in an OSGI plugin’s CustomForm? Denied. Want to use ZUL’s <script> tag to import custom JS? It laughs at your naivety. Want to sneak in via <?script?> Processing Instruction? Every path is blocked.

The Chancellor knew well: you cannot subdue the barbarian king by brute force alone. Only by winning hearts and minds can you prevail. And so he marched south — capturing and releasing Meng Huo five times — until the JavaScript barbarian king finally submitted, kneeling beneath the banner of Clients.evalJavaScript().

This article chronicles that entire southern campaign — five failed approaches (the five captures and releases), one ultimate solution (winning hearts and minds), and four rebel officers subdued along the march. All code is battle-tested. Copy and deploy.

First Capture: Frontal Assault — <script> as a <borderlayout> Child

Battle cry: “We need JavaScript? Just shove a <script> tag in there!”

The Chancellor started simple enough: Meng Huo is just JavaScript, right? Surely the mighty Han dynasty can handle a frontal assault — drop two <script> tags inside <borderlayout>, one for CDN, one for custom JS, and call it a day.

<borderlayout>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"/>
    <script src="~./js/rnd-charts.js"/>
    <north>...</north>
</borderlayout>

The result? Meng Huo roared with laughter and sauntered away. ZK slapped back with:

Unsupported child for Borderlayout: <Script null>

Turns out ZK’s <borderlayout> is an extremely picky eater — it only accepts five children: <north>, <south>, <east>, <west>, <center>. Shoving a <script> in there is like bringing a barbarian envoy into the imperial throne room — the palace guards block you at the door: “This person is not on the guest list.”

First capture. Meng Huo is unimpressed. Released.

Second Capture: The Ambush — <script> Inside a Conditionally Visible Container

Battle cry: “Frontal assault failed? Let’s set an ambush! Hide it inside vlayout!”

After reviewing the first defeat, the Chancellor opted for subterfuge. Hide the <script> inside a conditionally visible <vlayout>, and load it when the time is right:

<vlayout visible="@load(not vm.testEntryMode)">
    <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"/>
    <script src="~./js/rnd-charts.js"/>
    <div id="chartContainer"/>
</vlayout>

No structural error this time. The Chancellor thought: “Brilliant, the ambush worked—” Before the words left his mouth, the console fired back:

renderTestTrendChart is not defined

Meng Huo doubled over laughing: “Your ambush troops never even attacked!”

The problem: a <script> inside a conditionally hidden container has completely unpredictable execution timing. When the container is visible="false", the scripts inside may never execute at all. When the container becomes visible, the execution order is chaos. It’s like stationing three thousand soldiers in a mountain pass — the horn sounds, but they’re still asleep. Wrong timing, and even the most brilliant ambush is worthless.

Second capture. Meng Huo remains defiant. Released again.

Third Capture: Flanking Maneuver — <?script?> Processing Instruction + ~./ Path

Battle cry: “Can’t breach the gates? Take the mountain trail!”

The Chancellor consulted the ZK military manual (official docs) and found a side path: <?script?> Processing Instruction. This isn’t a ZK component — it’s a page-level directive, free from <borderlayout>‘s picky diet:

<?script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"?>
<?script src="~./js/rnd-charts.js"?>
<borderlayout>...</borderlayout>

The Chancellor was thrilled: “Brilliant! A flanking maneuver — Meng Huo won’t see this coming!”

The CDN path worked — ECharts loaded successfully. But the custom rnd-charts.js? The console replied coldly:

renderTestTrendChart is not defined

Meng Huo chuckled: “Your supply line went to the wrong camp.”

The issue is the ~./ path. <?script?> is page-level processing, so ~./ resolves to the main webapp (/webui/), not your OSGI bundle’s Web-ContextPath. The CDN is an external URL, so it’s fine — but your bundle’s JS files? The quartermaster drove the supply wagons ten miles… straight into someone else’s camp.

Third capture. Meng Huo laughs heartily and walks away. Released.

Fourth Capture: Rerouting — <?script?> + Absolute Path

Battle cry: “~./ went wrong? I’ll use an absolute path!”

The Chancellor was getting frustrated. If ~./ resolves incorrectly, then surely using the OSGI bundle’s Web-ContextPath as an absolute path should work?

<?script src="/rnd/js/rnd-charts.js"?>

Deployed with full confidence…

File still not found

Meng Huo didn’t even bother laughing this time. Just shook his head: “This road doesn’t exist.”

The OSGI bundle’s Web-ContextPath: /rnd doesn’t necessarily serve static JS files correctly. This depends on iDempiere’s web resource configuration. The bundle’s Web-ContextPath is primarily for ZK’s ~./ mechanism, not a general-purpose static file server. You picked a different road, but that road simply doesn’t exist — drawn on the map, but a cliff in reality.

Fourth capture. Meng Huo shakes his head with a sigh. Released.

Fifth Capture: Turning a Spy — w:onCreate to Set DOM ID

Battle cry: “Can’t break in? Let’s turn someone on the inside!”

Four frontal engagements, four defeats. The Chancellor changed tactics — instead of loading JS from outside, plant a mole inside ZK’s rendered DOM. Use w:onCreate to manually set a fixed DOM ID on the client side, so JavaScript can find the chart container via getElementById:

<div id="trendChart" w:onCreate="this.$n().id = 'rndTrendChart';"/>

Then in JavaScript:

document.getElementById('rndTrendChart')

Seems airtight? The spy has infiltrated enemy territory, just waiting for the signal to open the gates…

Then the spy defected.

ZK’s widget lifecycle has its own rules: it may overwrite your manually set DOM ID during certain rendering phases, or the widget might not be fully initialized when you call getElementById. The spy (DOM ID) was planted, but under pressure from ZK’s lifecycle, he “surrendered” — sometimes it works, sometimes the element can’t be found. Completely unreliable.

It’s like turning one of Meng Huo’s generals and agreeing to open the gates at high noon — only for the general to discover at the appointed hour that the gate key isn’t in his hands, and he’s not entirely sure where the gate is either.

Fifth capture. Meng Huo smirks coldly. The Chancellor sits in silence. Five captures, five releases. The score remains unsettled.

Total Submission: Winning Hearts and Minds — Clients.evalJavaScript()

Grand strategy: “Attacking the city is inferior; winning hearts is supreme. The highest art of war is to conquer the whole nation intact.”

Five head-on collisions, five retreats. The Chancellor sat alone in his tent for three days, poring over every military text, until he arrived at a single insight: It’s not that JavaScript won’t obey — you’ve been commanding it the wrong way all along.

ZUL’s <script> and <?script?> both load JS on “ZK’s territory” — paths follow ZK’s rules, timing depends on ZK’s mood. But Clients.evalJavaScript() is different. It sends JavaScript directly from the Java side to the browser for execution, bypassing all of ZK’s path resolution and component lifecycle restrictions.

This is winning hearts and minds — don’t fight the barbarian king on his turf. Walk straight into his heart, and he’ll serve you willingly.

Strategy One: loadChartScripts() — Dynamic CDN Loading + Function Definition

Step one: in Form.java’s initForm(), use Clients.evalJavaScript() to dynamically create a <script> element that loads the ECharts CDN, then define custom functions after the CDN loads.

@Override
protected void initForm() {
    // ... ZUL loading, WSearchEditor setup ...

    // Load ECharts CDN + define chart function
    loadChartScripts();
}

private void loadChartScripts() {
    String loadECharts = "if(!window.echarts){"
        + "var s=document.createElement('script');"
        + "s.src='https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js';"
        + "s.onload=function(){" + getChartFunctionJs() + "};"
        + "document.head.appendChild(s);"
        + "}else if(!window.myChartFunction){" + getChartFunctionJs() + "}";
    Clients.evalJavaScript(loadECharts);
}

private String getChartFunctionJs() {
    return "window.myChartFunction=function(dom,data){...};";
}

The brilliance lies in three layers of defense:

  • Layer one: if(!window.echarts) — If ECharts isn’t loaded yet, dynamically create a <script> element with an onload callback. Custom functions are defined only after the CDN finishes loading. Supplies first, then train the troops.
  • Layer two: s.onload — Ensures functions are defined only after the CDN is fully loaded. No jumping the gun.
  • Layer three: else if(!window.myChartFunction) — If the user reopens the Form and ECharts is already in memory, just define the functions — no redundant CDN loading. Veteran soldiers need no retraining; deploy them straight to battle.

Strategy Two: CSS Class Container — No DOM ID Dependency

Step two: in ZUL, mark the chart container with sclass (CSS class). Do not use id or w:onCreate to set a DOM ID.

<div sclass="rnd-trend-chart" hflex="1" height="750px"
     visible="@load(not empty vm.testResults)"/>

Why? Because the painful lesson of the fifth capture taught us: ZK owns its DOM IDs. You hardcode one, and it’s like nailing a nameplate to someone else’s door — the landlord can rip it off anytime. But CSS classes? ZK doesn’t touch those. Using sclass to mark a container is like planting a flag on Meng Huo’s tent — the flag stays, and JavaScript can always find it.

Strategy Three: querySelector + setTimeout Retry Mechanism

Step three: in the ViewModel, use document.querySelector to find the container, paired with an IIFE + setTimeout retry mechanism to wait for the asynchronous CDN load to complete.

@Command
public void refreshChart() {
    String jsonData = buildJsonData();

    Clients.evalJavaScript(
        "(function _try(){"
        + "var dom=document.querySelector('.rnd-trend-chart');"
        + "if(!dom||!window.echarts){setTimeout(_try,200);return;}"
        + "var chart=echarts.getInstanceByDom(dom)||echarts.init(dom);"
        + "// ... chart.setOption(...) ..."
        + "})()");
}

The essence of this code is a self-retrying IIFE (Immediately Invoked Function Expression):

  • document.querySelector('.rnd-trend-chart') — Finds the DOM element by CSS class. Rock solid.
  • if(!dom||!window.echarts) — If the container hasn’t rendered yet, or the ECharts CDN hasn’t finished loading, retry in 200 milliseconds.
  • echarts.getInstanceByDom(dom)||echarts.init(dom) — Reuse the existing chart instance if one exists; create a new one otherwise.

This is the highest form of winning hearts and minds: no rushing, no pressure, just patient waiting until conditions are ripe. Meng Huo, whenever you’re ready, I’ll be here — I check in every 200 milliseconds anyway.

After five captures, total submission. The barbarian king finally kneels.

March Formation: Layout Pitfalls

Formation rule: “Fail to survey the terrain, and the entire army perishes.”

Meng Huo has submitted, but the road home has two treacherous stretches. This isn’t about the enemy — it’s your own marching formation that’s out of order.

Hazard One: Fixed-Height Chart + vflex Conflict

What happens when you put a 750px-tall ECharts chart and a vflex="1" listbox inside the same <vlayout>?

<!-- Wrong: 750px chart crushes vflex="1" listbox to 0 height -->
<vlayout vflex="1">
    <listbox vflex="1"/>        <!-- gets crushed -->
    <div height="750px"/>       <!-- eats all space -->
</vlayout>

Answer: the listbox gets crushed to a sliver — zero height. The 750px general plants himself there and devours all available space, leaving the other soldiers pressed against the wall.

Fix: Give the listbox a fixed height too, and add overflow-y:auto to the outer <vlayout> for scrolling. Marching column too long for the road? Let the column scroll — don’t force everyone to squeeze into a single lane:

<vlayout hflex="1" vflex="1" style="overflow-y:auto;">
    <listbox height="200px"/>
    <groupbox>
        <listbox height="150px"/>
    </groupbox>
    <div sclass="rnd-trend-chart" height="750px"/>
</vlayout>

Hazard Two: Dynamic Height for Multiple Sub-Charts

When multiple TestSpecs require sub-charts, a fixed 750px height isn’t enough. You need to dynamically adjust the container height based on the number of sub-charts. It’s like encountering mountains mid-march — the terrain changed, so the formation must adapt:

var n = data.specs.length || 1;
dom.style.height = (n * 250) + 'px';
// dispose then init — force ECharts to recalculate dimensions
var chart = echarts.getInstanceByDom(dom);
if (chart) { chart.dispose(); }
chart = echarts.init(dom);

The key is dispose() before init(). ECharts memorizes the container’s dimensions at initialization. If you change the height without reinitializing, it still draws at the old size — like soldiers marching in plains formation on a mountain trail. Of course they’ll crash into walls.

Subduing the Remaining Rebels: Four Officers Brought to Heel

Mopping-up rule: “The king has surrendered, but his lieutenants still roam free.”

The JavaScript barbarian king has submitted, but four of his officers remain at large, each with their own attitude problem. Leave them unsettled, and your CustomForm will never know peace.

The Deaf Lieutenant: @bind Missing getter/setter

ZUL declares a two-way @bind:

<listbox selectedItem="@bind(vm.selectedItem)"/>

But the ViewModel only has a getter and no setter:

public MyType getSelectedItem() { return selectedItem; }
public void setSelectedItem(MyType item) { this.selectedItem = item; }

Missing the setter causes PropertyNotWritableException. This lieutenant can report intelligence (getter), but when you give orders he can’t hear you (no setter) — his ears are purely decorative. @bind is bidirectional. Getter and setter are both mandatory.

The Shape-Shifting General: Yes-No Boolean/String Conflict

iDempiere’s Yes-No field (AD_Reference_ID=20) stores char(1) (Y/N) in the database, but PO.get_Value() returns a Boolean. If the generated X_ class getter uses (String)get_Value(...) directly, you’ll get slapped with a ClassCastException — because what you received is a Boolean, not a String.

This general enters camp claiming to be a String (the DB stores char), but once inside the tent, he shapeshifts into a Boolean. A classic two-faced officer.

Fix: Check the type before casting:

public String getIsComplete() {
    Object oo = get_Value(COLUMNNAME_IsComplete);
    if (oo instanceof Boolean) return ((Boolean)oo) ? "Y" : "N";
    return (String)oo;
}

The Undersized Armor: varchar(1) Can Only Store One Character

Database column Result varchar(1) can only store a single character. Your ViewModel returns "Pass" or "Fail"? Way too long — it won’t fit. This officer’s armor only comes in XS, and you’re trying to force him into XXL. Naturally, it doesn’t fit.

Fix: Store abbreviations in the DB, display full names in the UI. Two separate logics:

public String getPassFail() {    // for DB storage
    if (...) return "F";
    return "P";
}
public String getPassFailLabel() {  // for ZUL display
    String pf = getPassFail();
    if ("P".equals(pf)) return "Pass";
    if ("F".equals(pf)) return "Fail";
    return "";
}

The Redundant Overseer: Unnecessary apply="BindComposer"

When ZUL already specifies viewModel="@id('vm') @init(...)", ZK automatically applies BindComposer. Adding apply="org.zkoss.bind.BindComposer" on top of that is like assigning two overseers to the same army — conflicting orders and a flood of parser warnings.

This overseer is entirely redundant and can be safely removed. One fewer person barking orders, and morale actually improves.

Meng Huo’s Surrender Speech

I am JavaScript — Meng Huo, King of the Southern Barbarians.

Since the dawn of the browser, I have ruled the DOM, conquered the window object, and none have dared defy me.

In the first battle, the Chancellor sent <script> charging straight into <borderlayout>, only to be blocked at the gate by the Five Brothers (north, south, east, west, center). I watched from the ramparts, laughing myself hoarse.

In the second battle, the Chancellor hid <script> in ambush inside a <vlayout>. All I had to do was wait for the timing to unravel — one is not defined and the ambush collapsed. Didn’t even break a sweat.

In the third battle, the Chancellor took the <?script?> back trail, but the ~./ supply route led him straight into someone else’s camp. I didn’t even have to lift a finger — he got lost on his own.

In the fourth battle, the Chancellor tried the absolute path /rnd/. The road didn’t exist. I lounged on the hilltop, watching him wander the wilderness all night.

In the fifth battle, the Chancellor turned a DOM ID as his inside man. But ZK’s lifecycle gave one order, and the spy defected right back to me. The Chancellor’s man was always ZK’s man.

Five battles. Five victories! I was riding high, thinking this Zhuge Liang was nothing special — the wild frontier would forever be my domain.

And then the Chancellor deployed Clients.evalJavaScript() — no siege, no ambush, no detour, no espionage. He walked straight into my heart and settled everything with three strategies: dynamic loading to ensure timing, CSS classes to ensure targeting, setTimeout to ensure patience.

I submit.

From this day forward, I shall no longer attempt to escape through ZUL, no longer rely on unreliable DOM IDs. I shall stand obediently beneath the banner of evalJavaScript(). When the Chancellor commands me to draw charts, I draw charts. When the Chancellor commands me to retry, I report for duty every 200 milliseconds.

The southern barbarians shall never rebel again.

日本語

南征序曲:蛮荒の地に蛮王あり

古来、蛮荒の地には猛獣が多く、iDempiere OSGI の地には JavaScript という蛮王がいる。

前回の物語で、丞相・諸葛孔明は八陣図の法をもって、iDempiere における CustomForm の布陣方法を天下に示した。OSGI ClassLoader の兵站から ZK MVVM の陣旗まで、一路破竹の勢い。丞相が「天下太平、成都に帰って茶でも飲もう」と思った矢先、斥候が駆け込んできた。「丞相!南蛮がまた反乱です!」

南蛮王とは誰か? JavaScript である。

あの JavaScript、ブラウザの中では横行闊歩し放題だったくせに、ZK Framework の領地に来てもまるで服従しない。OSGI プラグインの CustomForm で ECharts CDN を読み込みたい?許さない。ZUL の <script> タグでカスタム JS を導入したい?甘いと嗤う。<?script?> Processing Instruction で迂回奇襲?道ごと封鎖される。

丞相は深く悟った。蛮王は力で敵わぬ、ただ攻心為上あるのみ。かくして丞相は大軍を率いて南征し、五たび擒え五たび放ち、JavaScript 蛮王がついに心服口服し、Clients.evalJavaScript() の旗の下にひれ伏すまで戦い続けた。

本稿はこの南征戦役の全記録である——五つの失敗した試み(五擒五縦)、一つの究極解法(攻心為上)、そして行軍途中に収服した四人の叛将。すべてのコードは実戦検証済み、コピーして即使用可能。

第一擒:正面強攻 — <script><borderlayout> の子要素に

戦術口訣:「JavaScript を読み込みたい?<script> を突っ込めばいいじゃないか!」

丞相も最初は単純に考えた。蛮王といっても所詮 JavaScript だろう?堂々たる大漢天朝、正面から攻めればよい。<borderlayout> の中に <script> を二つ放り込めば解決——CDN 用と自作 JS 用だ。

<borderlayout>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"/>
    <script src="~./js/rnd-charts.js"/>
    <north>...</north>
</borderlayout>

結果は?蛮王は大笑いし、悠然と立ち去った。ZK が容赦なくエラーを叩きつける:

Unsupported child for Borderlayout: <Script null>

実は ZK の <borderlayout> は極度の偏食家で、受け入れる子要素は五つだけ:<north><south><east><west><center><script> を入れようとするのは、蛮族の使者を皇宮の正殿に連れ込むようなもの——禁衛軍が即座に門前で阻む。「この者、名簿に載っておらぬ。」

第一擒、蛮王服さず、これを縦す。

第二擒:伏兵の計 — 条件付き表示コンテナ内の <script>

戦術口訣:「正面がだめなら伏兵だ!vlayout に隠せばいけるだろう?」

第一戦の反省を踏まえ、丞相は暗手を打つことにした。<script> を条件付き表示の <vlayout> コンテナに忍ばせ、時機が来たら読み込む作戦だ:

<vlayout visible="@load(not vm.testEntryMode)">
    <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"/>
    <script src="~./js/rnd-charts.js"/>
    <div id="chartContainer"/>
</vlayout>

今度は構造エラーは出なかった。丞相は「妙なり、伏兵成功——」と言いかけたその瞬間、コンソールがバシッと返してきた:

renderTestTrendChart is not defined

蛮王は腹を抱えて笑った。「お前の伏兵、出撃すらしていないぞ!」

問題は、条件付きで非表示にされたコンテナ内の <script> は実行タイミングがまったく予測不能だということ。コンテナが visible="false" の時、中の script はそもそも実行されない可能性がある。コンテナが可視になっても、script の実行順序は混沌。山谷に三千の伏兵を配置したのに、号砲が鳴った時にはまだ全員寝ていたようなものだ——タイミングが合わなければ、どんな精妙な伏兵も無駄である。

第二擒、蛮王なお服さず、再びこれを縦す。

第三擒:迂回奇襲 — <?script?> Processing Instruction + ~./ パス

戦術口訣:「城門が破れぬなら、山道を迂回せよ!」

丞相は ZK の兵法書(公式ドキュメント)を紐解き、一本の間道を発見した。<?script?> Processing Instruction だ。これは ZK コンポーネントではなく、ページレベルの処理指令であり、<borderlayout> の偏食制限を受けない:

<?script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"?>
<?script src="~./js/rnd-charts.js"?>
<borderlayout>...</borderlayout>

丞相は大喜び。「この計、妙なり!迂回奇襲、蛮王は防げまい!」

CDN の道は確かに通じた——ECharts ライブラリは無事に読み込まれた。だがカスタムの rnd-charts.js は?コンソールが冷たく返した:

renderTestTrendChart is not defined

蛮王はニヤリと笑う。「お前の兵站、間違った陣営に届いたぞ。」

問題は ~./ パスにあった。<?script?> はページレベルの処理であり、~./ パスはメインの webapp(/webui/)に解決される。OSGI バンドルの Web-ContextPath ではない。CDN は外部リンクなのでパスの影響を受けないが、バンドル内の JS ファイルは?兵糧官が輜重を率いて十里進んだ結果、着いたのは他人の陣営だった。

第三擒、蛮王大笑して去る、またこれを縦す。

第四擒:別路再攻 — <?script?> + 絶対パス

戦術口訣:「~./ が間違えた?なら絶対パスを書けばよい!」

丞相はやや頭に血が上っていた。~./ の解決が間違っているなら、OSGI バンドルの Web-ContextPath を絶対パスとして直接指定すれば、たどり着けるはずだ。

<?script src="/rnd/js/rnd-charts.js"?>

自信満々でデプロイ……

ファイルが見つからない

蛮王は今度は笑う気にもならず、ただ首を振った。「この道は通じぬ。」

OSGI バンドルの Web-ContextPath: /rnd が静的 JS ファイルを正しく配信するとは限らない。これは iDempiere の web resource 設定に依存する。バンドルの Web-ContextPath は主に ZK の ~./ メカニズム用であり、汎用の静的ファイルサーバーではない。別の道を選んだつもりが、その道はそもそも存在しなかった——地図には描かれているが、実地は断崖だった。

第四擒、蛮王嘆息して首を振る、再びこれを縦す。

第五擒:内応工作 — w:onCreate で DOM ID を設定

戦術口訣:「外から攻められぬなら、内部に間者を送り込め!」

四度の正面衝突、四度の敗北。丞相は戦略を転換した——外部から JS ファイルを読み込むのではなく、ZK がレンダリングした DOM の中に内応を潜入させる。具体的には w:onCreate でクライアント側に固定の DOM ID を手動設定し、JavaScript が getElementById でチャートコンテナを見つけられるようにする:

<div id="trendChart" w:onCreate="this.$n().id = 'rndTrendChart';"/>

そして JavaScript 側で:

document.getElementById('rndTrendChart')

完璧に見える?内応はすでに敵陣に潜入し、合図一つで城門を開ける手はずだった……

ところが、内応が寝返った。

ZK の widget lifecycle には独自のルールがある。特定のレンダリング段階で手動設定した DOM ID を上書きすることがあるし、getElementById を呼び出した時点で widget の初期化が完了していない可能性もある。内応(DOM ID)は確かに潜入させたが、ZK lifecycle の圧力の下で「投降」してしまった——ある時は機能し、ある時は要素が見つからない。まったく信用できない。

蛮王の大将を一人寝返らせ、正午に城門を開ける約束をしたのに、いざその時刻になってみると、大将は城門の鍵を持っておらず、そもそも城門がどこにあるかも定かでなかった——そんな話だ。

第五擒、蛮王冷笑す。丞相沈黙す。五擒五縦、勝負いまだつかず。

心服口服:攻心為上 — Clients.evalJavaScript()

戦略口訣:「城を攻むるは下策、心を攻むるは上策。兵法の極意、全国を上と為す。」

五度の真っ向勝負、五度の敗退。丞相は陣中に三日間こもり、兵法書を隅々まで読み返し、ついに一つの真理に到達した。JavaScript が言うことを聞かないのではない。お前がずっと間違った方法で命令していたのだ。

ZUL の <script><?script?> はいずれも「ZK の領地」で JS を読み込むもの——パスは ZK のルールに従い、タイミングは ZK の顔色次第。だが Clients.evalJavaScript() は違う。Java 側からブラウザへ直接 JavaScript を送って実行させる。ZK のパス解決もコンポーネントのライフサイクル制限も、すべて迂回する。

これが攻心為上——蛮王の地盤で戦うのではなく、直接その心の中に歩み入り、心からの忠誠を得る。

攻心第一策:loadChartScripts() — CDN 動的読み込み+関数定義

第一歩:Form.java の initForm() で、Clients.evalJavaScript() を使い <script> 要素を動的に生成して ECharts CDN を読み込み、CDN の読み込み完了後にカスタム関数を定義する。

@Override
protected void initForm() {
    // ... ZUL loading, WSearchEditor setup ...

    // Load ECharts CDN + define chart function
    loadChartScripts();
}

private void loadChartScripts() {
    String loadECharts = "if(!window.echarts){"
        + "var s=document.createElement('script');"
        + "s.src='https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js';"
        + "s.onload=function(){" + getChartFunctionJs() + "};"
        + "document.head.appendChild(s);"
        + "}else if(!window.myChartFunction){" + getChartFunctionJs() + "}";
    Clients.evalJavaScript(loadECharts);
}

private String getChartFunctionJs() {
    return "window.myChartFunction=function(dom,data){...};";
}

精妙なのは三層の防御にある:

  • 第一層:if(!window.echarts) — ECharts がまだ読み込まれていなければ、<script> 要素を動的生成し、onload コールバックを設定。CDN の読み込みが完了してから、初めてカスタム関数を定義する。まず兵糧を確保し、それから兵を鍛える。
  • 第二層:s.onload — CDN の読み込みが完了するまで関数定義を待つ。焦らない。
  • 第三層:else if(!window.myChartFunction) — ユーザーが Form を再度開いた場合、ECharts はすでにメモリにある。関数だけ定義すればよく、CDN を重複読み込みする必要はない。歴戦の将は再訓練不要、即座に出陣せよ。

攻心第二策:CSS class コンテナ — DOM ID に依存しない

第二歩:ZUL で sclass(CSS class)を使ってチャートコンテナを標識する。絶対に idw:onCreate で DOM ID を設定してはならない。

<div sclass="rnd-trend-chart" hflex="1" height="750px"
     visible="@load(not empty vm.testResults)"/>

なぜか?第五擒の痛い教訓が教えてくれた——ZK の DOM ID は ZK が管理するものだ。お前が無理やり ID を設定するのは、他人の家に勝手に表札を釘打ちするようなもの——大家はいつでも引き剥がせる。だが CSS class は違う。ZK はそれに手を出さない。sclass でコンテナを標識するのは、蛮王の天幕に旗を立てるようなもの——旗がある限り、JavaScript はいつでも見つけられる。

攻心第三策:querySelector + setTimeout リトライ機構

第三歩:ViewModel で document.querySelector を使ってコンテナを検索し、IIFE + setTimeout のリトライ機構で CDN の非同期読み込み完了を待つ。

@Command
public void refreshChart() {
    String jsonData = buildJsonData();

    Clients.evalJavaScript(
        "(function _try(){"
        + "var dom=document.querySelector('.rnd-trend-chart');"
        + "if(!dom||!window.echarts){setTimeout(_try,200);return;}"
        + "var chart=echarts.getInstanceByDom(dom)||echarts.init(dom);"
        + "// ... chart.setOption(...) ..."
        + "})()");
}

このコードの神髄は自動リトライ IIFE(即時実行関数式)にある:

  • document.querySelector('.rnd-trend-chart') — CSS class で DOM 要素を探す。泰山のごとく盤石。
  • if(!dom||!window.echarts) — コンテナがまだレンダリングされていないか、ECharts CDN の読み込みが完了していなければ、200 ミリ秒後にリトライ。
  • echarts.getInstanceByDom(dom)||echarts.init(dom) — 既存のチャートインスタンスがあれば再利用、なければ新規作成。

これが攻心為上の最高境地——急かさず、圧をかけず、辛抱強く条件が整うのを待つ。蛮王よ、お前の準備ができた時にいつでも参ろう——どうせ 200 ミリ秒ごとに見に来るのだから。

五擒の後、心服口服す。蛮王、ついに膝を屈す。

行軍布陣:レイアウトの注意点

布陣口訣:「地形を察せずんば、三軍覆没す。」

蛮王は服したが、凱旋の道にはまだ二つの険地がある。これは敵の問題ではなく、自軍の行軍隊列の乱れだ。

険地その一:固定高さのチャート + vflex の衝突

750px のECharts チャートと vflex="1" の listbox を同じ <vlayout> に入れたら、何が起きるか?

<!-- 誤り:750px チャートが vflex="1" の listbox を高さ 0 に押し潰す -->
<vlayout vflex="1">
    <listbox vflex="1"/>        <!-- 押し潰される -->
    <div height="750px"/>       <!-- 全スペースを占領 -->
</vlayout>

答えは、listbox が一本の線に押し潰され、高さゼロになる。750px の大将軍がそこにどっかり座り、すべての空間を食い尽くし、他の兵士は壁に張り付くしかない。

解法:listbox にも固定の height を指定し、外側の <vlayout>overflow-y:auto を付けてスクロールを有効にする。行軍隊列が道に収まらない?ならば隊列をスクロール可能にせよ——一本道に無理やり詰め込むな:

<vlayout hflex="1" vflex="1" style="overflow-y:auto;">
    <listbox height="200px"/>
    <groupbox>
        <listbox height="150px"/>
    </groupbox>
    <div sclass="rnd-trend-chart" height="750px"/>
</vlayout>

険地その二:複数指標サブチャートの動的高さ

複数の TestSpec でサブチャートを表示する場合、750px の固定高さでは足りない。サブチャートの数に応じてコンテナの高さを動的に調整する必要がある。行軍中に山地に遭遇するようなもの——地形が変われば、陣形も変えねばならない:

var n = data.specs.length || 1;
dom.style.height = (n * 250) + 'px';
// dispose してから init — ECharts にサイズを再計算させる
var chart = echarts.getInstanceByDom(dom);
if (chart) { chart.dispose(); }
chart = echarts.init(dom);

要点は先に dispose() してから init() すること。ECharts は初期化時にコンテナのサイズを記憶する。高さを変更しても再初期化しなければ、元のサイズで描画し続ける——兵士が平原の陣形のまま山道を行軍するようなもの。壁にぶつからないわけがない。

残党収服:四人の叛将、逐一帰順す

収服口訣:「蛮王は降れど、残党いまだ靖まらず。」

JavaScript 蛮王は降伏した。だがその配下にはまだ四人の小将がおり、それぞれ一癖ある。この四人を片付けなければ、CustomForm に平和は訪れない。

命令を聞かぬ副将:@bind に getter/setter が足りない

ZUL で @bind の双方向バインディングを書いた:

<listbox selectedItem="@bind(vm.selectedItem)"/>

ViewModel には getter しかなく setter がない:

public MyType getSelectedItem() { return selectedItem; }
public void setSelectedItem(MyType item) { this.selectedItem = item; }

setter が欠けると PropertyNotWritableException が発生する。この副将は軍情を報告すること(getter)はできるが、命令を受けること(setter)ができない——耳が飾りだからだ。@bind は双方向。getter と setter、どちらも欠かせない。

変幻将軍:Yes-No Boolean/String の衝突

iDempiere の Yes-No フィールド(AD_Reference_ID=20)はデータベースに char(1)Y/N)で格納されるが、PO.get_Value() が返すのは Boolean だ。生成された X_ クラスの getter が (String)get_Value(...) を直接使うと、ClassCastException で殴られる——受け取ったのは Boolean であって String ではないからだ。

この将軍、入営時には「私は文字列です」と申告し(DB は char で格納)、天幕に入るやブール値に変身する。典型的な変幻将軍だ。

修正:キャストの前に型を確認する:

public String getIsComplete() {
    Object oo = get_Value(COLUMNNAME_IsComplete);
    if (oo instanceof Boolean) return ((Boolean)oo) ? "Y" : "N";
    return (String)oo;
}

体格制限:varchar(1) は一文字しか入らない

データベースカラム Result varchar(1) には一文字しか格納できない。ViewModel が "Pass""Fail" を返す?長すぎて入らない。この将軍の鎧は XS サイズしかないのに、XXL を着せようとしている——当然入るわけがない。

解法:DB には略称を保存し、表示にはフルネームを使う。二系統のロジックに分ける:

public String getPassFail() {    // DB 格納用
    if (...) return "F";
    return "P";
}
public String getPassFailLabel() {  // ZUL 表示用
    String pf = getPassFail();
    if ("P".equals(pf)) return "Pass";
    if ("F".equals(pf)) return "Fail";
    return "";
}

余計な監軍:冗長な apply="BindComposer"

ZUL ですでに viewModel="@id('vm') @init(...)" を指定していれば、ZK は自動的に BindComposer を適用する。その上さらに手動で apply="org.zkoss.bind.BindComposer" を追加すると?同じ軍に監軍を二人派遣したのと同じ——命令が衝突し、パーサー警告の嵐になる。

この監軍はまったくの冗長であり、安全に除去できる。口出しする人間が一人減れば、むしろ軍心は安定する。

孟獲曰:蛮王の降伏の弁

我は JavaScript、南蛮王・孟獲なり。

ブラウザ開闢以来、我は DOM を支配し、window を縦横し、天下に我に逆らう者なし。

第一戦、丞相は <script><borderlayout> に正面から突入させたが、五人の門番(north、south、east、west、center)に阻まれた。我は城壁の上で高みの見物、腹を抱えて笑い転げた。

第二戦、丞相は <script><vlayout> の闇に伏兵として潜ませた。我はただタイミングの乱れを待つだけ——is not defined の一撃で伏兵は全滅。汗一つかかぬ勝利。

第三戦、丞相は <?script?> の間道を迂回したが、~./ の兵站路は他人の陣営に通じていた。我は指一本動かす必要もなかった——丞相が勝手に迷子になった。

第四戦、丞相は絶対パス /rnd/ に切り替えた。が、そもそも道が存在しない。我は山頂で涼みながら、丞相が荒野を一晩さまようのを眺めていた。

第五戦、丞相は DOM ID を内応として送り込んだ。が、ZK lifecycle の一声で内応は即座に我の側に寝返った。丞相の手の者は、所詮 ZK の手の者。

五戦五勝!我は意気揚々、この諸葛孔明も大したことはないと思った——蛮荒の地は、永遠に我の天下であると。

そして丞相は Clients.evalJavaScript() を繰り出した——攻城もせず、伏兵も置かず、迂回もせず、間者も送らず。直接、我の心に歩み入り、三策で乾坤を定めた。動的読み込みでタイミングを保証し、CSS class で位置を保証し、setTimeout で忍耐を保証した。

我、服せり。

本日より、我は ZUL からの脱走を企てず、信頼できぬ DOM ID に頼らず、おとなしく evalJavaScript() の旗の下に立つ。丞相がチャートを描けと命じれば描き、リトライせよと命じれば 200 ミリ秒ごとに参上する。

南人再び反せず。

Ray Lee (System Analyst)
作者 Ray Lee (System Analyst)

iDempeire ERP Contributor, 經濟部中小企業處財務管理顧問 李寶瑞