タイトルをご覧になって、何言ってるの、NativeScriptでxmlからUIレイアウトを作れるなんて常識じゃん、と思われた方も多いと思うので、もう少し補足します。
自分が好きなタイミングでxmlからViewを作りたい! という事です。
現状は、js(ts)モジュールと同名のxmlファイルを自動的に読み込んでくれるオンリーですよね。
AndroidにはlayoutInflaterという仕組みがあって、xmlからいつでもViewを作り出すことができますよね。
iOSもloadFromNibなんてのがありますね。
我らがNativeScriptにも同じようなのがあるはず!
・・・と鼻息を荒くしてたら、ど真ん中直球なのはたぶん無いです。
なので、自分で用意する事にしました。
オープンソースの素晴らしいところは、隅から隅までソースを読めるので、内部的に何をやっているか丸わかりな所。
色々読んでいると、ui/builderモジュールというのがあって、loadという関数が用意されてるのでこれを利用できそうだと、試してみました。
<StackLayout xmlns="http://schemas.nativescript.org/tns.xsd">
<Label text="ほげ"/>
</StackLayout>
こんなのを用意します。
var view = builder.load('view_a.xml');
うん、フツーにダメでした。nullが返ってきてしまいますね。
さらにソースを読んでみると、この関数は絶対パスを与えてあげないといけないらしい。
色々悩みましたが、
function resolveFileName(moduleName, ext) {
var fs = require('file-system');
var currentAppPath = fs.knownFolders.currentApp().path;
var moduleNamePath = fs.path.join(currentAppPath, moduleName);
return require('file-system/file-name-resolver').resolveFileName(moduleNamePath, ext);
}
こんな関数を用意してみました。
var fileName = resolveFileName('view_a', 'xml');
var view = builder.load(fileName);
今度はちゃんとviewにxmlの内容が格納されたようです。やったぜ。
気を良くして、
function inflateLayout(moduleName, context?) {
var fileName = resolveFileName(moduleName, 'xml');
var Builder = require('ui/builder');
return Builder.load(fileName, context);
}
こんな関数も追加してみました。
ん? contextとはなんぞや? と思われた方、鋭いです。
<StackLayout xmlns="http://schemas.nativescript.org/tns.xsd">
<Label text="{{text}}"/>
</StackLayout>
var view = inflateLayout('view_b', {'text': 'ほげ'});
こんな風に、bindできちゃうんですね。NativeScriptならではです。
試してませんが、Obsarbaleでも通りそうな感じの実装でした。
contextなんて名前にダマされましたが、Builder.loadの第2引数はxmlの挙動を操作するjsをrequireしたもののようです。
さらに改良しました。
function inflateLayout(moduleName, context?) {
var fileName = resolveFileName(moduleName, 'xml');
if (fileName) {
var Builder = require('ui/builder');
var jsFileName = resolveFileName(moduleName, 'js');
var exports = null;
if (jsFileName) {
exports = require(jsFileName);
}
var view = Builder.load(fileName, exports);
view.bindingContext = context;
return view;
} else {
return null;
}
}
こんな感じでしょうか。
jsの方の読みこみは検証していないです。。。
これをListViewに応用してみたいと思います。
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo">
<StackLayout>
<ListView items="{{items}}" itemLoading="onItemLoading"/>
</StackLayout>
</Page>
<StackLayout xmlns="http://schemas.nativescript.org/tns.xsd">
<Label text="{{text}}" fontSize="16"/>
</StackLayout>
<StackLayout xmlns="http://schemas.nativescript.org/tns.xsd">
<Label text="{{text}}" fontSize="32"/>
</StackLayout>
import {EventData} from "data/observable";
import {Page} from "ui/page";
import {ItemEventData} from "ui/list-view";
var page: Page;
export function navigatingTo(args: EventData) {
page = <Page>args.object;
page.bindingContext = {
'items': [
{'text': 'test1'},
{'text': 'test2'},
{'text': 'test3'},
{'text': 'test4'},
{'text': 'test5'},
{'text': 'test6'},
{'text': 'test7'},
]
};
}
export function onItemLoading(args: ItemEventData) {
var position = args.index;
var file;
if (position % 2 == 0) {
file = 'cell_small';
} else {
file = 'cell_big';
}
args.view = inflateLayout(file);
}
<以下略>
こんな風にすると、ListViewのテンプレートを書かなくても、1行おきに小さいラベルと大きなラベルが交互に表示されるようなものが出来てしまいます。
片方はラベルじゃなくて画像でももちろんOKです。
各行のUIパーツの高さは異なりますが、きちんと反映されます。
ちなみに、inflateしたviewのbindingContextはどうなってるのか? と思われるかも知れませんが、onItemLoadingの中でviewのbindingContextをいくらいじっても、この関数を呼び出した後、ListViewが無慈悲にも自分のitemsの内容で上書きします。
NativeScriptが公式に複数のテンプレートに対応してくれれば良いのですがねえ。