Next.jsで作成したフォームの投稿をPowerCMS Xのフォーム機能で管理する

PowerCMS X Advent Calendar 2021」の2日目です。当サイトはPowerCMS XNext.jsを利用し、JamstackでWebサイト構築を実現するための研究・開発を目的とした個人サイトです。

当サイトでも開設当初から「問い合わせフォーム」を用意しています。「Decoupling」の原則もふまえどのサービスを使おうかと考えたのですが、APIを利用してPowerCMS Xのフォーム機能を利用するのが現状最も便利なのでは?と考えました。開設時は単純にフォームの入力内容を送信するだけでしたが、Reactの知識も少し深まり現在はエラーメッセージの表示もできるようになりました。

本稿ではNext.jsやAPIを利用し、PowerCMS Xのフォーム機能を利用する手順などを解説します。

PowerCMS X管理画面での事前準備

まずPowerCMS Xの管理画面にて、入力してほしい内容を1つ1つ設定する「設問」、そして設問をグループ化した「フォーム」を準備します。ドキュメントが「フォームの作成 | PowerCMS X」にありますので、それに沿って設定を行ってください。ただし、設問のビューは何も設定する必要はありません。(後ほどReactで記述します。)ベースネームがinput要素のname属性値になることに留意してください。

フォーム送信時に管理者とフォーム送信者にメールを送りたいので、PowerCMS Xのビューを書きました。以下が管理者宛メールのサンプルです。PAML3を有効にしTwig風な記述をしています。

PowerCMS X R&D Websiteにて問い合わせを受け付けました。

受付番号:
{{contact_id}}
{% for param in post_params %}
{{param.post_question}} :
{{param.post_value}}
{% endfor %}

-- 
PowerCMS X R&D Website
https://rd.powercmsx.anothersky.jp/

pages/api/contact.jsの作成

次にNext.jsのAPIルート機能でフロントエンドとPowerCMS XのAPIとの間をつなぐ機能を実装します。フロントエンドから/api/contactにフォームの内容を送信するとそれをPowerCMS XのAPIに送信し、結果をフロントエンドに返すような動作をします。これによりPowerCMS XのURLを隠すことができますし、設定にもよりますがCORSのエラーも発生しません。

clientは以前「PTRESTfulAPIClientの使用方法」で紹介したもので、submitContactメソッドにてフォームのPOSTが実行できます。フォームの入力内容はreq.bodyにあるのでそれを利用します。入力内容にエラーがあった場合はPowerCMS XのAPIからのレスポンスerrorsにベースネームとエラーメッセージが入るので、後ほど表示に利用します。

import { client } from '../../libs/client';
import { formId, workspaceId, defaultErrors } from '../../constants/form';

export default async function handler(req, res) {
  if(req.method !== 'POST') {
    res.status(405).json({
      errors: 'Method not allowed.',
    });
    return;
  }

  const data = {
    name:       req.body.name,
    email:      req.body.email,
    subject:    req.body.subject,
    content:    req.body.content,
    MagicToken: req.body.token,
    have_used_pcmsx: req.body.have_used_pcmsx,
  };

  const submitResponse = await client.submitContact(formId, workspaceId, data);
  const submitObject = await submitResponse.json();
  if (submitObject.Success) {
    res.status(200).json({
      result: 'success',
    });
    return;
  }

  res.status(400).json({
    result:   'error',
    messages: submitObject.message ? submitObject.message : submitObject.messages,
    errors:   submitObject.errors ? submitObject.errors : defaultErrors,
  });
};

フォームのビューなどを実装する

次にpages/form.jsを実装していきます。まずはビューにあたるJSXやフォームの送信時に必要なトークンを取得するコードなどを実装します。まだまだ勉強中ですが「MUI: The React component library you always wanted」を利用し必要な入力項目分フィールドを書いていきました。MUIはerrorプロパティにエラーの有無を、helperTextにエラーメッセージを渡すとよしなに表示してくれるようです。

後ほどステートフックでmessagesにエラーメッセージを格納するので、messagesに内容がある場合はそれを列挙するようにしておきます。送信処理中を示すloadingもステートフックで状態をセットします。

import * as React from 'react';
import Head from 'next/head';
import {
  Typography,
  List,
  ListItem,
  ListItemText,
  Box,
  TextField,
  Button,
  CircularProgress,
  FormControl,
  FormControlLabel,
  FormLabel,
  RadioGroup,
  Radio
} from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
import { usePostContact } from '../hooks/usePostContact';
import { client } from '../libs/client';
import { formId, workspaceId } from '../constants/form';

