SharePoint Framework で TODO管理アプリを作る (4/4) 詳細画面の作成

続きです。今回も SharePoint Online のデータへのアクセスが含まれるので、SPFxっぽいかもです。
毎度ながら内容に関して 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. 新規作成用関数の作成

データ操作用の関数なので、api フォルダを更新します。

``` :思い出のフォルダ構成 
...src/
　└ webparts/
　　└ spfxTodo/
    　├ api/ (データ操作用関数を管理)
    　├ assets/
　    │　└ stylesheets/
    　├ components/
　    │　├ atoms/
　    │　├ molecules/ (中くらいの部品を管理)
　    │　└ organisms/
　    │　└ pages/ (画面を管理)
　    │　└ templates/
　    └ loc/
```

詳細画面用の単一アイテムを取得する関数と、リストアイテム作成用の関数を作っていきます。
Postなので作成するデータを作ってあげないといけませんが、列名を key 値を value にした Object を渡してあげるだけであとはいい感じにしてくれます。

``` TypeScript:api/index.ts
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';

/********** 画面から呼び出し用関数 **********/
/********** TodoList GET **********/
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';

import * as util from '../components/util';

import { ITodoItem } from '../components/molecules/ITodoItem';

/********** 画面から呼び出し用関数 **********/
/********** TodoList GET **********/
const getTodoListOptions = "?$select=ID,Title,LimitDate,Note,Modified,Status&$filter=Status eq 'Run'&$orderby=LimitDate asc";

export const GetTodoListItems =
	async (setState: any, targetListName: string, context: WebPartContext) => {
		setState({ loading: true });
		const todoListItems: Array<Object> = await GetListItems(context, targetListName, getTodoListOptions);
		setState({ loading: false, todoListItems });
	};

/********** TodoDetail GET **********/
export const GetTodoListItemByID =
	async (setState: any, targetListName: string, context: WebPartContext, ID: string) => {

		setState({ loading: true });
		const todoListItem: Object = await GetListItemByID(context, targetListName, ID);
		const todoItem: ITodoItem = util.ObjectMerge({ Title: "", LimitDate: null, Note: "", Status: "", ID: "", Created: null, Modified: null }, todoListItem);
		setState({ loading: false, iTodoItem: todoItem });
	};

/********** TodoDetail POST **********/
export const CreateTodoListItem =
	async (setState: any, targetListName: string, context: WebPartContext, todoItem: ITodoItem) => {

		setState({ loading: true });
		const createTodoItem: ITodoItem = util.ObjectMerge({ Title: "", LimitDate: null, Note: '', Status: '' }, todoItem);
		createTodoItem.Status = "Run";
		const res = await CreateListItem(context, targetListName, createTodoItem);
	};

/********** リストアイテムの操作用共通関数 **********/
const defHeaders: HeadersInit = { "Content-type": "application/json", "Accept": "application/json" };

/********** 検索 **********/
const GetListItems =
	async (context: WebPartContext, listName: string, options: string) => {

		if (!options) {
			options = "";
		}
		const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items${options}`;
		const res: SPHttpClientResponse = await SpRestGet(context, restUri);
		const resJson: any = await res.json();
		const resJsonArray: Array<Object> = resJson.value;
		return resJsonArray;
	};

const GetListItemByID =
	async (context: WebPartContext, listName: string, ID: string) => {

		const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items(${ID})`;
		const res: SPHttpClientResponse = await SpRestGet(context, restUri);
		const resJson: any = await res.json();
		return resJson;
	};

/********** 作成 **********/
const CreateListItem = async (context: WebPartContext, listName: string, dataJson: Object) => {
	const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items`;
	const options: ISPHttpClientOptions = {
		body: JSON.stringify(dataJson),
		headers: defHeaders
	};
	const res: SPHttpClientResponse = await SpRestPost(context, restUri, options);
	const resJson: any = await res.json();
	return resJson;
};

/********** 更新 **********/

/********** 削除 **********/


/********** Spへのアクセス用共通関数 **********/
/********** GET Request **********/
const SpRestGet =
	async (context: WebPartContext, RestUri: string): Promise<SPHttpClientResponse> => {

		const res: SPHttpClientResponse = await context.spHttpClient.get(RestUri, SPHttpClient.configurations.v1);
		//エラーチェックは他のサイトが詳しいので省きます
		return res;
	};

