【Fiori】Flexible Column Layoutを一から実装する（後編）

##1. 目的
[前編](https://qiita.com/tami/items/fadb5064ead06fbd7872)に続き、Flexible Column Layoutの実装をやっていきます。
今回は、**Semantic Helper**の役割と使い方について理解を深めたいと思います。

- Semantic Helperはどうやって実装したらいいか
- Semantic Helperと、Component、ベースとなるビュー、表示するビューにはどのような関わりがあるのか

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/436f35af-b73f-fe41-7929-1ff320fc7dc1.png)

##2. 全体像
はじめに、全体像を説明します。Semantic Helperと、コンポーネントで定義するモデルという2つのオブジェクトが重要な役割を果たします。

###2-1. Semantic Helper
Semantic Helperは以下の役割を持ちます。

- ナビゲーションによって別のビューに移動するとき、どのレイアウトを使えばいいか提案してくれる
- 最大化/元に戻す/閉じるなどのアクションボタンの表示制御をしてくれる

###2-2. コンポーネントのモデル
このモデルは、Semantic Helperが返してくれる現在のUIステータスに関する情報を持ちます。コンポーネントにモデルを置くのは、すべてのビューからアクセスできるようにするためです。
以下のようなパラメータが格納されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6fb0a57a-2538-933e-0d83-dfcfec6ce20a.png)
このうち、layoutはFlexibleColumnLayoutのlayoutプロパティにバインドされます。
また、actionButtonsInfoはボタンの可視性と、ボタンが押された後にどのレイアウトに遷移するかという情報を持っています。


###2-3. 要（かなめ）はAppビュー
Routingやボタン押下に伴うレイアウトをコントロールする上で、要となるのはAppビューです。AppビューはFlexibleColumnLayoutが乗っている土台で、Routingによってレイアウトが変わったり、手でカラムの幅を変えたりしたときのイベントをキャッチすることができます。このとき、AppビューはSemantic Helperを呼んで現在のUIステータスを取得し、コンポーネントのモデルに記録しておきます。
このおかげで、Detailビューでは現在のUIステータスに応じてボタンを表示したり、ボタン押下後に表示すべきレイアウトを決定することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/55ae764a-f2e8-ef25-2dc4-613de24c5630.png)

##3. 実装
###3-1. Component
Componentで行う重要なことは以下の2つです。

- UIステータスを保持するためのモデルを登録 (init)
- Semantic Helperを返すためのファンクションを定義 (getHelper)

```javascript:Component.js
sap.ui.define([
	"sap/ui/core/UIComponent",
	"sap/ui/Device",
	"sap/ui/model/json/JSONModel",
	"demo/Train_23_FlexibleColumnLayout/model/models",
	"sap/f/FlexibleColumnLayoutSemanticHelper",
	"sap/f/library"
], function (UIComponent, Device, JSONModel, models, FlexibleColumnLayoutSemanticHelper, fioriLibrary) {
	"use strict";

	return UIComponent.extend("demo.Train_23_FlexibleColumnLayout.Component", {

		metadata: {
			manifest: "json"
		},

		/**
		 * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
		 * @public
		 * @override
		 */
		init: function () {
			//UIステータスを保持するためのモデルを登録
			var oModel = new JSONModel();
			this.setModel(oModel);
			
			// call the base component's init function
			UIComponent.prototype.init.apply(this, arguments);

			// enable routing
			var oRouter = this.getRouter();
			oRouter.attachBeforeRouteMatched(this._onBeforeRouteMatched, this);
			oRouter.initialize();

			// set the device model
			this.setModel(models.createDeviceModel(), "device");
		},
		
		_onBeforeRouteMatched: function(oEvent) {
			var oModel = this.getModel(),
				sLayout = oEvent.getParameters().arguments.layout,
				//遷移先で使用するlayoutを提案
				//0: masterのみ 1: master-detail 2: master-detail-detail 3~: さらに次のビュー
				oNextUIState; 
				
			//Routingで"layout"パラメータが渡されなかったら、デフォルトレベル(0)を設定する
			if(!sLayout) {
				//sLayout = fioriLibrary.LayoutType.OneColumn;
				this.getHelper().then(function(oHelper) {
					oNextUIState = oHelper.getNextUIState(0);	
					oModel.setProperty("/layout", oNextUIState.layout);
				});
				return;
			}
			oModel.setProperty("/layout", sLayout);
		},
		
		getHelper: function () {
			return this._getFcl().then(function(oFCL) {
				var oSettings = {
					defaultTwoColumnLayoutType: fioriLibrary.LayoutType.TwoColumnsMidExpanded
					//defaultThreeColumnLayoutType: fioriLibrary.LayoutType.ThreeColumnsMidExpanded 3カラムのとき
				};
				//FCLコントロールと上の設定を渡す
				return (FlexibleColumnLayoutSemanticHelper.getInstanceFor(oFCL, oSettings));
			});
		},
		
		_getFcl: function () {
			return new Promise(function(resolve, reject) {
				var oFCL = this.getRootControl().byId("fcl");
				//まだできていなかったら、できてから返してもらう
				if (!oFCL) {
					this.getRootControl().attachAfterInit(function(oEvent) {
						resolve(oEvent.getSource().byId("fcl"));
					}, this);
					return;
				}
				resolve(oFCL);
			}.bind(this));
		}
	});
});
```

