【SAPUI5】Fragmentを使いこなす

##はじめに
フラグメントとは、再利用可能なUIパーツです。以下のような目的で使うことが多いと思います。
①ビューの一部を部品化するため（再利用のため、またはビューをすっきりさせるため）
②ダイアログ等のポップアップを出すため

フラグメントの種類にはXML、JS、HTMLがありますが、この記事ではXMLを対象にします。
今回は、「これさえ知っていればフラグメントを使うのに困らない」といえる、基礎から応用の技について紹介したいと思います。

##内容
1. フラグメントの定義
2. フラグメントのロード
3. フラグメント内のコントロールの取得
4. フラグメントのコントローラー
5. フラグメントのロード処理を共通化する

###サンプルアプリ
フラグメントの説明のため、サンプルアプリを作成します。画面の一部にフラグメントを埋め込んでいます。
コード：https://github.com/miyasuta/FragmentSample

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4df9a629-ceca-a684-fc33-cd071bf90730.png)

"Show Dialog"ボタンを押すとダイアログが出ます。これもフラグメントで作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8ac4334c-edf8-0c34-3391-70d52c5a1428.png)

プロジェクト構成は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f1d1fcde-bed0-447b-37e6-cf40e19382dd.png)

ODataサービスは定番のNorthwindを使います。
https://services.odata.org/V2/Northwind/Northwind.svc/

##1. フラグメントの定義
フラグメント用のXMLファイルを作成します。ファイルの名称は、`<namespace>.<FragmentName>.fragment.xml`とします。

フラグメント1：ビューに埋め込むフォーム

```xml:Form.fragment.xml
<core:FragmentDefinition
	xmlns="sap.m"
	xmlns:core="sap.ui.core">
	<Panel headerText="{i18n>panelTitle1}">
		<VBox class="sapUiSmallMargin" width="50%">
			<Label text="{i18n>supplierID}" labelFor="supplierId"/>
			<Input id="supplierId" />
			<Label text="{i18n>companyName}" labelFor="companyName"/>
			<Input id="companyName" />
		</VBox>		
	</Panel>
</core:FragmentDefinition>
```

フラグメント2：ダイアログ

```xml:Dialog.fragment.xml
<core:FragmentDefinition
	xmlns="sap.m"
	xmlns:core="sap.ui.core">
	<Dialog title="{i18n>Suppliers}">
		<content>
			<List items="{/Suppliers}">
				<items>
					<StandardListItem
						title="{SupplierID}"
						description="{CompanyName}"
						type="Active" 
						press="onItemPress" />
				</items>
			</List>
		</content>
	</Dialog>
</core:FragmentDefinition>
```

##2. フラグメントのロード
フラグメントをロードする方法は2つあります。

1. XMLビューの中で静的に宣言する
2. コントローラーの中で動的にロードする

###2.1. XMLビューの中で静的に宣言する
フラグメント1は親のビューであるHome Viewに埋め込みます。Home Viewは以下のようになります。

```xml:Home.view.xml
<mvc:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" controllerName="demo.Train_27_Fragment.controller.Home"
	xmlns:html="http://www.w3.org/1999/xhtml">

	<Page id="page" title="{i18n>fragmentSample}">
		<content>
			<Panel headerText="{i18n>mainPart}">
				<Button text="{i18n>showDialog}" press="onShowDialog" class="sapUiTinyMarginEnd"/>
				<Button text="{i18n>submit}" press="onSubmit" />				
			</Panel>			
			<!--フラグメントの埋め込み-->
			<core:Fragment fragmentName="demo.Train_27_Fragment.fragment.Form" type="XML" />
		</content>
	</Page>
</mvc:View>
```
###2.2. コントローラーの中で動的にロードする
フラグメント2、"Show Dialog"ボタンが押されたときに動的にロードします。Home Viewのコントローラーで以下のように書きます。