/********** POST Request **********/
const SpRestPost =
	async (context: WebPartContext, RestUri: string, options: ISPHttpClientOptions): Promise<SPHttpClientResponse> => {

		const res: SPHttpClientResponse = await context.spHttpClient.post(RestUri, SPHttpClient.configurations.v1, options);
		//エラーチェックは外部サイトが詳しいので省きます
		return res;
	};
```

#### 2. 詳細画面用コンポーネントの新規作成機能の作成

次に先ほどの関数を画面から呼び出します。
今回は入力フォームの部分だけ、TodoItemForm として子コンポーネントに切り出しました。
関数の呼び出しは親コンポーネントで定義して、関数ごと子コンポーネントに渡す方針とします。

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

import { Stack, IStackTokens } from 'office-ui-fabric-react';

import Loading from '../molecules/Loading';
import * as api from '../../api';

import TodoItemForm from '../molecules/TodoItemForm';

import { IPageProps } from './IPageProps';
import { ITodoItem } from '../molecules/ITodoItem';

interface State {
  loading: boolean;
  formType: "new" | "edit";
  iTodoItem: ITodoItem;
}

class TodoDetail extends React.Component<IPageProps, State> {

  constructor(props: IPageProps) {
    super(props);
    this.state = {
      loading: false, formType: "new",
      iTodoItem: { Title: "", LimitDate: new Date().toJSON(), Note: "", Status: "", ID: "", Created: null, Modified: null }
    };
  }

  private CreateTodoListItem = async (todoItem: ITodoItem) => {
    await api.CreateTodoListItem(this.setState.bind(this), this.props.todoListName, this.props.webPartContext, todoItem);
    this.props.routeProps.history.push({ pathname: "/" });
  }

  private GetTodoListItemByID = async () =>
    await api.GetTodoListItemByID(this.setState.bind(this), this.props.todoListName, this.props.webPartContext, this.props.routeProps.match.params.ID)

  private HistoryPushTodoList = () => {
    this.props.routeProps.history.push({ pathname: "/" });
  }

  public render() {
    const stackTokens: IStackTokens = { childrenGap: 10 };
    const { routeProps } = this.props;
    const { match } = routeProps;
    const { loading, iTodoItem } = this.state;
    return (
      <div>
        <Stack tokens={stackTokens}>
          <Stack.Item align="auto">
            {loading ? (
              <Loading />
            ) : (
                <TodoItemForm itemID={match.params.ID || null} formType={match.params.ID ? "edit" : "new"} iTodoItem={iTodoItem}
                  CreateTodoListItem={this.CreateTodoListItem} GetTodoListItemByID={this.GetTodoListItemByID}
                  HistoryPushTodoList={this.HistoryPushTodoList} />
              )}
          </Stack.Item>
        </Stack>
      </div>
    );
  }
}
export default TodoDetail;
```

``` TypeScript:molecules/ITodoItemFormProps.ts
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { ITodoItem } from './ITodoItem';

export interface ITodoItemFormProps {
  itemID: string;
  formType: "new" | "edit";
  iTodoItem: ITodoItem;
  CreateTodoListItem: Function;
  GetTodoListItemByID: Function;
  HistoryPushTodoList: Function;
}
```