###3-2. App
Appビューでは、ルートや画面のカラム数が変わったときにSemantic Helperを呼んで現在のUIステータスを取得し、Componentのモデルにセットします。

####ビュー
stateChangeイベントは、layoutプロパティが変わったり、ブラウザのサイズ変更により画面に表示できるカラム数が変わったときに発火します。

```xml:App.view.xml
<mvc:View controllerName="demo.Train_23_FlexibleColumnLayout.controller.App" xmlns:mvc="sap.ui.core.mvc" 
	xmlns:f="sap.f" xmlns="sap.m"
	displayBlock="true">
	<App id="app">
		<f:FlexibleColumnLayout id="fcl" 
			layout="{/layout}"
			stateChange="onStateChanged">
		</f:FlexibleColumnLayout>
	</App>
</mvc:View>
```
####コントローラー

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

	return Controller.extend("demo.Train_23_FlexibleColumnLayout.controller.App", {
		onInit: function () {
			this.oOwnerComponent = this.getOwnerComponent();
			this.oRouter = this.oOwnerComponent.getRouter();
			
			//ルートが変わったことを監視
			this.oRouter.attachRouteMatched(this.onRouteMatched, this);
		},
		
                
		//ルートが変わったときに発生するイベント
		onRouteMatched: function (oEvent) {			
			this._updateUIElements();
		},
		
		//layoutや画面サイズが変わったときに発生するイベント
		onStateChanged: function (oEvent) {			
			this._updateUIElements();
		},
		
		_updateUIElements: function () {
			var oModel = this.oOwnerComponent.getModel(),
				oUIState;
				
			this.oOwnerComponent.getHelper().then(function(oHelper) {
				//現在のUIステータスを取得してモデルに保存
				oUIState = oHelper.getCurrentUIState();
				oModel.setData(oUIState);
			});
		}
	});
});
```

###3-3. Master
Masterビューでは、ナビゲーションの際にSemantic Helperを呼び、次のUIステータスを取得します。UIステータスが持っているlayoutを、ナビゲーションのパラメータに設定します。

```javascript:Master.controller.js
sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/f/library"
], function (Controller, fioriLibrary) {
	"use strict";

	return Controller.extend("demo.Train_23_FlexibleColumnLayout.controller.Master", {


		onInit: function () {
			this.oRouter = this.getOwnerComponent().getRouter();
		},
		
		onItemPress: function (oEvent) {			
			var sPath = oEvent.getSource().getBindingContext("products").getPath(), //"/Products/2"
				sProduct = sPath.split("/").slice(-1).pop(), //2
				oNextUIState;
				
			this.getOwnerComponent().getHelper().then(function (oHelper) {
				oNextUIState = oHelper.getNextUIState(1);
				this.oRouter.navTo("detail", {
					layout: oNextUIState.layout,
					productId: sProduct
				});
			}.bind(this));
		}

	});

});
```
`getNextUIState(0)`の数字の数字の部分を変えて呼んでみると、以下の結果が返ってきます。layoutの値は結構長いので、「次のレイアウトは何だっけ？」ということを意識しなくて済むのは助かりますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c8b03414-6493-b9f8-ab02-5149c12d6462.png)

###3-4. Detail
####ビュー
Detailビューでは、ボタンの可視性をUIステータスのactionButtonsInfoプロパティによって制御しています。レイアウトの状態が変わるたび、AppコントローラーでモデルにUIステータスをセットしていました。その値をここで利用しているわけです。

```xml:Detail.view.xml
				<f:navigationActions>
					<OverflowToolbarButton type="Transparent" icon="sap-icon://full-screen" 
						tooltip="Full Screen" press="onFullScreen"  
						visible="{= ${/actionButtonsInfo/midColumn/fullScreen} !== null }" />
					<OverflowToolbarButton type="Transparent" icon="sap-icon://exit-full-screen" 
						tooltip="Exit Full Screen Mode" press="onExitFullScreen" 
						visible="{= ${/actionButtonsInfo/midColumn/exitFullScreen} !== null }" />
					<OverflowToolbarButton type="Transparent" icon="sap-icon://decline" 
						tooltip="Close" press="onClose"  
						visible="{= ${/actionButtonsInfo/midColumn/closeColumn} !== null }" />					
				</f:navigationActions>