```javascript:Home.controller.js
sap.ui.define([
	"demo/Train_27_Fragment/controller/BaseController",
	"sap/ui/core/Fragment"
], function (BaseController, Fragment) {
	"use strict";

	return BaseController.extend("demo.Train_27_Fragment.controller.Home", {
		onShowDialog: function () {
			if (!this._oDialog) {
				Fragment.load({
					name: "demo.Train_27_Fragment.fragment.Dialog",
					controller: this
				}).then(function(oDialog) {
					//ダイアログがロードされたら
					this._oDialog = oDialog;
					this._oDialog.open();
				}.bind(this));
			}else {
				this._oDialog.open();
			}
		}
	});

});
```
Fragmentのloadファンクションはフラグメントを非同期にロードします。結果(Promise)が返ってきたらダイアログをオープンします。
また、返ってきたダイアログのインスタンスをコントローラーのプロパティ`this._oDialog`に追加しています。こうすると、次回以降ダイアログが呼ばれたときは`this._oDialog`を開くだけでよくなります。

さて、この時点でアプリを実行して、"Show Dialog"を押してみましょう。すると、No dataと出てきてしまいます。Dialog.fragment.xmlの中でバインドを定義していますが、モデルがセットされていないようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/50f44587-6b5f-8c40-654b-f6f3115f6601.png)

####フラグメントでモデルが使えない？
実は、コントローラーから開いたフラグメントの場合、モデルが自動的には使用可能になりません。
参考：[Using Dialogs Defined as Fragments]
(https://sapui5.hana.ondemand.com/1.75.0/#/topic/aeb86c181b9742a2bf88049abf9ccb95)

対処方法は2つあります。
①ロードした後にフラグメントにモデルをセットする
②ロードした後にフラグメントをビューのdependentに追加する

どちらのやり方も上記のリンク先に載っています。今回は②の方法でやってみます。

```javascript
				}).then(function(oDialog) {
					this._oDialog = oDialog;
					//ダイアログからモデルを使用できるようにする
					this.getView().addDependent(this._oDialog);
					this._oDialog.open();
				}.bind(this));
```

こうすると、データもちゃんと表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3c1bbe1c-d7f8-e1a7-890b-79a5e725372f.png)

##3. フラグメント内のコントロールの取得
コントローラーからフラグメント内のコントロールにアクセスしたい場合があります。
コントロールの取得方法はフラグメントがどのようにロードされたかによって変わってきます。

| フラグメントのID       |フラグメントのロード方法 |コントロールの取得方法    |
|:-----------------|:------------------|:------------------|
| なし             |XMLビューで静的に宣言 |this.byId("controlId")|
| なし             |コントローラーでロード |sap.ui.getCore().byId("controlId")|
| あり             |XMLビューで静的に宣言 |this.byId(sap.ui.core.Fragment.createId("fragmentId", "controlId"))|
| あり             |コントローラーでロード |sap.ui.core.Fragment.byId("fragmentId", "controlId")|
[Retrieving Control Instances by Their ID](https://sapui5.hana.ondemand.com/#/topic/8b325518a2ae48ee91eb2dacda1d1420)をもとに作成

###フラグメントのIDとは
フラグメントをロードする際に、フラグメント自体にIDをつけることができます。これによって、以下のようなid重複を回避できます。

- ビューのコントロールと同じidをフラグメントのコントロールで使用している場合
- ビュー内で同じフラグメントを複数回使用した場合

###3.1. フラグメントにIDを指定しない場合
今回のサンプルではフラグメントにIDを指定していなかったので、まずは指定しない場合の例を見てみましょう。
IDを指定しない場合、静的に宣言したフラグメントのコントロールにはビューのIDがprefixとしてつきます。これは、ビューの中にある通常のコントローラーと同じです。対してコントローラーでロードしたフラグメントにはprefixがつきません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6c72bfea-d297-1b6c-eead-44f5586f9f88.png)

####3.1.1 XMLビューで静的に宣言したフラグメントの場合
フラグメント内のコントロールは、ビューのコントロールと同じように取得することができます。

```javascript
var oControl = this.byId("controlId");
```

例として、"Show Dialog"でサプライヤを選択したときに、Supplier IDとCompany Nameに値を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a00d35bb-8a2f-d65d-47aa-73e82a15f846.png)


