inertia.js(react) + survey.js - 4: surveyプレビュー
ここでは前段までに作成されたsurvey.jsをpreviewしてゆく。
プレビューっちゅーことはsurvey.js本体の発動ということなんで、これはある程度本番に近い。
サービスクラスの配置
細々としたことはサービスに配置するのがベストプラクティスとされているつまり、コントローラーが太くなってきたらサービスへの移動を考える、んだけど、ここでは最初から作ってしまう。
app/Services ディレクトリを作成し、app/Services/SurveyService.php を作る。そして以下のようにsurveyの設定を受けとるダミーを作っておこう。
<?php
namespace App\Services;
use App\Models\Survey;
class SurveyService
{
public function getSurveySetting(Survey $survey)
{
return "setting";
}
}
続いて app/Http/Controllers/SurveyController.php からこのサービスを呼び出すテストを行っていく。
use App\Services\SurveyService;
を冒頭に書いておいての
public function show(Survey $survey, SurveyService $surveyService): Response
{
dd($surveyService->getSurveySetting($survey));
なんかで確認できる
pageデーターの取得もサービスにやらっせる
pageとはsurveyの直下にある構造の概念であーる
現状のSurveyPageデーターは以下の通りだった
[!] Aliasing 'SurveyPage' to 'App\Models\SurveyPage' for this Tinker session.
= Illuminate\Database\Eloquent\Collection {#7490
all: [
App\Models\SurveyPage {#7492
id: 1,
survey_id: 1,
name: "基本情報",
title: "",
description: "",
created_at: "2023-10-01 20:06:53",
updated_at: "2023-10-01 20:06:53",
},
App\Models\SurveyPage {#7493
id: 2,
survey_id: 1,
name: "職業と収入",
title: "",
description: "",
created_at: "2023-10-01 20:06:53",
updated_at: "2023-10-01 20:06:53",
},
],
}
ご覧のようにsurvay_idを持っており、surveyと紐付くようにできているから、リレーションを書いておく必要がある
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Survey extends Model
{
use HasFactory;
protected $fillable = [
'title',
'description',
];
protected $casts = [
'settings' => 'array',
];
public function pages(): HasMany
{
return $this->hasMany(SurveyPage::class);
}
}
これでsurveyオブジェクトからpages()メソッドを叩くとこのページデーターが出てくることだろう。
で、このpageデーターを元にグルグル回してelementとchoiceをひっつける。ちなみにpageとelementにはリレーションが設定済みである
SurvePageと
// app/Models/SurveyPage.php
public function elements(): HasMany
{
return $this->hasMany(SurveyElement::class);
}
SurveyElementである
// app/Models/SurveyElement.php
public function choices(): HasMany
{
return $this->hasMany(SurveyElementChoice::class);
}
この状態で
$pagesData = $pages->map(function ($page) {
$elements = $page->elements->map(function ($element) {
$choices = $element->choices->map(function ($choice) {
return [
'value' => $choice->id,
'text' => $choice->choice,
];
})->all();
return [
'type' => $element->type,
'name' => (string)$element->id,
'title' => $element->title,
'isRequired' => $element->is_required,
'choices' => $choices,
];
})->all();
return [
'name' => $page->name,
'elements' => $elements,
];
});
dd($pagesData);
こんな風にしてあげると
このような構造体になるんだけど、このロジックをコントローラーに置くでなく、先程のサービスに配置する。これをgetServiceData()という関数にしよう。
app/Services/SurveyService.php
<?php
namespace App\Services;
use App\Models\Survey;
use Illuminate\Support\Collection;
class SurveyService
{
public function getSurveyData(Survey $survey): Collection
{
$pages = $survey->pages()->with(['elements.choices'])->get();
$pagesData = $pages->map(function ($page) {
$elements = $page->elements->map(function ($element) {
$choices = $element->choices->map(function ($choice) {
return [
'value' => $choice->id,
'text' => $choice->choice,
];
})->all();
return [
'type' => $element->type,
'name' => (string)$element->id,
'title' => $element->title,
'isRequired' => $element->is_required,
'choices' => $choices,
];
})->all();
return [
'name' => $page->name,
'elements' => $elements,
];
});
return $pagesData;
}
そうすれば
public function show(Survey $survey, SurveyService $surveyService): Response
{
dd($surveyData = $surveyService->getSurveyData($survey));
exit;
このようにコントローラーから簡単に取得できる
データーの組立
最終的にこの取得したデーターをpagesに渡してjson化すればok
public function show(Survey $survey, SurveyService $surveyService): Response
{
$pagesData = $surveyService->getSurveyData($survey);
$surveyData = [
'pages' => $pagesData,
];
$surveyData = json_encode($surveyData, JSON_UNESCAPED_UNICODE);
なんだけど、回答データーがあった場合はそれも表示する必要がある。しかし今のところはこれはモックにしておこう。最終的にはこのようなコントローラーとなった。
public function show(Survey $survey, SurveyService $surveyService): Response
{
$pagesData = $surveyService->getSurveyData($survey);
$surveyData = [
'pages' => $pagesData,
];
$surveyData = json_encode($surveyData, JSON_UNESCAPED_UNICODE);
$responseData = null;
$responseCount = 0;
return Inertia::render('Surveys/Show', [
'surveyModel' => $survey,
'surveyData' => $surveyData,
'responseData' => $responseData,
'readOnly' => $responseCount ? true : false,
]);
}
survey.jsのinstall
さてさて、こっちはフロントエンドであーる。つまりsurvey.jsの本体だ。やる事がいろいろあって忙しいけど現代のweb作りはこんなもんだから。
npmパッケージが無いと何もできないので
npm install survey-react survey-react-ui
preview保存用route
Route::resource('surveys', SurveyController::class);
Route::post('surveys/{survey}/preview', [SurveyController::class, 'previewStore'])->name('surveys.previewStore');
とりあえずこんな感じでpreviewにpostしたらプレビューデーターを保存するという仕様にした。
Show.jsx
import React, { useCallback } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import 'survey-core/defaultV2.min.css';
import { Model } from 'survey-core';
import { Survey } from 'survey-react-ui';
import { Head, router } from '@inertiajs/react';
export default function SurveyShow({
auth, surveyModel, surveyData, responseData, readOnly,
}) {
const survey = new Model(surveyData);
if (readOnly) {
survey.mode = 'display';
}
const surveyComplete = useCallback((sender) => {
router.post(route('surveys.previewStore', surveyModel.id), sender.data);
}, []);
survey.onComplete.add(surveyComplete);
survey.data = responseData;
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Survey</h2>}
>
<Head title="Survey" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<Survey model={survey} />
;
</div>
</div>
</AuthenticatedLayout>
);
}
殺風景といえば殺風景なのである。readOnlyに関しては実はControllerから渡してやらなくてもいいのかもしれないが、まあ現状では関係ないといえばそう。
これで
こうなるはずだ。タイトルの設定はおおいにTODOである
でpreviewStoreを作って
public function previewStore(Request $request)
{
dd($request->all());
}
とすれば、送信後に
などとなるだろう。ここでpreviewデーターの保存を行う
プレビューデーターの保存と表示
これは本番では実際にはDBに書き込むのであるが、管理者のプレビューデーターなんてのは消滅してくれた方がいいくらいなわけだからセッションに保存する。
public function previewStore(Request $request, Survey $survey): RedirectResponse
{
$request->session()->put('preview_data', $request->all());
return redirect(route('surveys.show', $survey))
->with(['success' => __('Preview data received')]);
}
保存はとくに何も考えていない
これを取り出す場合
public function show(Request $request, Survey $survey, SurveyService $surveyService): Response
{
$pagesData = $surveyService->getSurveyData($survey);
$surveyData = [
'pages' => $pagesData,
];
$surveyData = json_encode($surveyData, JSON_UNESCAPED_UNICODE);
$previewData = $request->session()->get('preview_data');
$responseCount = count($previewData);
return Inertia::render('Surveys/Show', [
'surveyModel' => $survey,
'surveyData' => $surveyData,
'responseData' => $previewData,
'readOnly' => $responseCount ? true : false,
]);
}
これだけで正しく動作するはずだ
このように送信された値がただしく取れている。
セッションの破棄
プレビューデーターを破棄したい場合も当然あるだろうから、その場合のrouteも作ってみよう。
routes/web.php
Route::resource('surveys', SurveyController::class);
Route::post('surveys/{survey}/preview', [SurveyController::class, 'previewStore'])->name('surveys.previewStore');
Route::delete('surveys/{survey}/preview', [SurveyController::class, 'previewDestroy'])->name('surveys.previewDestroy');
破棄ルート
public function previewDestroy(Request $request, Survey $survey)
{
$request->session()->forget('preview_data');
return redirect(route('surveys.show', $survey))
->with(['success' => __('Preview data destroyed')]);
}
これは単純にsessionデストロイしているだけである。laravelではsession()->forgot()というようだ
resources/js/Pages/Surveys/Show.jsx
const handleDelete = () => {
router.delete(route('surveys.previewDestroy', surveyModel.id));
};
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Survey</h2>}
>
<Head title="Survey" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
{readOnly && (
<div className="text-right mb-4">
<DangerButton onClick={() => handleDelete()}>
<VscTrash className="mr-2" /> {t('Delete Preview Data')}
</DangerButton>
</div>
)}
<Survey model={survey} />
</div>
</div>
</AuthenticatedLayout>
);
的な。
左寄がいいんだかわるいんだかはまあ考えてみてください
次回
今急いで構造を作ったので結構ボロボロっすね。次回は諸々修正していくぞっと。