```
以下は、MasterとDetailビューが両方開かれた状態でのUIステータスを表しています。actionButtonsInfoのmidColumn/fullScreen, midColumn/closeColumnの値が入っているので、「最大化」と「閉じる」ボタンが表示された状態なります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/68deb4f5-e621-6136-30e9-546bf7673a45.png)

####コントローラー
最大化などのボタンが押されたとき、[前回](https://qiita.com/tami/items/fadb5064ead06fbd7872#%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC-2)はlayoutプロパティを直接指定していました。今回はactionButtonsInfoにボタンが押されたあとに設定すべきレイアウトの値を持っているので、それをRoutingのパラメータに指定しています。

```javascript:Detail.controller.js
sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/f/library",
	"sap/ui/model/json/JSONModel"
], function (Controller, fioriLibrary, JSONModel) {
	"use strict";

	return Controller.extend("demo.Train_23_FlexibleColumnLayout.controller.Detail", {

		onInit: function () {
			var oModel = new JSONModel({
				fullScreen: true,
				exitFullScreen: false,
				close: true
			});
			this.getView().setModel(oModel, "buttons");
			
			this.oOwnerComponent = this.getOwnerComponent();
			this.oRouter = this.oOwnerComponent.getRouter();
			this.oModel = this.oOwnerComponent.getModel(); //layoutを持っているモデル
			
			this.oRouter.getRoute("detail").attachPatternMatched(this._onProductMatched, this);
		},
		
		onFullScreen: function () {
			var sNextLayout = this.oModel.getProperty("/actionButtonsInfo/midColumn/fullScreen");
			this.oRouter.navTo("detail", {layout: sNextLayout, productId: this._product});
		},
		
		onExitFullScreen: function () {
   			var sNextLayout = this.oModel.getProperty("/actionButtonsInfo/midColumn/exitFullScreen");
			this.oRouter.navTo("detail", {layout: sNextLayout, productId: this._product});
		},
		
		onClose: function () {
   			var sNextLayout = this.oModel.getProperty("/actionButtonsInfo/midColumn/closeColumn");
			this.oRouter.navTo("master", {layout: sNextLayout, productId: this._product});
		},
		
		_onProductMatched: function (oEvent) {
			this._product = oEvent.getParameter("arguments").productId;
			this.getView().bindElement({
				path: "/Products/" + this._product,
				model: "products"
			});
		}
	});
});
```
ところで、前回はRouterを使わずにlayoutプロパティを直接変更していました。今回はRouterを使わないとだめなのでしょうか？
結論から言うと、Routerを使わないとだめでした。Routerを使わないとAppコントローラーのRouteMatchedイベントが動かず、UIステータスが更新されません。結果、ボタンの表示があるべき状態になりませんでした。

##4. Semantic Helperの使い方まとめ

- Semantic Helperは、現在、または指定したビューレベルにおけるUIステータスを返す
- UIステータスを使ってボタンの制御や次画面で使用するレイアウトを取得することができる
- UIステータスはComponentに置いて、各ビューからアクセスできるようにする