``` TypeScript:molecules/TodoItemForm.tsx
import * as React from 'react';

import {
  PrimaryButton, DefaultButton, Stack, IStackTokens, DatePicker, TextField, Separator
} from 'office-ui-fabric-react';

import * as util from '../util';

import { ITodoItemFormProps } from './ITodoItemFormProps';
import { ITodoItem } from './ITodoItem';


class TodoItemForm extends React.Component<ITodoItemFormProps, ITodoItem> {

  constructor(props: ITodoItemFormProps) {
    super(props);
    this.state = this.props.iTodoItem;
  }

  private CreateTodoListItem = async (e, itodoItem: ITodoItem) => {
    e.preventDefault();
    await this.props.CreateTodoListItem(itodoItem);
  }
  private GetTodoListItemByID = async () => this.props.GetTodoListItemByID();

  private HistoryPushTodoList = () => this.props.HistoryPushTodoList();

  public async componentDidMount() {
    if (this.props.formType == "edit" && !(this.state.ID)) {
      await this.GetTodoListItemByID();
    }
  }

  public render() {
    const stackTokens: IStackTokens = { childrenGap: 10 };
    const { formType } = this.props;
    const { Title, LimitDate, Note, ID, Created, Modified } = this.state;
    return (
      <div>
        <form>
          <Stack horizontal tokens={stackTokens}>
            <Stack.Item align="auto" grow={4}>
              <TextField label="件名" value={Title} onChange={(e, newVal) => this.setState({ Title: newVal })} />
            </Stack.Item>
            <Stack.Item align="auto" grow={2}>
              <DatePicker label="期限" value={new Date(LimitDate)}
                formatDate={(dispDate) => util.DateFormatJa(dispDate.toJSON())}
                onSelectDate={(newDate) => this.setState({ LimitDate: newDate.toJSON() })} />
            </Stack.Item>
          </Stack>
          <Stack horizontal tokens={stackTokens} >
            <Stack.Item align="auto" grow={6}>
              <TextField label="メモ" value={Note} multiline rows={6} resizable={false}
                onChange={(e, newVal) => this.setState({ Note: newVal })} />
            </Stack.Item>
          </Stack>
          <Separator />
          <Stack horizontal tokens={stackTokens} >
            <Stack.Item align="auto" grow={2}>
              <TextField label="ID" value={ID || "New"} borderless={true} readOnly={true} />
            </Stack.Item>
            <Stack.Item align="auto" grow={2}>
              <TextField label="作成日時" value={Created ? util.DateTimeFormatJa(Created) : "-"} borderless={true} readOnly={true} />
            </Stack.Item>
            <Stack.Item align="auto" grow={2}>
              <TextField label="更新日時" value={Modified ? util.DateTimeFormatJa(Modified) : "-"} borderless={true} readOnly={true} />
            </Stack.Item>
          </Stack>
          <Separator />
          <Stack tokens={stackTokens}>
            <Stack horizontal horizontalAlign="end" tokens={stackTokens}>
              <Stack.Item align="auto">
                <PrimaryButton type="submit" value="Create" onClick={(e) => this.CreateTodoListItem(e, this.state)} disabled={formType === "edit"} text="Create" />
              </Stack.Item>
             </Stack>
            <Stack horizontal horizontalAlign="end" tokens={stackTokens}>
              <Stack.Item align="auto">
                <DefaultButton text="Back" onClick={this.HistoryPushTodoList} />
              </Stack.Item>
            </Stack>
          </Stack>
        </form>
      </div>
    );
  }
}
export default TodoItemForm;
```

Hosted workbench の実行で以下のように表示できていれば成功です。
Createボタンをクリックするとアイテムが作成されると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/312282/577ca033-fd96-e211-8745-a7dac8f9b0a1.png)

#### 3. 更新、削除用関数の作成

ほとんど同じコードですが、更新、削除も作成します。

