ページ作成フォームの構築

Topics

  • ステップ1: 「Create a new page」ボタンの追加
  • ステップ2: 制御された PageForm の抽出
  • ステップ3: CreatePageForm の構築
    • Title, onChangeTitle, hasEdits
    • onSave, onCancel
    • lastError, isSaving
  • すべてを一つに
  • 次のステップ
  • 前のパートでは、ページの編集 機能を作成しました。このパートでは、ページの作成 機能を追加します。これから構築するフォームは以下のようになります。

    ステップ1: 「Create a new page」ボタンの追加

    まず、ページの作成 フォームを表示するボタンを作成します。これはパート3で作成した edit ボタンと同様です。

    import { useDispatch } from '@wordpress/data';
    import { Button, Modal, TextControl } from '@wordpress/components';
    
    function CreatePageButton() {
    	const [isOpen, setOpen] = useState( false );
    	const openModal = () => setOpen( true );
    	const closeModal = () => setOpen( false );
    	return (
    		<>
    			<Button onClick={ openModal } variant="primary">
    				Create a new Page
    			</Button>
    			{ isOpen && (
    				<Modal onRequestClose={ closeModal } title="Create a new page">
    					<CreatePageForm
    						onCancel={ closeModal }
    						onSaveFinished={ closeModal }
    					/>
    				</Modal>
    			) }
    		</>
    	);
    }
    
    function CreatePageForm() {
    	// Empty for now
    	return <div/>;
    }
    
    
    

    素晴らしい ! では、MyFirstApp で、このピカピカの新しいボタンを表示してみましょう。

    function MyFirstApp() {
    	// ...
    	return (
    		<div>
    			<div className="list-controls">
    				<SearchControl onChange={ setSearchTerm } value={ searchTerm }/>
    				<CreatePageButton/>
    			</div>
    			<PagesList hasResolved={ hasResolved } pages={ pages }/>
    		</div>
    	);
    }
    
    

    次のようになるはずです。

    Top ↑

    ステップ2: 制御された PageForm の抽出

    ボタンを設置したので、フォームの作成に集中できます。このチュートリアルはデータ管理が目的のため、完全なページエディターは作成しません。代わりに、フォームには1つのフィールド「投稿タイトル」のみを含めます。

    幸運なことにパート3で作成した EditPageForm が、全体の80%をすでに達成しています。ユーザーインターフェースの大部分を利用可能で、CreatePageForm 内でもこれを再利用します。フォームの UI を別のコンポーネントに抽出することから始めましょう。

    function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
    	// ...
    	return (
    		<PageForm
    			title={ page.title }
    			onChangeTitle={ handleChange }
    			hasEdits={ hasEdits }
    			lastError={ lastError }
    			isSaving={ isSaving }
    			onCancel={ onCancel }
    			onSave={ handleSave }
    		/>
    	);
    }
    
    function PageForm( { title, onChangeTitle, hasEdits, lastError, isSaving, onCancel, onSave } ) {
    	return (
    		<div className="my-gutenberg-form">
    			<TextControl
    				label="Page title:"
    				value={ title }
    				onChange={ onChangeTitle }
    			/>
    			{ lastError ? (
    				<div className="form-error">Error: { lastError.message }</div>
    			) : (
    				false
    			) }
    			<div className="form-buttons">
    				<Button
    					onClick={ onSave }
    					variant="primary"
    					disabled={ !hasEdits || isSaving }
    				>
    					{ isSaving ? (
    						<>
    							<Spinner/>
    							Saving
    						</>
    					) : 'Save' }
    				</Button>
    				<Button
    					onClick={ onCancel }
    					variant="tertiary"
    					disabled={ isSaving }
    				>
    					Cancel
    				</Button>
    			</div>
    		</div>
    	);
    }
    
    

    このコードの変更は、既存のアプリケーションの動作については影響を与えないはずです。念のため、ページを編集してみましょう。

    素晴らしい ! 編集フォームはそのままで、新しく CreatePageForm を作成する構築要素ができました。

    Top ↑

    ステップ3: CreatePageForm の構築

    CreatePageForm コンポーネントに残る唯一の作業は、PageForm コンポーネントのレンダーに必要な、以下の7つのプロパティの提供です。

    • title
    • onChangeTitle
    • hasEdits
    • lastError
    • isSaving
    • onCancel
    • onSave

    それぞれ見ていきましょう。

    Top ↑

    Title, onChangeTitle, hasEdits

    EditPageForm は Redux のステートに存在する既存のエンティティレコードを更新し、保存しました。そのため、editedEntityRecords セレクタに依存していました。

    しかし CreatePageForm の場合、既存のエンティティレコードはありません。空のフォームがあるだけです。ユーザーがタイプした内容はフォームに対してローカルのため、React の useState フックを使用して追跡できます。

    function CreatePageForm( { onCancel, onSaveFinished } ) {
    	const [title, setTitle] = useState();
    	const handleChange = ( title ) => setTitle( title );
    	return (
    		<PageForm
    			title={ title }
    			onChangeTitle={ setTitle }
    			hasEdits={ !!title }
    			{ /* ... */ }
    		/>
    	);
    }
    
    

    Top ↑

    onSave, onCancel

    EditPageForm では、saveEditedEntityRecord('postType', 'page', pageId ) アクションをディスパッチして、Redux ステート内の編集内容を保存しました。

    しかし、CreatePageForm では、Redux のステートに編集内容はありません。pageIdもありません。この場合、ディスパッチが必要なアクションは saveEntityRecord (名前に Edited はありません) で、pageId の代わりに、新しいエンティティレコードを表すオブジェクトを受け取ります。

    saveEntityRecord に渡されたデータは、適切な REST API エンドポイントへの POST リクエストとして送信されます。例えば、以下のようなアクションをディスパッチします。

    saveEntityRecord( 'postType', 'page', { title: "Test" } );
    
    

    /wp/v2/pages WordPress REST API のエンドポイントに対して、リクエストボディに単一フィールド title=Test を指定して、POST リクエストをトリガーします。

    これで saveEntityRecord の詳細が分かったので、CreatePageForm 内で使用してみます。

    function CreatePageForm( { onSaveFinished, onCancel } ) {
    	// ...
    	const { saveEntityRecord } = useDispatch( coreDataStore );
    	const handleSave = async () => {
    		const savedRecord = await saveEntityRecord(
    			'postType',
    			'page',
    			{ title }
    		);
    		if ( savedRecord ) {
    			onSaveFinished();
    		}
    	};
    	return (
    		<PageForm
    			{ /* ... */ }
    			onSave={ handleSave }
    			onCancel={ onCancel }
    		/>
    	);
    }
    
    

    もう一つ、詳細を触れなければならない点があります。新規に作成したページは、PagesList にまだ登録されていません。REST API のドキュメントによると、/wp/v2/pages エンドポイントはデフォルトで status=draft のページを作成 (POST リクエスト) しますが、status=publish のページを return (GET リクエスト) します。解決策は status パラメータを明示的に渡すことです。

    function CreatePageForm( { onSaveFinished, onCancel } ) {
    	// ...
    	const { saveEntityRecord } = useDispatch( coreDataStore );
    	const handleSave = async () => {
    		const savedRecord = await saveEntityRecord(
    			'postType',
    			'page',
    			{ title, status: 'publish' }
    		);
    		if ( savedRecord ) {
    			onSaveFinished();
    		}
    	};
    	return (
    		<PageForm
    			{ /* ... */ }
    			onSave={ handleSave }
    			onCancel={ onCancel }
    		/>
    	);
    }
    
    

    この変更をローカルの CreatePageForm コンポーネントに適用します。残りの2つのプロパティを片付けましょう。

    Top ↑

    lastError, isSaving

    EditPageForm は getLastEntitySaveError セレクタと isSavingEntityRecord セレクタを使用してエラーと進行状況の情報を取得しました。どちらの場合も、3つの引数 ( 'postType', 'page', pageId ) を渡しました。

    しかし、CreatePageForm には pageId がありません。ではどうするか ? id のないエンティティレコードの情報の取得には、pageId 引数を省略できます。これが新しく作成されたものになります。結果、useSelect の呼び出しは、 EditPageForm での呼び出しと非常に似たものになります。

    function CreatePageForm( { onCancel, onSaveFinished } ) {
    	// ...
    	const { lastError, isSaving } = useSelect(
    		( select ) => ( {
    			// Notice the missing pageId argument:
    			lastError: select( coreDataStore )
    				.getLastEntitySaveError( 'postType', 'page' ),
    			// Notice the missing pageId argument
    			isSaving: select( coreDataStore )
    				.isSavingEntityRecord( 'postType', 'page' ),
    		} ),
    		[]
    	);
    	// ...
    	return (
    		<PageForm
    			{ /* ... */ }
    			lastError={ lastError }
    			isSaving={ isSaving }
    		/>
    	);
    }
    
    

    以上です ! 新しいフォームは以下のようになります。

    Top ↑

    すべてを一つに

    この章で構築したすべてを一つにまとめます。

    function CreatePageForm( { onCancel, onSaveFinished } ) {
    	const [title, setTitle] = useState();
    	const { lastError, isSaving } = useSelect(
    		( select ) => ( {
    			lastError: select( coreDataStore )
    				.getLastEntitySaveError( 'postType', 'page' ),
    			isSaving: select( coreDataStore )
    				.isSavingEntityRecord( 'postType', 'page' ),
    		} ),
    		[]
    	);
    
    	const { saveEntityRecord } = useDispatch( coreDataStore );
    	const handleSave = async () => {
    		const savedRecord = await saveEntityRecord(
    			'postType',
    			'page',
    			{ title, status: 'publish' }
    		);
    		if ( savedRecord ) {
    			onSaveFinished();
    		}
    	};
    
    	return (
    		<PageForm
    			title={ title }
    			onChangeTitle={ setTitle }
    			hasEdits={ !!title }
    			onSave={ handleSave }
    			lastError={ lastError }
    			onCancel={ onCancel }
    			isSaving={ isSaving }
    		/>
    	);
    }
    
    function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
    	const { page, lastError, isSaving, hasEdits } = useSelect(
    		( select ) => ( {
    			page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
    			lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId ),
    			isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
    			hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
    		} ),
    		[pageId]
    	);
    
    	const { saveEditedEntityRecord, editEntityRecord } = useDispatch( coreDataStore );
    	const handleSave = async () => {
    		const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
    		if ( savedRecord ) {
    			onSaveFinished();
    		}
    	};
    	const handleChange = ( title ) => editEntityRecord( 'postType', 'page', page.id, { title } );
    
    	return (
    		<PageForm
    			title={ page.title }
    			onChangeTitle={ handleChange }
    			hasEdits={ hasEdits }
    			lastError={ lastError }
    			isSaving={ isSaving }
    			onCancel={ onCancel }
    			onSave={ handleSave }
    		/>
    	);
    }
    
    function PageForm( { title, onChangeTitle, hasEdits, lastError, isSaving, onCancel, onSave } ) {
    	return (
    		<div className="my-gutenberg-form">
    			<TextControl
    				label="Page title:"
    				value={ title }
    				onChange={ onChangeTitle }
    			/>
    			{ lastError ? (
    				<div className="form-error">Error: { lastError.message }</div>
    			) : (
    				false
    			) }
    			<div className="form-buttons">
    				<Button
    					onClick={ onSave }
    					variant="primary"
    					disabled={ !hasEdits || isSaving }
    				>
    					{ isSaving ? (
    						<>
    							<Spinner/>
    							Saving
    						</>
    					) : 'Save' }
    				</Button>
    				<Button
    					onClick={ onCancel }
    					variant="tertiary"
    					disabled={ isSaving }
    				>
    					Cancel
    				</Button>
    			</div>
    		</div>
    	);
    }
    
    

    あとはページを更新してフォームを楽しむだけです。

    Top ↑

    次のステップ

    原文

    最終更新日: