
inertia.js(react) + survey.js - 6: 設定の詳細変更(json連携)
さてさてさて、今surveyの構造を今一度見てみよう。
Schema::create('surveys', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description')->nullable();
$table->json('settings')->nullable();
$table->timestamps();
});
このsettingsで設定を変更できるようになっているんだけど、未使用である。これはsurvey.jsの設定が投入される事を想定している。たとえば現在

Nextが示すように2ページでの表示になっている。これはPage構造をそのようにしているからなんだけど、1ページに全部表示したいという事も可能なわけだ。questionsOnPageMode という設定に見ることができる

まずは手動で確認してみる
今previewで表示しているので、ここに無理矢理ハードコードしてつっこんでみよう。
public function show(Request $request, Survey $survey, SurveyService $surveyService): Response
{
$pagesData = $surveyService->getSurveyData($survey);
$surveyData = [
'questionsOnPageMode' => 'singlePage',
'pages' => $pagesData,
];
$surveyData = json_encode($surveyData, JSON_UNESCAPED_UNICODE);
$responseCount = 0;
if ($previewData = $request->session()->get('preview_data')) {
$responseCount = count($previewData);
}
return Inertia::render('Surveys/Show', [
'surveyModel' => $survey,
'surveyData' => $surveyData,
'responseData' => $previewData,
'readOnly' => $responseCount ? true : false,
]);
}