``` TypeScript:api/index.ts
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';

import * as util from '../components/util';

import { ITodoItem } from '../components/molecules/ITodoItem';

/********** 画面から呼び出し用関数 **********/
/********** TodoList GET **********/
const getTodoListOptions = "?$select=ID,Title,LimitDate,Note,Modified,Status&$filter=Status eq 'Run'&$orderby=LimitDate asc";

export const GetTodoListItems =
	async (setState: any, targetListName: string, context: WebPartContext) => {
		setState({ loading: true });
		const todoListItems: Array<Object> = await GetListItems(context, targetListName, getTodoListOptions);
		setState({ loading: false, todoListItems });
	};

/********** TodoDetail GET **********/
export const GetTodoListItemByID =
	async (setState: any, targetListName: string, context: WebPartContext, ID: string) => {

		setState({ loading: true });
		const todoListItem: Object = await GetListItemByID(context, targetListName, ID);
		const todoItem: ITodoItem = util.ObjectMerge({ Title: "", LimitDate: null, Note: "", Status: "", ID: "", Created: null, Modified: null }, todoListItem);
		setState({ loading: false, iTodoItem: todoItem });
	};

/********** TodoDetail POST **********/
export const CreateTodoListItem =
	async (setState: any, targetListName: string, context: WebPartContext, todoItem: ITodoItem) => {

		setState({ loading: true });
		const createTodoItem: ITodoItem = util.ObjectMerge({ Title: "", LimitDate: null, Note: '', Status: '' }, todoItem);
		createTodoItem.Status = "Run";
		const res = await CreateListItem(context, targetListName, createTodoItem);
	};

export const UpdateTodoListItem =
	async (setState: any, targetListName: string, context: WebPartContext, todoItem: ITodoItem) => {

		setState({ loading: true });
		const updateTodoItem: ITodoItem = util.ObjectMerge({ Title: "", LimitDate: null, Note: '', Status: '' }, todoItem);
		const res = await UpdateListItem(context, targetListName, todoItem.ID, updateTodoItem);
	};

export const CompleteTodoListItem =
	async (setState: any, targetListName: string, context: WebPartContext, todoItem: ITodoItem) => {

		setState({ loading: true });
		const updateTodoItem: ITodoItem = util.ObjectMerge({ Title: "", LimitDate: null, Note: '', Status: '' }, todoItem);
		updateTodoItem.Status = "Done";
		const res = await UpdateListItem(context, targetListName, todoItem.ID, updateTodoItem);
	};

export const DeleteTodoListItem =
	async (setState: any, targetListName: string, context: WebPartContext, todoItem: ITodoItem) => {

		setState({ loading: true });
		const res = await DeleteListItem(context, targetListName, todoItem.ID);
	};

/********** リストアイテムの操作用共通関数 **********/
const defHeaders: HeadersInit = { "Content-type": "application/json", "Accept": "application/json" };
const updHeaders: HeadersInit = { "Content-type": "application/json", "Accept": "application/json", "X-HTTP-Method": "MERGE", "If-Match": "*" };
const delHeaders: HeadersInit = { "Content-type": "application/json", "Accept": "application/json", "X-HTTP-Method": "DELETE", "If-Match": "*" };

/********** 検索 **********/
const GetListItems =
	async (context: WebPartContext, listName: string, options: string) => {

		if (!options) {
			options = "";
		}
		const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items${options}`;
		const res: SPHttpClientResponse = await SpRestGet(context, restUri);
		const resJson: any = await res.json();
		const resJsonArray: Array<Object> = resJson.value;
		return resJsonArray;
	};

const GetListItemByID =
	async (context: WebPartContext, listName: string, ID: string) => {

		const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items(${ID})`;
		const res: SPHttpClientResponse = await SpRestGet(context, restUri);
		const resJson: any = await res.json();
		return resJson;
	};

/********** 作成 **********/
const CreateListItem = async (context: WebPartContext, listName: string, dataJson: Object) => {
	const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items`;
	const options: ISPHttpClientOptions = {
		body: JSON.stringify(dataJson),
		headers: defHeaders
	};
	const res: SPHttpClientResponse = await SpRestPost(context, restUri, options);
	const resJson: any = await res.json();
	return resJson;
};

/********** 更新 **********/
const UpdateListItem = async (context: WebPartContext, listName: string, ID: string, dataJson: Object) => {
	const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items(${ID})`;
	const options: ISPHttpClientOptions = {
		body: JSON.stringify(dataJson),
		headers: updHeaders
	};
	const res: SPHttpClientResponse = await SpRestPost(context, restUri, options);
	return res;
};

/********** 削除 **********/
const DeleteListItem = async (context: WebPartContext, listName: string, ID: string) => {
	const restUri: string = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/Items(${ID})`;
	const options: ISPHttpClientOptions = {
		headers: delHeaders
	};
	const res: SPHttpClientResponse = await SpRestPost(context, restUri, options);
	return res;
};

/********** Spへのアクセス用共通関数 **********/
/********** GET Request **********/
const SpRestGet =
	async (context: WebPartContext, RestUri: string): Promise<SPHttpClientResponse> => {

		const res: SPHttpClientResponse = await context.spHttpClient.get(RestUri, SPHttpClient.configurations.v1);
		//エラーチェックは外部サイトが詳しいので省きます
		return res;
	};

/********** POST Request **********/
const SpRestPost =
	async (context: WebPartContext, RestUri: string, options: ISPHttpClientOptions): Promise<SPHttpClientResponse> => {

		const res: SPHttpClientResponse = await context.spHttpClient.post(RestUri, SPHttpClient.configurations.v1, options);
		//エラーチェックは外部サイトが詳しいので省きます
		return res;
	};
```

#### 4. 詳細画面用コンポーネントの更新、削除機能の作成

ここもほとんど同じです。更新、削除関数の呼び出しを追加していきます。

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

import { Stack, IStackTokens } from 'office-ui-fabric-react';

import Loading from '../molecules/Loading';
import * as api from '../../api';

import TodoItemForm from '../molecules/TodoItemForm';

import { IPageProps } from './IPageProps';
import { ITodoItem } from '../molecules/ITodoItem';

interface State {
  loading: boolean;
  formType: "new" | "edit";
  iTodoItem: ITodoItem;
}

class TodoDetail extends React.Component<IPageProps, State> {

  constructor(props: IPageProps) {
    super(props);
    this.state = {
      loading: false, formType: "new",
      iTodoItem: { Title: "", LimitDate: new Date().toJSON(), Note: "", Status: "", ID: "", Created: null, Modified: null }
    };
  }

  private CreateTodoListItem = async (todoItem: ITodoItem) => {
    await api.CreateTodoListItem(this.setState.bind(this), this.props.todoListName, this.props.webPartContext, todoItem);
    this.props.routeProps.history.push({ pathname: "/" });
  }
  private UpdateTodoListItem = async (todoItem: ITodoItem) => {
    await api.UpdateTodoListItem(this.setState.bind(this), this.props.todoListName, this.props.webPartContext, todoItem);
    this.props.routeProps.history.push({ pathname: "/" });
  }
  private CompleteTodoListItem = async (todoItem: ITodoItem) => {
    await api.CompleteTodoListItem(this.setState.bind(this), this.props.todoListName, this.props.webPartContext, todoItem);
    this.props.routeProps.history.push({ pathname: "/" });
  }
  private DeleteTodoListItem = async (todoItem: ITodoItem) => {
    await api.DeleteTodoListItem(this.setState.bind(this), this.props.todoListName, this.props.webPartContext, todoItem);
    this.props.routeProps.history.push({ pathname: "/" });
  }

  private GetTodoListItemByID = async () =>
    await api.GetTodoListItemByID(this.setState.bind(this), this.props.todoListName, this.props.webPartContext, this.props.routeProps.match.params.ID)

  private HistoryPushTodoList = () => {
    this.props.routeProps.history.push({ pathname: "/" });
  }

  public render() {
    const stackTokens: IStackTokens = { childrenGap: 10 };
    const { routeProps } = this.props;
    const { match } = routeProps;
    const { loading, iTodoItem } = this.state;
    return (
      <div>
        <Stack tokens={stackTokens}>
          <Stack.Item align="auto">
            {loading ? (
              <Loading />
            ) : (
                <TodoItemForm itemID={match.params.ID || null} formType={match.params.ID ? "edit" : "new"} iTodoItem={iTodoItem}
                  CreateTodoListItem={this.CreateTodoListItem} UpdateTodoListItem={this.UpdateTodoListItem} 
                  CompleteTodoListItem={this.CompleteTodoListItem} DeleteTodoListItem={this.DeleteTodoListItem} 
                  GetTodoListItemByID={this.GetTodoListItemByID} HistoryPushTodoList={this.HistoryPushTodoList} />
              )}
          </Stack.Item>
        </Stack>
      </div>
    );
  }
}
export default TodoDetail;
```

``` TypeScript:molecules/ITodoItemFormProps.ts
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { ITodoItem } from './ITodoItem';

export interface ITodoItemFormProps {
  itemID: string;
  formType: "new" | "edit";
  iTodoItem: ITodoItem;
  CreateTodoListItem: Function;
  UpdateTodoListItem: Function;
  CompleteTodoListItem: Function;
  DeleteTodoListItem: Function;
  GetTodoListItemByID: Function;
  HistoryPushTodoList: Function;
}
```

``` TypeScript:molecules/TodoItemForm.tsx
import * as React from 'react';

import {
  PrimaryButton, DefaultButton, Stack, IStackTokens, DatePicker, TextField, Separator
} from 'office-ui-fabric-react';

import * as util from '../util';

import { ITodoItemFormProps } from './ITodoItemFormProps';
import { ITodoItem } from './ITodoItem';


class TodoItemForm extends React.Component<ITodoItemFormProps, ITodoItem> {

  constructor(props: ITodoItemFormProps) {
    super(props);
    this.state = this.props.iTodoItem;
  }

  private CreateTodoListItem = async (e, itodoItem: ITodoItem) => {
    e.preventDefault();
    await this.props.CreateTodoListItem(itodoItem);
  }
  private UpdateTodoListItem = async (e, itodoItem: ITodoItem) => {
    e.preventDefault();
    await this.props.UpdateTodoListItem(itodoItem);
  }
  private CompleteTodoListItem = async (e, itodoItem: ITodoItem) => {
    e.preventDefault();
    await this.props.CompleteTodoListItem(itodoItem);
  }
  private DeleteTodoListItem = async (e, itodoItem: ITodoItem) => {
    e.preventDefault();
    await this.props.DeleteTodoListItem(itodoItem);
  }

  private GetTodoListItemByID = async () => this.props.GetTodoListItemByID();

  private HistoryPushTodoList = () => this.props.HistoryPushTodoList();

  public async componentDidMount() {
    if (this.props.formType == "edit" && !(this.state.ID)) {
      await this.GetTodoListItemByID();
    }
  }

  public render() {
    const stackTokens: IStackTokens = { childrenGap: 10 };
    const { formType } = this.props;
    const { Title, LimitDate, Note, ID, Created, Modified } = this.state;
    return (
      <div>
        <form>
          <Stack horizontal tokens={stackTokens}>
            <Stack.Item align="auto" grow={4}>
              <TextField label="件名" value={Title} onChange={(e, newVal) => this.setState({ Title: newVal })} />
            </Stack.Item>
            <Stack.Item align="auto" grow={2}>
              <DatePicker label="期限" value={new Date(LimitDate)}
                formatDate={(dispDate) => util.DateFormatJa(dispDate.toJSON())}
                onSelectDate={(newDate) => this.setState({ LimitDate: newDate.toJSON() })} />
            </Stack.Item>
          </Stack>
          <Stack horizontal tokens={stackTokens} >
            <Stack.Item align="auto" grow={6}>
              <TextField label="メモ" value={Note} multiline rows={6} resizable={false}
                onChange={(e, newVal) => this.setState({ Note: newVal })} />
            </Stack.Item>
          </Stack>
          <Separator />
          <Stack horizontal tokens={stackTokens} >
            <Stack.Item align="auto" grow={2}>
              <TextField label="ID" value={ID || "New"} borderless={true} readOnly={true} />
            </Stack.Item>
            <Stack.Item align="auto" grow={2}>
              <TextField label="作成日時" value={Created ? util.DateTimeFormatJa(Created) : "-"} borderless={true} readOnly={true} />
            </Stack.Item>
            <Stack.Item align="auto" grow={2}>
              <TextField label="更新日時" value={Modified ? util.DateTimeFormatJa(Modified) : "-"} borderless={true} readOnly={true} />
            </Stack.Item>
          </Stack>
          <Separator />
          <Stack tokens={stackTokens}>
            <Stack horizontal horizontalAlign="end" tokens={stackTokens}>
              <Stack.Item align="auto">
                <PrimaryButton type="submit" value="Create" onClick={(e) => this.CreateTodoListItem(e, this.state)} disabled={formType === "edit"} text="Create" />
              </Stack.Item>
              <Stack.Item align="auto">
                <PrimaryButton type="submit" value="Update" onClick={(e) => this.UpdateTodoListItem(e, this.state)} disabled={formType === "new"} text="Update" />
              </Stack.Item>
              <Stack.Item align="auto">
                <PrimaryButton type="submit" value="Complete" onClick={(e) => this.CompleteTodoListItem(e, this.state)} disabled={formType === "new"} text="Complete(Update)" />
              </Stack.Item>
              <Stack.Item align="auto">
                <PrimaryButton type="submit" value="Delete" onClick={(e) => this.DeleteTodoListItem(e, this.state)} disabled={formType === "new"} text="Delete" />
              </Stack.Item>
            </Stack>
            <Stack horizontal horizontalAlign="end" tokens={stackTokens}>
              <Stack.Item align="auto">
                <DefaultButton text="Back" onClick={this.HistoryPushTodoList} />
              </Stack.Item>
            </Stack>
          </Stack>
        </form>
      </div>
    );
  }
}
export default TodoItemForm;
```

Hosted workbench の実行で完成イメージ(4/4)みたいに表示されれば完成です。
お疲れさまでした！

アプリが完成したら、実際に展開したくなると思います。
その手順はまたまた毎度引用ばかりになりますが、以下を参考にされるといいと思います。

> https://sharepoint.orivers.jp/article/10302

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

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

アプリとして不十分な部分はたくさんありますが、基本的な操作を網羅できたかと思います。
あくまで一つの例として参考になれば幸いです。
みなさまもぜひ、いろんなアプリを作ってみてください。