見出し画像

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));

なんかで確認できる

getSurveySetting()の呼出に成功している

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>
  );

的な。


左寄がいいんだかわるいんだかはまあ考えてみてください


次回

今急いで構造を作ったので結構ボロボロっすね。次回は諸々修正していくぞっと。


この記事が気に入ったらサポートをしてみませんか?