```javascript
		onItemPress: function (oEvent) {
			//選択されたサプライヤの情報を取得
			var sSupplierId = oEvent.getSource().getBindingContext().getProperty("SupplierID");
			var sCompanyName = oEvent.getSource().getBindingContext().getProperty("CompanyName");
			
			//フラグメントのコントロールにアクセス
			var oSupplier = this.byId("supplierId");
			var oCompanyName = this.byId("companyName");

			//値をセット
			oSupplier.setValue(sSupplierId);
			oCompanyName.setValue(sCompanyName);

			this._oDialog.close();
		}
```

####3.1.2. コントローラーでロードしたフラグメントの場合
この場合、`this.byId("controlId")`でアクセスすることはできません。代わりに、以下の方法でアクセスします。

```javascript
var oControl = sap.ui.getCore().byId("controlId");
```

例として、"Show Dialog"でサプライヤを選択したときに、サプライヤにひもづく製品のリストを表示させます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8498bdf1-4ff5-0570-1a8f-8a64c1abb643.png)

```javascript
		onItemPress: function (oEvent) {
			//...			
			//製品のリストの表示
			this._showProducts(sSupplierId);
			this._oDialog.close();
		},

		_showProducts: function (sSupplierId) {
			//テーブルのフラグメントをロード
			if (!this._oTablePanel) {
				Fragment.load({
					name: "demo.Train_27_Fragment.fragment.Table",
					controller: this
				}).then(function(oPanel) {
					this._oTablePanel = oPanel;
					this._oPage.addContent(this._oTablePanel);
					//バインドの設定
					this._bindTable(sSupplierId);
				}.bind(this));
			}else {
				this._bindTable(sSupplierId);
			}
		},
		
		_bindTable: function (sSupplierId) {
			var sPath = "/Suppliers(" + sSupplierId + ")/Products";
			if (!this._oTemplate) {
				this._oTemplate = new ColumnListItem({
					cells: [
						new Text({
							text: "{ProductID}"
						}),
						new Text({
							text: "{ProductName}"
						}),
						new Text({
							text: "{CategoryID}"
						})					
					]
				});				
			}
			//フラグメント内のコントロールにアクセス
			var oTable = sap.ui.getCore().byId("productTable");
			oTable.bindItems({
				path: sPath,
				template: this._oTemplate
			});
		}
	});
```

```xml:Table.fragment.xml
<core:FragmentDefinition
	xmlns="sap.m"
	xmlns:core="sap.ui.core">
	<Panel xmlns="sap.m" headerText="{i18n>panelTitle2}">
		<Table id="productTable">
			<columns>
				<Column>
					<Text text="{i18n>productID}"/>
				</Column>
				<Column>
					<Text text="{i18n>productName}"/>
				</Column>	
				<Column>
					<Text text="{i18n>categoryID}"/>
				</Column>				
			</columns>
		</Table>		
	</Panel>
</core:FragmentDefinition>
```

`_bindTable`でテーブルにデータをバインドしています。サプライヤが選択される都度、ひもづく製品のパスをテーブルにバインドするため、フラグメント内にあるTableコントロールにアクセスする必要があります。
Tableコントロールには`this.byId("productTable")`でアクセスすることができません。代わりに、`sap.ui.getCore().byId("productTable")`でアクセスします。

###3.2. フラグメントにIDを指定した場合
次に、フォームとテーブルのフラグメントにそれぞれIDを指定してみます。

**XMLビューで静的に宣言**

```xml:Home.view.xml
	<Page id="page" title="{i18n>fragmentSample}">
		<content>
			<!--省略-->			
			<!--フラグメントの埋め込み（ID指定）-->
			<core:Fragment id="formFragment" fragmentName="demo.Train_27_Fragment.fragment.Form" type="XML" />
		</content>
	</Page>
```
**コントローラーでロード**

```javascript
		_showProducts: function (sSupplierId) {
			//テーブルのフラグメントをロード
			if (!this._oTablePanel) {
				Fragment.load({
					name: "demo.Train_27_Fragment.fragment.Table",
					controller: this,
					id: "tableFragment" //ID指定
				}).then(function(oPanel) {
					this._oTablePanel = oPanel;
					this._oPage.addContent(this._oTablePanel);
					//バインドの設定
					this._bindTable(sSupplierId);
				}.bind(this));
			}else {
				this._bindTable(sSupplierId);
			}
		}
```
IDを指定した場合、静的に宣言したフラグメントのコントロールには**ビューのID + フラグメントのID**がprefixとしてつきます。コントローラーでロードしたフラグメントにはフラグメントのIDがprefixとしてつきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9d2c20a4-d078-29df-4540-3f15ece8b278.png)