export default function Form({ token }) {
  const { postContact, loading, messages, errors } = usePostContact();  // 後ほど実装します

  return (
    <>
      <Typography component="h1">
        お問い合わせ
      </Typography>
      <Typography variant="body1">
        当サイトに関するご質問・お問い合わせは下記フォームをご利用ください。
      </Typography>
      {messages.length && (
        <div id="error" role="alert" tabIndex={-1}>
          <Typography component="h2">
            エラーが発生しました
          </Typography>
          <List>
            {messages.map((message, index) => {
              return(
                <ListItem key={index}>
                  <ListItemText primary={message} />
                </ListItem>
              )
            })}
          </List>
        </div>
      )}
      <Box
        component="form"
        onSubmit={postContact}
      >
        <TextField
          required
          fullWidth
          id="name"
          label="お名前"
          error={errors.name ? true : false}
          helperText={errors.name}
        />
        <TextField
          required
          fullWidth
          type="email"
          id="email"
          label="E-mail"
          error={errors.email ? true : false}
          helperText={errors.email}
        />
        <TextField
          required
          fullWidth
          id="subject"
          label="件名"
          error={errors.subject ? true : false}
          helperText={errors.subject}
        />
        <TextField
          required
          fullWidth
          multiline
          rows={4}
          type="textarea"
          id="content"
          label="お問い合わせ内容"
          error={errors.content ? true : false}
          helperText={errors.content}
        />
        <div>
          <FormControl component="fieldset">
            <FormLabel component="legend">PowerCMS Xの利用経験</FormLabel>
            <RadioGroup
              row
              aria-label="PowerCMS Xの利用経験"
              defaultValue="あり"
              name="have_used_pcmsx"
            >
              <FormControlLabel value="あり" control={<Radio />} label="あり" />
              <FormControlLabel value="なし" control={<Radio />} label="なし" />
            </RadioGroup>
          </FormControl>
        </div>
        <TextField type="hidden" id="token" value={token} sx={{ display: 'none' }} />
        {loading && (
          <Box>
            <CircularProgress />
          </Box>
        )}
        <Button type="submit" variant="contained" endIcon={<SendIcon />}>
          送信する
        </Button>
      </Box>
    </>
  )
}

export async function getServerSideProps() {
  const response = await client.getContactToken(formId, workspaceId);
  const tokenObject = await response.json();
  const token = tokenObject.magic_token;

  return {
    props: {
      token,
    },
  }
}

フォームのロジックをカスタムフックで実装する

続いてhooks/usePostContact.jsにフォームをsubmitした時に呼び出されるpostContactメソッドやステートの処理を実装します。

postContactメソッドは先に実装したpages/api/contact.jsに対してフォームの入力内容を送信する処理、またレスポンスに応じてサンクス画面に遷移させたり、ステートフックでmessageserrorsにエラーの情報を格納したりする処理を記述します。loadingに真偽値を設定し送信処理中にCircularProgress(くるくる回って処理中であることを示すSVG)を表示する工夫もしました。

import Router from 'next/router';
import { useState } from 'react';
import { defaultErrors } from '../constants/form';

export function usePostContact() {
  const [loading, setLoading]   = useState(false);
  const [messages, setMessages] = useState([]);
  const [errors, setErrors]     = useState(defaultErrors);

  const postContact = async event => {
    event.preventDefault();
    setLoading(true);

    const data = {
      name:    event.target.name.value,
      email:   event.target.email.value,
      subject: event.target.subject.value,
      content: event.target.content.value,
      token:   event.target.token.value,
      have_used_pcmsx: event.target.have_used_pcmsx.value,
    };
    const response = await fetch('/api/contact', {
      body: JSON.stringify(data),
      headers: {
        'Content-Type': 'application/json'
      },
      method: 'POST'
    });

    if (response.status === 200) {
      Router.push('/form_send');
      return;
    }

    const result = await response.json()
    setLoading(false);
    setMessages(Array.isArray(result.messages) ? result.messages : [result.messages]);
    setErrors(result.errors);
    document.getElementById('error').focus();
  };

  return { postContact, loading, messages, errors };
}

フォームの動作確認

フォームに入力をして送信が完了するとPowerCMS Xの管理画面で投稿が確認できます。
フォームの投稿内容を表示した画面

またPowerCMS Xからメールが送信されます。
フォームの投稿内容を管理者に通知するメール

フォームの入力内容に誤りがある場合はエラーメッセージを表示します。
フォームに入力エラーがあった場合の表示

これはPowerCMS Xの機能ですが、フォームの投稿を選択した後集計を実行するとラジオボタンの項目等を集計して表示することができます。
フォームの投稿内容を集計した画面

まとめ

PTRESTfulAPIClientを利用することでPowerCMS Xのフォーム機能を容易に利用でき、コンテンツのみならず閲覧者からの投稿も管理できることが分かりました。APIを利用した実装の参考になれば幸いです。

繰り返しになりますが「Decoupling」の原則は読みました。よりよいフォームを提供するサービス等があればAPIルートを書き換えてそれに差し替えれば良いのです。