このように、page配列と同列に設定を与えるとそれを食ってくれる。なお、
"singlePage" - Combines all survey pages into a single page.
"questionPerPage" - Creates a separate page for every question.
"standard" (default) - Retains the original structure specified in the JSON schema.
ってことなんだけど、変な値を食わせたらdefaultに落ちるみたいだからそこは気にしなくてもいいっぽい。
設定インタフェース
とりあえずEditだけベタっと付けてみよう
export default function SurveyEdit({ auth, survey }) {
const { t } = useLaravelReactI18n();
const {
data, setData, put, errors, processing, recentlySuccessful,
} = useForm({
title: survey.title,
description: survey.description,
settings: '',
});
const updateSetting = (key, value) => {
setData('settings', {
...data.settings,
[key]: value,
});
};
...
<div className="mt-3">
<InputLabel htmlFor="questionsOnPageMode" value="questionsOnPageMode" />
<select
id="questionsOnPageMode"
value={data.settings?.questionsOnPageMode || 'standard'}
onChange={(e) => updateSetting('questionsOnPageMode', e.target.value)}
>
<option value="singlePage">singlePage</option>
<option value="questionPerPage">questionPerPage</option>
<option value="standard">standard</option>
</select>
</div>
こんな感じで付けると

こんな感じになるから、これを保存する
public function update(Request $request, Survey $survey): RedirectResponse
{
$data = $request->all();
$survey->update($data);
return redirect(route('surveys.index', $survey))
->with(['success' => __('Survey Settings Updated')]);
}
class Survey extends Model
{
use HasFactory;
protected $fillable = [
'title',
'description',
'settings',
];
> Survey::find(2)
[!] Aliasing 'Survey' to 'App\Models\Survey' for this Tinker session.
= App\Models\Survey {#7496
id: 2,
title: "あああ",
description: "あ",
settings: "{"questionsOnPageMode": "questionPerPage"}",
created_at: "2023-10-02 12:38:58",
updated_at: "2023-10-02 13:22:14",
}
ハードコードしてたところを直す
public function show(Request $request, Survey $survey, SurveyService $surveyService): Response
{
$pagesData = $surveyService->getSurveyData($survey);
$settings = $survey->settings;
$surveyData = [
'pages' => $pagesData,
];
if ($settings) {
$surveyData = array_merge($surveyData, $settings);
}
$surveyData = json_encode($surveyData, JSON_UNESCAPED_UNICODE);
$responseCount = 0;
if ($previewData = $request->session()->get('preview_data')) {
$responseCount = count($previewData);
}
return Inertia::render('Surveys/Show', [
'surveyModel' => $survey,
'surveyData' => $surveyData,
'responseData' => $previewData,
'readOnly' => $responseCount ? true : false,
]);
}
さらには編集モードで設定があるときはそれをデフォルトにする
const {
data, setData, put, errors, processing, recentlySuccessful,
} = useForm({
title: survey.title,
description: survey.description,
settings: survey.settings,
});
これで、とりあえずは完了ではある。
リファクタリングの検討
このままの勢いでインタフェースを増やしたりするとコードがどんどん増えてしまってよろしくない
<div className="mt-3">
<InputLabel htmlFor="questionsOnPageMode" value="questionsOnPageMode" />
<select
id="questionsOnPageMode"
value={data.settings?.questionsOnPageMode || 'standard'}
onChange={(e) => updateSetting('questionsOnPageMode', e.target.value)}
>
<option value="singlePage">singlePage</option>
<option value="questionPerPage">questionPerPage</option>
<option value="standard">standard</option>
</select>
</div>
ハードコードしてるのはこの部分くらいなので、これを何とかしてみよう
コンポーネント化
<div className="mt-3">
<SurveySelectSetting
settingKey="questionsOnPageMode"
value={data.settings?.questionsOnPageMode}
onChange={updateSetting}
options={["singlePage", "questionPerPage", "standard"]}
/>
</div>
みたいにしておいて
resources/js/Components/SurveySelectSetting.jsx
import InputLabel from '@/Components/InputLabel';
export default function SurveySelectSetting({ settingKey, value, onChange, options }) {
return (
<>
<InputLabel htmlFor={settingKey} value={settingKey} />
<select
id={settingKey}
value={value || 'standard'}
onChange={(e) => onChange(settingKey, e.target.value)}
>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</>
);
}
を作る。これで一応の解決は見られる
controllerからデーターの注入
このviewのハードコードをさらに抽象化する
public function edit(Survey $survey, SurveyService $surveyService): Response
{
$surveyMetaData = $surveyService->getSettingsMetadata();
return Inertia::render('Surveys/Edit', [
'survey' => $survey,
'surveyMetaData' => $surveyMetaData,
]);
}
このようにメタデーターを受信できるようにする。メタデーターの構造体自体はサービスに定義しといたらいいだろう。
app/Services/SurveyService.php
public function getSettingsMetadata(): array
{
return [
'questionsOnPageMode' => [
'type' => 'select',
'options' => ['singlePage', 'questionPerPage', 'standard'],
'default' => 'standard',
'label' => __('Question On Page Mode'),
],
];
}
あとはコンポーネントをちょっと変更して
resources/js/Components/SurveySelectSetting.jsx
import InputLabel from '@/Components/InputLabel';
export default function SurveySelectSetting({ settingKey, value, onChange, options, label }) {
return (
<>
<InputLabel htmlFor={settingKey} value={label || settingKey} />
<select
id={settingKey}
value={value || 'standard'}
onChange={(e) => onChange(settingKey, e.target.value)}
>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</>
);
}
Edit.jsxも以下のように変更する
{Object.entries(surveyMetaData).map(([settingKey, metaData]) => {
const { type, options, default: defaultValue, label } = metaData;
switch (type) {
case 'select':
return (
<div className="mt-3" key={settingKey}>
<SurveySelectSetting
settingKey={settingKey}
value={data.settings?.[settingKey] || defaultValue}
onChange={updateSetting}
options={options}
label={label}
/>
</div>
);
default:
return null;
}
})}
設定の追加
ここまでのプログラミングがよくわかんくてもメンテナンスはここから行うので大丈夫っちゃ大丈夫。
まず設定したい項目を探して必ずapiドキュメントを引く。ここではshowQuestionNumbers の設定を追加してみよう

public function getSettingsMetadata(): array
{
return [
'questionsOnPageMode' => [
'type' => 'select',
'options' => ['singlePage', 'questionPerPage', 'standard'],
'default' => 'standard',
'label' => __('Question On Page Mode'),
],
'showQuestionNumbers' => [
'type' => 'select',
'options' => ['on', 'onpage', 'off'],
'default' => 'on',
'label' => __('Show Question Numbers'),
],
];
}
このように増やすと…

こうなる。あとは正しく動作する事を確認すればよい

TODO的なやつ
Create.jsxとEdit.jsxは共通化できる
form request
設定項目の多言語化
プレビューのデーターストアがバグっている
次回
管理機能は一通り終わったのでユーザーがsurveyを受ける機能を作っていこう。これはユーザーがログインした後のdashboardにとりあえず表示すればokであろう