【Fiori】Object Pageにコンポーネントを埋め込む方法

##はじめに
以下のSAPブログに、Fiori elementsアプリを拡張するさまざまな方法が紹介されていました。その中でReuse Componentsというものがあるのを知ったので、実装してみようと思いました。
[Extending SAP Fiori elements Applications – What you need to know](https://blogs.sap.com/2020/06/12/extending-sap-fiori-elements-applications-what-you-need-to-know/)
[ドキュメント](https://sapui5.hana.ondemand.com/#/topic/d869d7ab3caa48b2a20dc20dfa248380)

##この記事で紹介すること
1. コンポーネントを作ってObject Pageに埋め込む
2. コンポーネントの表示位置を変更する
3. Object Pageのデータ変更に反応する

##1. コンポーネントを作ってObject Pageに埋め込む
Fiori elementsに埋め込むためのコンポーネントは、通常のコンポーネントとは作りが変わります。よって通常のコンポーネントと共有することはできません。

###ライブラリの作成
コンポーネント用のライブラリを用意します。comp1が埋め込むコンポーネントです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4ff54d6a-15b7-e9e3-a622-9f78977e2180.png)

ライブラリのmanifest.jsonにコンポーネントの情報を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ebf4fe0b-60c5-312b-e1ec-eeddb7bc8eff.png)

###コンポーネントの作成
以下はcomp1の中身です。コンポーネント側ではObject Pageのコンテキストがそのまま使用可能なので、manifest.jsonではモデルを定義していません。ここでデフォルトモデルを定義した場合、Object Pageのモデルと衝突して思った通りにデータが表示されません。コンポーネント独自のモデル定義が必要な場合は、名前つきモデルにします。

```json:manifest.json
{
	"_version": "1.8.0",
	"sap.app": {
		"id": "demo.testcomponent.comp1",
		"type": "component",
		"embeddedBy": "../",
		"i18n": "i18n/i18n.properties",
		"applicationVersion": {
			"version": "1.0.0"
		},
		"title": "{{compTitle}}",
		"description": "{{compDescription}}"
	},
	"sap.ui": {
		"technology": "UI5",
		"icons": {
			"icon": "",
			"favIcon": "",
			"phone": "",
			"phone@2": "",
			"tablet": "",
			"tablet@2": ""
		},
		"deviceTypes": {
			"desktop": true,
			"tablet": true,
			"phone": true
		},
		"supportedThemes": [
			"sap_fiori_3"
		]
	},
	"sap.ui5": {
		"resources": {
			"js": [],
			"css": []
		},
		"dependencies": {
			"minUI5Version": "1.65.6",
			"libs": {
				"sap.ui.core": {
					"lazy": false
				},
				"sap.m": {}
			},
			"components": {}
		},
		"models": {
			"i18n": {
				"type": "sap.ui.model.resource.ResourceModel",
				"uri": "i18n/i18n.properties"
			}
		},
		"contentDensities": {
			"compact": true,
			"cozy": true
		}
	}
}
```
画面は簡単なFormとします。

```xml:myComponent.fragment.xml
<core:FragmentDefinition
	xmlns="sap.m"
	xmlns:smartForm="sap.ui.comp.smartform"
	xmlns:smartField="sap.ui.comp.smartfield"	
	xmlns:core="sap.ui.core">
	
		<smartForm:SmartForm id="smartForm" title="{i18n>Title}" entityType="Z_C_SO_MOB67Type">
			<smartForm:Group label="{i18n>Group1}">
				<smartForm:GroupElement>
					<smartField:SmartField value="{CreatedAt}" />
				</smartForm:GroupElement>
				
				<smartForm:GroupElement>
					<smartField:SmartField value="{CreatedBy}" />
				</smartForm:GroupElement>				
			</smartForm:Group>
		</smartForm:SmartForm>
	
</core:FragmentDefinition>
```

Fiori elements用のコンポーネントと通常のコンポーネントとの違いは、Component.jsの実装部分です。

```javascript:Component.js
sap.ui.define([
	"sap/ui/core/UIComponent",
	"sap/suite/ui/generic/template/extensionAPI/ReuseComponentSupport"
], function(UIComponent, ReuseComponentSupport) {
	"use strict";
	
	return UIComponent.extend("demo.testcomponent.comp1.Component", {
		metadata: {
			manifest: "json",
			properties: {
				/* Standard properties for reuse components */
				uiMode: {
					type: "string",
					group: "standard"
				},
				semsnticObject: {
					type: "string",
					group: "standard"
				},
				stIsAreaVisible: {
					type: "boolean",
					group: "standard"
				}
		},
		
		// Standard life time event of a component. Used to transform this component into a reuse component for Fiori Elements
		init: function () {
			//Transform this component into a reuse component for Fiori Elements:
			ReuseComponentSupport.mixInto(this, "compModel");
			// Defensive call of init of the super class:
			(UIComponent.prototype.init || jQuery.noop).apply(this, arguments);
			
		},
	
});
```
Elements用のコンポーネントには以下の特徴があります。

