SharePoint Framework で TODO管理アプリを作る (2/4) 画面レイアウトと画面遷移

続きです。ここからはほぼ React で開発していくだけです。
内容に関して React も TypeScript もあまり上手に使えるわけではないですので、そのあたりご容赦ください。。。

# 完成イメージ(4/4)

Teams のタブにもいい感じにおさまります。
![GIF 2020-11-06 17-03-53.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/312282/e41e4a69-4eeb-4110-5304-8b68f02d5115.gif)

下は画像が荒いですね。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">来週の Global Microsoft 365 Developer Bootcamp 前に SharePoint Framework を予習しよう！Teams のタブにもなるよ！<br>Qiita用にツイート。<a href="https://twitter.com/hashtag/OfficeDev?src=hash&amp;ref_src=twsrc%5Etfw">#OfficeDev</a> <a href="https://twitter.com/hashtag/SharePointOnline?src=hash&amp;ref_src=twsrc%5Etfw">#SharePointOnline</a> <a href="https://twitter.com/hashtag/SharePointFramework?src=hash&amp;ref_src=twsrc%5Etfw">#SharePointFramework</a> <a href="https://twitter.com/hashtag/MicrosoftTeams?src=hash&amp;ref_src=twsrc%5Etfw">#MicrosoftTeams</a> <a href="https://t.co/m4oSyYCAP3">pic.twitter.com/m4oSyYCAP3</a></p>&mdash; nan_oka (@nanoka7o8) <a href="https://twitter.com/nanoka7o8/status/1324625754072653824?ref_src=twsrc%5Etfw">November 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

もちろん SharePoint Online のページとしても使えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/312282/23871061-35a0-0b21-0323-1fbd07ece508.png)

#### 関連記事
Webパーツ初期設定編 1/4
https://qiita.com/nanoka/items/834ee5216a5437dc04bc
画面レイアウトと画面遷移編 2/4
https://qiita.com/nanoka/items/35dea4791ad767497ed6
一覧画面編 3/4
https://qiita.com/nanoka/items/fbda2f09796a445dd9e2
詳細画面編 4/4
https://qiita.com/nanoka/items/92093ca05096728e2deb

# 手順
　1. 画面レイアウトと画面の決定
　2. 画面レイアウト用のコンポーネントの作成
　3. 画面用のコンポーネントの作成
　4. 画面レイアウトと画面の、呼び出し元コンポーネントの作成

# 手順の説明

#### 1. 画面レイアウトと画面の決定

今回は画面上部にメニューっぽいものがあって、画面中央から下部にかけてメインのコンテンツがあるようなレイアウトとしたいと思います。
また、コンテンツとして、以下の2画面を作ることとします。
- TODO一覧画面
- TODO詳細画面(作成、更新、削除機能付き）
- Help画面

#### 2. 画面レイアウト用のコンポーネントの作成

構成が決まったので、実際に開発を進めていきます。
templates フォルダに 以下のファイルを作成します。

今回のサンプルでは、Header も Body まったく同じになってしまいました。
実際にはここでレイアウトを定義する コードや CSS を書くのですが、後で記載する Stack というものを使ってサボることにしましたので、ほぼ空っぽのコンポーネントになっています。

``` :思い出のフォルダ構成 
...src/
　└ webparts/
　　└ spfxTodo/
    　├ api/
    　├ assets/
　    │　└ stylesheets/
    　├ components/
　    │　├ atoms/
　    │　├ molecules/
　    │　└ organisms/
　    │　└ pages/ (画面を管理)
　    │　└ templates/  (画面レイアウトを管理)
　    └ loc/
```

``` TSX:templates/BodyPanel.tsx
import * as React from 'react';
import styles from './Panel.module.scss';  

const BodyPanel = ({ children }) => (
  <div className={styles.body_panel}>
    {children}
  </div>
);
export default BodyPanel;
```

``` TSX:templates/HeaderPanel.tsx
import * as React from 'react';
import styles from './Panel.module.scss'; 

const HeaderPanel = ({ children }) => (
  <div className={styles.header_panel}>
    {children}
  </div>
);
export default HeaderPanel;  
```

``` scss:Panel.module.scss
@import '~office-ui-fabric-react/dist/sass/References.scss';

.header_panel {
  width: 95%;
}

.body_panel {
  width: 95%;
}

```


#### 3. 画面用のコンポーネントの作成

次に pages フォルダに以下のファイルを作成します。

まず親コンポーネントから受け取りたいものを定義したファイルを作成します。
今回は
- Webパーツのプロパティで設定した接続するリストの名前
- SharePoint とのやり取りで利用するコンテキスト
- Url のパラメータ受け取りや画面遷移に使うもの
この3つを定義しておきます。

``` TypeScript:pages/IPageProps.ts
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { RouteComponentProps } from 'react-router';

export interface IPageProps {
  todoListName: string;
  webPartContext: WebPartContext;
  routeProps: RouteComponentProps<{ID: string}>;
}
```

次に実際の画面用コンポーネントのファイルを作成します。
同じ形のファイルですが、正しく表示されているかの確認のため、表示される文字だけ違うものにしてあります。

``` TSX:pages/TodoList.tsx
import * as React from 'react';

import { IPageProps } from './IPageProps';

class TodoList extends React.Component<IPageProps> {

  constructor(props: IPageProps) {
    super(props);
  }

  public render() {
    return (
      <div>
        <p>TodoList</p>
      </div>
    );
  }

}
export default TodoList;
```

``` TSX:pages/TodoDetail.tsx
import * as React from 'react';

import { IPageProps } from './IPageProps';

class TodoDetail extends React.Component<IPageProps> {

  constructor(props: IPageProps) {
    super(props);
  }

  public render() {
    return (
      <div>
        <p>TodoDetail</p>
        <p>{this.props.routeProps.match.params.ID || "idなし"}</p>
      </div>
    );
  }

}
export default TodoDetail;
```

``` TSX:pages/Help.tsx
import * as React from 'react';

import { IPageProps } from './IPageProps';

class Help extends React.Component<IPageProps> {

  constructor(props: IPageProps) {
    super(props);
  }

  public render() {
    return (
      <div>
        <p>Help</p>
      </div>
    );
  }

}
export default Help;
```

#### 4. 画面レイアウトと画面の、呼び出し元コンポーネントの作成

最後に spfxTodo フォルダ直下に以下のファイルを作成します。

先ほどと同じように親からもらうものを定義します。

``` TypeScript:ISpAppProps.ts
import { WebPartContext } from "@microsoft/sp-webpart-base";

export interface ISpAppProps {
  todoListName: string;
  webPartContext: WebPartContext;
}
```

今回一番長いやつです。
import の部分では以下を読み込みます。
- 先ほど作成したコンポーネントの読み込み
- SharePoint っぽいUIを勝手にやってくれる office-ui-fabric-react
- 画面遷移をやってくれる react-router-dom

Stack はもちろん office-ui-fabric-react の各コンポーネントについては以下に詳細があります。

> https://developer.microsoft.com/ja-JP/fluentui#/controls/web

まず画面遷移について、普通の router を使うと直リンクが機能するかわからなかったので、Hashrouter を使っています。
レイアウトとして、HeaderPanel と BodyPanel を Stack でくるんでいますが、これは、単純に縦に並べたかっただけです。
今回、画面レイアウトが単純なのと、偶然見つけて使ってみたかったので、この Stack を使ってコンポーネントを並べるような作りにしています。
HeaderPanel の中では、各画面へのリンクと、対応するUri を並べています。
BodyPanel の中では、Uri に応じて表示するコンポーネントの定義と、その子コンポーネントに渡す値を定義しています。

``` TSX:SpApp.tsx
import * as React from 'react';
import styles from './SpApp.module.scss';

import { Route, NavLink, Switch, HashRouter } from 'react-router-dom';
import { DefaultButton, Stack, IStackTokens, Separator } from 'office-ui-fabric-react';

import HeaderPanel from './templates/HeaderPanel';
import BodyPanel from './templates/BodyPanel';

import TodoList from './pages/TodoList';
import TodoDetail from './pages/TodoDetail';
import Help from './pages/Help';

import { ISpAppProps } from './ISpAppProps';

class SpApp extends React.Component<ISpAppProps> {

  constructor(props: ISpAppProps) {
    super(props);
  }

  public render() {
    const stackTokens: IStackTokens = { childrenGap: 5 };
    const { todoListName, webPartContext } = this.props;
    return (
      <div className={styles.spapp}>
        <div className={styles.container}>
          <h1>SPFx Todo App</h1>
          <HashRouter>
            <Stack tokens={stackTokens}>
              <HeaderPanel>
                <nav>
                  <Stack horizontal tokens={stackTokens}>
                    <NavLink exact to={'/'} activeStyle={{ opacity: 0.6 }}>
                      <DefaultButton text='TodoList' />
                    </NavLink>
                    <NavLink to={'/TodoDetail'} activeStyle={{ opacity: 0.6 }}>
                      <DefaultButton text='TodoDetail' />
                    </NavLink>
                    <NavLink to={'/Help'} activeStyle={{ opacity: 0.6 }}>
                      <DefaultButton text='Help' />
                    </NavLink>
                  </Stack>
                </nav>
                <Separator />
              </HeaderPanel>
              <BodyPanel>
                <Switch>
                  <Route sensitive exact path='/' component={(props) => <TodoList todoListName={todoListName}
                    webPartContext={webPartContext} routeProps={props} />} />
                  <Route sensitive exact path='/TodoDetail' component={(props) => <TodoDetail todoListName={todoListName} 
                    webPartContext={webPartContext} routeProps={props}/>} />
                  <Route path='/TodoDetail/:ID' component={(props) => <TodoDetail todoListName={todoListName} 
                    webPartContext={webPartContext} routeProps={props}/>} />
                  <Route sensitive exact path='/Help' component={(props) => <Help todoListName={todoListName}
                    webPartContext={webPartContext} routeProps={props} />} />
                </Switch>
              </BodyPanel>
            </Stack>
          </HashRouter>
        </div>
      </div>
    );
  }
}
export default SpApp;
```
CSSは最低限として、画面の横幅だけ定めておきます。

``` scss:SpApp.module.scss
@import '~office-ui-fabric-react/dist/sass/References.scss';

.spapp {
  .container {
    max-width: 900px;
    margin: 0px auto;
  }
}

```

あともう一つ。前回作成したファイル更新し、今回作成したコンポーネントを呼び出します。
このファイルにあれこれ書いてもいいのですが、後々の取り出しやすさを考えて、別コンポーネントを呼び出す形にしています。

``` TSX:SpfxTodo.tsx
import * as React from 'react';
import styles from './SpfxTodo.module.scss';
import { ISpfxTodoProps } from './ISpfxTodoProps';
import { escape } from '@microsoft/sp-lodash-subset';

import SpApp from './SpApp';

export default class SpfxTodo extends React.Component<ISpfxTodoProps, {}> {
  public render(): React.ReactElement<ISpfxTodoProps> {
    return (
      <>
        <SpApp {...this.props}/>
      </>
    );
  }
}

```

Hosted workbench の実行で以下のように表示できていれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/312282/2ec02e1b-bccd-8816-4237-9747c27b7518.png)

アドレスバー に ID を打ち込めば ID が画面上に表示されます。直リンク対応もできそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/312282/f75caf4f-b2ba-2e4e-42e6-a42c43946f12.png)


以下に実際に動作させたファイルを置いておきますので、うまくいかない場合は参考にしてください。

> https://github.com/7o83/SpfxTodo2to4

次回は一覧画面を作ります。