####3.2.1 XMLビューで静的に宣言したフラグメントの場合
FragmentのcreateIdというメソッドを使って生成したIDを、this.byId()に渡します。

```javascript
var oControl = this.byId(sap.ui.core.Fragment.createId("fragmentId", "controlId"));
```

onItemPressでコントロールを取得する処理は以下のように変わります。

```javascript
			// var oSupplier = this.byId("supplierId");
			// var oCompanyName = this.byId("companyName");
			var oSupplier = this.byId(sap.ui.core.Fragment.createId("formFragment", "supplierId"));
			var oCompanyName = this.byId(sap.ui.core.Fragment.createId("formFragment", "companyName"));
```

####3.2.2. コントローラーでロードしたフラグメントの場合
FragmentのbyIdというメソッドを使って取得します。

```javascript
var oControl = sap.ui.core.Fragment.byId("fragmentId", "controlId");
```

_bindTableでTableコントロールを取得する処理は以下のように変わります。

```javascript
			//var oTable = sap.ui.getCore().byId("productTable");
			var oTable = sap.ui.core.Fragment.byId("tableFragment", "productTable");
```

##4. フラグメントのコントローラー
フラグメントのコントローラーは、ビューのコントローラーそのものを使うのが一般的です。しかし、ビューのコントローラーに色々詰め込みたくない場合や、フラグメントのコントローラーを再利用したい場合は、独自のコントローラーを渡すこともできます。

フラグメントに独自のコントローラーを渡す方法については、以下の記事をご参照ください。
[【SAPUI5】コントローラーを短くするには](https://qiita.com/tami/items/c9ca525ab8d6a95b3b4f#3-%E3%83%95%E3%83%A9%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%88%E3%81%AB%E7%8B%AC%E8%87%AA%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B)

##5. フラグメントのロード処理を共通化する
一つのビューで複数のフラグメントをロードする場合、フラグメントのロード処理を共通化すると便利です。複数のビューでその処理を使い回せるようにBaseControllerでロード処理を定義しておくとよいでしょう。

フラグメントの名称（例："Dialog"）をキーとした配列を作成しておいて、フラグメントをロードしたら配列にフラグメントを格納します。次回以降ロードしたい場合は、配列の中からロード済のフラグメントを取得して返します。

```javascript:BaseController.js
sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/ui/core/Fragment"
], function (Controller, Fragment) {
	"use strict";

	return Controller.extend("demo.Train_27_Fragment.controller.BaseController", {
		
		//フラグメントをロード
		loadFragment: function (sFragmentName, oController, sId) {
			if (!this._aFragments) {
				this._aFragments = {};
			}
			
			return new Promise(function(resolve, reject) {
				if (!this._aFragments[sFragmentName]) {
					var sName = this.getOwnerComponent().getMetadata().getComponentName() + ".fragment." + sFragmentName;
					Fragment.load({
						id: sId,
						name: sName,
						controller: oController
					}).then(function(oFragment) {
						this._aFragments[sFragmentName] = oFragment;
						this.getView().addDependent(oFragment);
						resolve(this._aFragments[sFragmentName]);
					}.bind(this));				
				}else {
					resolve(this._aFragments[sFragmentName]);
				}
			}.bind(this));
		},
		
		//ロードされていることがわかっているフラグメントを取得するとき
		getFragment: function (sFragmentName) {
			return this._aFragments[sFragmentName];
		}

	});
});
```

コントローラーから利用する場合は以下のようになります。

```javascript
sap.ui.define([
	"demo/Train_27_Fragment/controller/BaseController", //BaseControllerに
置き換え
], function (BaseController) {
	"use strict";

	return BaseController.extend("demo.Train_27_Fragment.controller.Home", {

		onShowDialog: function () {
			this.loadFragment("Dialog", this).then(function(oDialog) {
				oDialog.open();
			});                      
		}
```