**・Object Pageの情報を表すパラメータを持っている**

| パラメータ       |説明 | 
|:-----------------|:------------------|
| uiMode             |Object Pageの画面モード。"Create", "Display", または "Edit"が設定される |
| semanticObject             |Object Pageに表示されているセマンティックオブジェクト |
| stIsAreaVisible            |Reuse Componentが画面に表示されているか（true: 表示されている） |

**・initメソッドの中でElements用のコンポーネントに変換する**
以下のメソッドで、UIComponentがElements用のコンポーネントに変換されます。
`ReuseComponentSupport.mixInto(this, "<プロパティモデルの名前>"); `
<プロパティモデルの名前>を渡すとuiModeなどのパラメータがJSONモデルに格納され、コンポーネントのビューから使用できるようになります。 （参考：[APIリファレンス](https://sapui5.hana.ondemand.com/#/api/sap.suite.ui.generic.template.extensionAPI.ReuseComponentSupport%23methods/sap.suite.ui.generic.template.extensionAPI.ReuseComponentSupport.mixInto)）

Component.jsの残りの部分は以下のようにしました。シンプルにフラグメントを取得して表示します。

```javascript
		
		createContent: function () {
			//Return a fragment
			var oFragment = this._getFragment();
			return oFragment;
		},
		
		_getFragment: function () {
			if (!this._oFragment) {
				this._oFragment = sap.ui.xmlfragment(this.getId(), "demo.testcomponent.comp1.fragment.myComponent", this);
			}
			return this._oFragment;
		}
		
	});
```

###Object Pageに埋め込み
Object Pageがあるアプリのmanifest.jsonにライブラリ、およびコンポーネント使用の情報を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9718d79d-b88a-7cc1-5db2-f8b95be15d18.png)

続いて、pagesセクションに埋め込むコンポーネントの情報を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5d6208cf-7e48-e323-1a9e-49f33807afb5.png)

###結果
コンポーネントが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7025bc11-8a18-c6c2-012d-9d7146d23c50.png)


##2. コンポーネントの表示位置を変更する
デフォルトではコンポーネントはObject Pageの最後のセクションとして追加されます。ここでは、以下の方法について見ていきます。

- 既存セクションのサブセクションとして追加
- 任意の位置にセクションとして追加

###既存セクションのサブセクションとして追加
Order Infromationセクションのサブセクションとしてコンポーネントを追加してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b3c9fd51-a17c-cf22-9529-f902eba927a3.png)

UI.Facetsアノテーションの中からターゲットとなるUI.CollectionFacetのIDを探します。`<PropertyValue Property="ID" String="HeaderFacet"/>`より、"HeaderFacet"がIDです。

```xml
				<Annotation Term="UI.Facets">
					<Collection>
						<Record Type="UI.CollectionFacet"><PropertyValue Property="Label" String="Order Information"/><PropertyValue Property="ID" String="HeaderFacet"/>
							<PropertyValue Property="Facets">
								<Collection>
									<Record Type="UI.ReferenceFacet"><PropertyValue Property="Label" String="Basic Information"/><PropertyValue Property="ID" String="BasicGroup"/><PropertyValue Property="Target" AnnotationPath="@UI.FieldGroup#BasicFieldGroup"/></Record>
									<Record Type="UI.ReferenceFacet"><PropertyValue Property="Label" String="Order Status"/><PropertyValue Property="ID" String="StatusGroup"/><PropertyValue Property="Target" AnnotationPath="@UI.FieldGroup#StatusFieldGroup"/></Record>
								</Collection>
							</PropertyValue>
						</Record>
						<Record Type="UI.ReferenceFacet"><PropertyValue Property="Label" String="Item"/><PropertyValue Property="ID" String="ItemFacet"/><PropertyValue Property="Target" AnnotationPath="to_Item/@UI.LineItem"/></Record>
					</Collection>
				</Annotation>
```

manifest.jsonの"leadingSectionIdOrPath"に、上で確認したIDを指定します。（CollectionFacetの場合はID、ReferenceFacetの場合はアノテーションパスを指定します）

