【SAPUI5】ComponentベースのRoutingの進化について

##はじめに
ComponentベースのRoutingとは、Routingのターゲットにビューではなくコンポーネントを指定することです。これによってプロジェクトとは独立したコンポーネントを、あたかもプロジェクトの中にあるビューのように扱うことができます。

最近のSAP Blogを見ていて、ComponentベースのRoutingがすごく進化していると感じました。興味深かったのは、執筆当時のUI5バージョンでできることには限界があるものの、それぞれの著者が工夫して解決策を生み出している点です。プロジェクトによっては最新のUI5バージョンが使えないこともあるので、これらの工夫について知っておくことは価値があると思います。
この記事では、ComponentベースのRoutingに関する記事を見ながらどんな進歩があったのかについて見ていきたいと思います。

##参考にしたブログ
タイトル / 作者 / 公開日 / **UI5バージョン**

1. [UI5 – Nested routing with reusable components](https://blogs.sap.com/2019/08/23/ui5-nested-routing-with-reusable-components/) / Tom Van Doorslaer / 2019.8.23 / **1.60**
2. [UI5 – Component Based Routing](https://blogs.sap.com/2019/10/09/ui5-component-based-routing/) / 
Graham Robinson / 2019.10.9 / **1.68**
3. [UI5 – Navigate with Nested Components](https://blogs.sap.com/2019/12/16/ui5-navigate-with-nested-components/) / Graham Robinson / 2019.12.16 / **1.72**
4. [UI5er Buzz #46 – Routing with Nested Components](https://blogs.sap.com/2020/02/05/ui5er-buzz-46-routing-with-nested-components/) / 
Jiawei Cao / 2020.2.5 / **1.74**
5. [UI5ers Buzz #48: Consuming Title Changes of Nested Components](https://blogs.sap.com/2020/03/23/ui5ers-buzz-48-consuming-title-changes-of-nested-components/) / Florian Vogt / 2020.3.23 / **1.75**

##Routingの進歩

###～Version 1.58
Routingのターゲットにコンポーネントを指定することはできなかった。
（プロジェクト内のビューにコンポーネントを埋め込み、そのビューをターゲットにしていたと想像）

###Version 1.60
Routingのターゲットにコンポーネントを指定することができるようになった。
親と子のコンポーネントは別のRouting設定を持つことになり、それぞれがブラウザのハッシュを変更しようとしてしまう。これを避けるため、[ドキュメント](https://ui5.sap.com/1.60.8/#/topic/2c66fb3eb775426087401117cfa6a94e)では以下のように、子のRouterで親を指定することが推奨されていた。

```javascript
sap.ui.core.UIComponent.extend("Child", {
   metadata : {
     routing: {
       routes: [
         {
           pattern: "members/{id}",
           name: "members",
           parent: "Father:teams"
         }
       ]
     }
   }
 });
```

####問題点
子に対して親が固定されてしまい、子のコンポーネントを使い回せない

####[UI5 – Nested routing with reusable components](https://blogs.sap.com/2019/08/23/ui5-nested-routing-with-reusable-components/)での解決策
**Routeの定義にワイルドカード `:xxx:`を使う**
以下は子のコンポーネントでのRoutingの定義。`:parent*:`の部分にはparent/で始まるハッシュ、または任意のハッシュを入れられる（※）。これによって、親のコンポーネントが固定されることはなくなる。
※参考：[Routing and Navigation>"rest as string" parameter](https://sapui5.hana.ondemand.com/#/topic/3d18f20bd2294228acb6910d8e8a5fb5)

```json
{
  "pattern": ":parent*:&/list",
  "name": "List",
  "target": ["List"]
},{
  "pattern": ":parent*:&/Detail/{ResId}",
  "name": "Detail",
  "target": ["Detail"]
},{
  "pattern": ":parent*:",
  "name": "home",
  "target": ["List"]
}
```
これで親子固定の問題は回避できましたが、次の問題点が出てきました。

子のコンポーネント内でナビゲーションしたときにブラウザのハッシュが子のコンポーネントのものに置き換わる。この結果、親コンポーネントはデフォルトのルートを表示してしまう。

**解決策：独自のnavToメソッドを定義し、親と子のパラメータを全て渡す**
（前提）親と子で共通のBaseControllerを使用している

- BaseControllerでrouteMatchedのイベントハンドラを定義し、そこでRoutingで渡されたパラメータを取得する。
- BaseControllerで独自のnavToメソッドを定義する。このメソッドでは、Routing用に渡されたパラメータと、routeMatchedのタイミングで取得したパラメータを合わせて本来のRouterに渡す。こうすると、親のRoutingで使用していたパラメータも失われない。

**追記**
2020年2月のUI5Conの動画で、1.60での実装について解説されていました。
[Lessons learned of working with components and composite controls - Robin Pannee](https://youtu.be/wF-_Aqq9g9k?t=974)

###Version 1.63
親のRouterの定義で、親子の階層を表現できるようになった。子のルートにprefixをつけることで、URLのハッシュのうちどの部分が子のコンポーネントに属するのかを区別する。⇒[ドキュメント](https://ui5.sap.com/1.63.0/#/topic/fb19f501b16e4e4991eb6a017770945b)

以下の例で、home->supplier->(supplierの)detailと遷移したときのハッシュは次のようになる。
`#/suppliers&/s/detail/1` 

親のコンポーネント

```json:manifest.json
			"routes": [
				{
					"name": "home",
					"pattern": "",
					"target": "home"
				},
				{
					"name": "suppliers",
					"pattern": "suppliers",
					"target": {
						"name": "suppliers",
						"prefix": "s"
					}
				}
```

子のコンポーネント

```json:manifest.json
			"routes": [
				{
					"name": "list",
					"pattern": "",
					"target": "list"
				},
				{
					"name": "detail",
					"pattern": "detail/{id}",
					"target": "detail"
				}
```

####問題点
親のコンポーネントから子の特定のルートに直接遷移できない。子のコンポーネントが呼び出されるとき、子のRouterは空のストリングを受け取るため、デフォルトのルートを表示してしまう。

####[UI5 – Component Based Routing](https://blogs.sap.com/2019/10/09/ui5-component-based-routing/)での解決方法

**やりたいこと**
Supplierコンポーネントのdetailルートから、Productコンポーネントのdetailへ遷移したい。（SupplierはProductの親になっている）
デフォルトではProduct-detailへの直接の遷移ができず、listが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9f56319d-cb8b-5c91-c028-ac902a06bbdc.png)


**解決方法**
Productのlistコントローラーのパターンマッチイベントの中で、URLの中から親のハッシュセグメントが持っているProduct idを取り出す。idが見つかったら、自らdetailルートにナビゲーションする。
著者も指摘しているとおり、この方法は子のコンポーネントが親のRoutingパターンを知らないとできないので、親子の結びつきが強くなってしまうのが欠点。

```javascript
		onInit: function() {
			Controller.prototype.onInit.apply(this, arguments)

			this.getOwnerComponent()
				.getRouter()
				.getRoute("list")
				.attachPatternMatched(this._onPatternMatched, this)
		},
		_onPatternMatched: function() {
			Controller.prototype.onInit.apply(this, arguments)

			const oRouter = this.getOwnerComponent().getRouter()
			try {
				const aHash = oRouter.oHashChanger.parent.hash.split("/")
				if (aHash.length > 1) {
					switch (aHash[0]) {
						case "products":
							oRouter.navTo(
								"detail",
								{
									id: aHash[1]
								},
								true
							)
							break
						default:
					}
				}
			} catch {}
		},
```

###Version 1.72
RouterのnavToメソッドが拡張されて、ナビゲーションの際に子（および階層化された子孫）の特定のルートを指定できるようになった。⇒[ドキュメント](https://ui5.sap.com/1.72.1/#/topic/8e9d6e4df5464eb5a6e40696c4d2ccd6)

親のRouterを使って子1と子2、および孫のルートを指定して遷移する例

```javascript
oRouter.navTo("home", {
    // this route doesn't need any parameter
}, {
    //子1のルート
    childComp1: {
        route: "detail",
        parameters: {
            ...
        }
    },
    //子2のルート
    childComp2: {
        route: "detail",
        parameters: {
            ...
        },
        componentTargetInfo: {
            //孫のルート
            grandChildComp1: {
                route: "detail",
                parameters: {
                    ...
                }
            }
        }
    }
});
```
####課題：子のコンポーネント間のナビゲーション
以下の例で、Root ComponentはProduct、Supplier、Categoryの3つのコンポーネントをルートに定義している。この中で、Product-Supplier間、およびProduct-Category間の双方向の遷移を可能にしたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c8ec41cd-8359-f6ca-1ed9-878256875f9b.png)
[UI5er Buzz #46 – Routing with Nested Components](https://blogs.sap.com/2020/02/05/ui5er-buzz-46-routing-with-nested-components/)より引用

ルートコンポーネントのRouting定義は以下のようになっている。

```json:manifest.json
			"routes": [
				{
					"name": "home",
					"pattern": "",
					"target": [
						"home"
					]
				},
				{
					"name": "suppliers",
					"pattern": "suppliers",
					"target": {
						"name": "suppliers",
						"prefix": "s"
					}
				},
				{
					"name": "categories",
					"pattern": "categories",
					"target": {
						"name": "categories",
						"prefix": "c"
					}
				},				
				{
					"name": "products",
					"pattern": "products",
					"target": {
						"name": "products",
						"prefix": "p"
					}
				}
			],
			"targets": {
				"home": {
					"viewType": "XML",
					"viewName": "Home"
				},
				"suppliers": {
					"type": "Component",
					"usage": "suppliersComponent"
				},
				"categories": {
					"type": "Component",
					"usage": "categoriesComponent"
				},				
				"products": {
					"type": "Component",
					"usage": "productsComponent"
				},
				"notFound": {
					"viewName": "NotFound",
					"transition": "show"
				}
			}
```

####[UI5 – Navigate with Nested Components](https://blogs.sap.com/2019/12/16/ui5-navigate-with-nested-components/)での解決方法
子のコンポーネントを生成する際に、ルートコンポーネントの名前を渡す。子のコンポーネントは親をたどってルートを取得し、ルートコンポーネントのRouterを使ってナビゲーションする。

ルートコンポーネントのmanifest.json定義

```json
"productsComponent": {
  "name": "yelcho.reuse.products",
  "settings": {},
  "componentData": {
    "parentComponentName": "yelcho.mydemo.nestcomproute.Component" <-ルートコンポーネント
  },
  "lazy": true
}
```
子のコンポーネントからさかのぼってルートコンポーネントを探す

```javascript
getMainComponent: function() {
  let oElement = this.oContainer
  while (oElement && !this._mainComponent) {
    try {
      oElement = oElement.getParent()
      if (
        oElement.getMetadata().getName() ===
        this.oComponentData.parentComponentName
      ) {
        this._mainComponent = oElement
      }
    } catch {}
  }
  return this._mainComponent ? this._mainComponent : this
},
```


####[UI5er Buzz #46 – Routing with Nested Components](https://blogs.sap.com/2020/02/05/ui5er-buzz-46-routing-with-nested-components/)での解決方法
子のコンポーネントからナビゲーションする際に、navToの代わりにイベントを発生させる。
Rootコンポーネントがこのイベントをキャッチし、ナビゲーションを行う。
[GitHubのソースコード](https://github.com/Yelcho/UI5-Comp-Routing)

**仕組み**

- 各コンポーネントは同じBaseComponentを継承している
- BaseComponentの中でnavToの代わりのイベントに対するイベントハンドラを定義している
- イベントハンドラの中で、自コンポーネントで定義されたルートを使用してナビゲーションを実行する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0d7c1391-36df-5547-5ea2-47f2833cd471.png)

子のコンポーネントでイベントを発生させる。

```javascript:Productのコントローラー
			oOwnerComponent.fireEvent("toSupplier", {
				supplierID: sSupplierID,
				supplierKey: encodeURIComponent("/" + oModel.createKey("Suppliers", {
					SupplierID: sSupplierID
				}))
			});
```
ルートコンポーネントでは、イベントマッピング（どのコンポーネントのどのイベントに反応するか、そのときnavToに渡すパラメータは何か）を定義している。

```javascript:RootのComponent.js
			productsComponent: [{
					name: "toSupplier",
					route: "suppliers",
					componentTargetInfo: {
						suppliers: {
							route: "detail",
							parameters: {
								id: "supplierID"
							},
							componentTargetInfo: {
								products: {
									route: "list",
									parameters: {
										basepath: "supplierKey"
									}
								}
							}
						}
					}
				}]
```

BaseComponentでイベントハンドラを定義している。イベントマッピングに従ってナビゲーション先のルート、およびcomponentTargetInfoを編集し、navToメソッドに渡す。（※コメントは筆者が追加）

```javascript
				//このコンポーネントに対して定義されたイベントを取得
				aEvents = this.eventMappings[oOptions.usage];
				if (Array.isArray(aEvents)) {
					aEvents.forEach(function(oEventMapping) {
						//イベントハンドラの設定
						//oEventMappingにはコンポーネントで定義したパラメータが入っている
						oObject.attachEvent(oEventMapping.name, function(oEvent) {
							var oComponentTargetInfo;
							if (oEventMapping.route) {
								if (oEventMapping.componentTargetInfo) {
									oComponentTargetInfo = deepClone(oEventMapping.componentTargetInfo);
									//イベントパラメータをもとにcomponentTargetInfoを編集する
									processComponentTargetInfo(oComponentTargetInfo, oEvent);
								}
								//ナビゲーション実行
								that.getRouter().navTo(oEventMapping.route, {}, oComponentTargetInfo);
							//子のコンポーネント経由で転送されてくる場合
							} else if (oEventMapping.forward) {
								that.fireEvent(oEventMapping.forward, oEvent.getParameters());
							}

						});
					});
				}
```
###Version 1.75 [New]
※この記事を書いている間に、OpenUI5のバージョン1.75で新しい機能が追加されました。
RoutingのTargetに、propagateTitleプロパティが追加された。propagateTitleを設定しておくと、ネストされたコンポーネントでタイトルが変更されたときに、親のコンポーネントに通知することができる。

[ドキュメント](https://openui5.hana.ondemand.com/1.75.0/#/topic/1238d706b130433c9bd6b85cfb77cece.html)
[サンプル](https://openui5.hana.ondemand.com/1.75.0/#/entity/sap.ui.core.routing.Router/sample/sap.ui.core.sample.RoutingNestedComponent)
ブログ：[UI5ers Buzz #48: Consuming Title Changes of Nested Components](https://blogs.sap.com/2020/03/23/ui5ers-buzz-48-consuming-title-changes-of-nested-components/)