```json
						"embeddedComponents": {
							"myFirstComponentEmbeding": {
								"id": "myFirstComponentEmbeding",
								"componentUsage": "myReuseComponent",
								"title": "{{myFirstComponentTitle}}",
								"leadingSectionIdOrPath": "HeaderFacet"
							}
						}
```

結果は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3fe63382-a1b9-ff24-380f-84aa5526f635.png)

###任意の位置にセクションとして追加
VisualEditorを使うと、セクションを任意の位置に移動することができます。

事前にプロジェクトの設定でVisualEditorで使うRun Configurationを設定しておきます。これがないとコンポーネントのライブラリを読み込むことができません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2c3d97eb-1d48-1550-3237-b5fb0ece4289.png)

VisualEditorを編集モードにしたら、コンポーネントのセクションをつかんで任意の場所にドラッグ&ドロップします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/046a2739-1eca-b970-513e-b71d3b678e00.png)
Order Informationの下に移動しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9777ee9e-8572-9dfd-7727-e51101a14881.png)
結果は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8162ac20-21d5-23be-8cce-183f2628fd55.png)

##3. Object Pageのデータ変更に反応する
Object Pageのデータが変更されたときにコンポーネントのデータをリフレッシュする方法について紹介します。

###コンポーネント側の設定
コンポーネント側ではstRefreshというメソッドを定義します。このメソッドはObject Pageがロードされたときや、Object Pageでの変更が保存されたときに呼ばれます。

```javascript:Component.js
		init: function () {
			//...
		},
		
        stRefresh: function(oModel, oBindingContext, oExtensionAPI) {
		    this.oModel = oModel;
		    this.oBindingContext = oBindingContext;
		    this.oExtensionAPI = oExtensionAPI;
		    this._doRefresh();
        },
        
        _doRefresh: function () {
        	this.getModel("localModel").setProperty("/lastRefreshed", new Date());	
        },
```

画面に最終リフレッシュ時刻を表示するようにします。（諸般の事情によりSmartFormからSimpleFormに変更しました）

```xml
	<form:SimpleForm id="simpleForm">
		<Label text="Created At" labelFor="createdAt"/>
		<Text id="createdAt" text="{
			path: 'CreatedAt',
			type: 'sap.ui.model.type.DateTime',
			formatOptions: {
				style: 'medium'
			}}"/>
		<Label text="Created by" labelFor="createdBy"/>
		<Text id="createdBy" text="{CreatedBy}"/>
		<Label text="Delivery Status" labelFor="deliveryStatus"/>
		
		<!--最終リフレッシュ時刻を表示-->
		<Label text="Last Refreshed" labelFor="lastRefreshed" />
		<Text id="lastRefreshed" 
			text="{
				path: 'localModel>/lastRefreshed',
				type: 'sap.ui.model.type.DateTime',
				formatOptions: {
					style: 'medium'
			}}" />			
	</form:SimpleForm>
```

画面には以下のように表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/80424e5c-9959-0557-2048-67b56584ef07.png)

###Object Page側の設定
Object Page側でデータを編集中にリアルタイムにコンポーネントをリフレッシュしたい場合、以下の設定が必要になります。

※リアルタイムなリフレッシュが不要な場合、コンポーネントのstRefreshの実装だけでよいです
※Object Pageと共有しているコンテキストはリアルタイムにコンポーネントに反映されるので、特に何もする必要はありません

ここでは、Delivery Statusを変更したらコンポーネントをリフレッシュするようにします。manifest.jsonにstRefreshTriggerというプロパティを追加します。

```json
						"embeddedComponents": {
							"myFirstComponentEmbeding": {
								"id": "myFirstComponentEmbeding",
								"componentUsage": "myReuseComponent",
								"title": "{{myFirstComponentTitle}}",
								"settings": {
									"stRefreshTrigger": "{DeliveryStatus}"
								}
							}
						}
```

・複数の項目のうちいずれかが変更された場合にリフレッシュするとき
`"{Price}{Supplier}"`
・複数の項目が変更された場合にリフレッシュするとき
` "{= ${Price}+${Supplier}}"`

上記の設定により、保存ボタンを押す前にもコンポーネントのリフレッシュが呼ばれるようになります。

##使い道
Reuse Componentを使うことで、Fiori elements単体ではできないことも実現できるようになります。そのよい例が添付ファイルです。以下のブログに標準のファイル添付コンポーネントをFiori Elementsアプリに埋め込む方法が紹介されています。
[Attachment Service to your rescue in S4HANA via Fiori Elements using Reuse Components (GOS)](https://blogs.sap.com/2019/12/16/attachment-service-to-your-rescue-in-s4hana-fiori-elements-using-reuse-components-gos-dms/)