surveyの設定を変更する

さすがにこの記事は長くなりすぎてるから、推敲したのを後々マガジンにしていきますね

もう結構ディープになってきたね!

{surveys.map((survey, index) => (
    <div key={index} className="bg-white rounded-lg shadow-lg p-4">
        <div className="flex justify-between items-center mb-2">
            <h4 className="text-lg font-semibold">{survey.title}</h4>
            <Link href={route("surveys.edit", survey.id)}>
                <button className="bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-1 px-2 rounded text-xs">
                    Edit settings
                </button>
            </Link>
        </div>


        <p className="text-sm text-gray-700 mb-3">{survey.description}</p>
        {survey.hasResponse ? (
            <Link href={route("surveys.show", survey.id)}>
                <button className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
                    View Your Response
                </button>
            </Link>
        ) : (
            <Link href={route("surveys.show", survey.id)}>
                <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
                    Take Survey
                </button>
            </Link>
        )}
    </div>
))}

こんな感じでEdit settingsボタンを出す

もっとも、一般ユーザーがSurveyの設定変更をやるのはちょっとユースケースとしては考え辛いので、最終的にマトモなアプリにするならここは管理者的な人間がやる事になるだろう。いずれにせよ今はとりあえず置いておこう。

まあともかくeditを考える。これはControllerである。

    public function edit(Survey $survey)
    {
        return Inertia::render('Surveys/Edit', [
            'survey' => $survey,
        ]);
    }

編集フォーム

編集に関してはProfile/Edit.jsxを参考にするといいだろうから、まずコピペするけど、これはPartialize(分割)されており、このままでは動作しない

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
{/*
import DeleteUserForm from './Partials/DeleteUserForm';
import UpdatePasswordForm from './Partials/UpdatePasswordForm';
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
*/}
import { Head } from '@inertiajs/react';

export default function Edit({ auth, mustVerifyEmail, status }) {
    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Profile</h2>}
        >
            <Head title="Profile" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
                {/*
                    <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                        <UpdateProfileInformationForm
                            mustVerifyEmail={mustVerifyEmail}
                            status={status}
                            className="max-w-xl"
                        />
                    </div>

                    <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                        <UpdatePasswordForm className="max-w-xl" />
                    </div>

                    <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                        <DeleteUserForm className="max-w-xl" />
                    </div>
            */}
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

面倒なところをコメントで削りこんだ

これは3つのフォームで構築されているようなので(Inertiaってか非同期をつかうと従来型のbladeではできなかったような三連フォームみたいなのも簡単にできますよ) updatepasswordとdeleteuserは抜いとこう

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
{/*
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
*/}
import { Head } from '@inertiajs/react';

export default function Edit({ auth, mustVerifyEmail, status }) {
    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Profile</h2>}
        >
            <Head title="Profile" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
                    <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                {/*
                        <UpdateProfileInformationForm
                            mustVerifyEmail={mustVerifyEmail}
                            status={status}
                            className="max-w-xl"
                        />
            */}
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

そしたらPartializeされているところを埋めちゃう

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';

export default function Edit({ auth, mustVerifyEmail, status }) {
    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Profile</h2>}
        >
            <Head title="Profile" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
                    <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                        {/*

            <form onSubmit={submit} className="mt-6 space-y-6">
                <div>
                    <InputLabel htmlFor="name" value="Name" />

                    <TextInput
                        id="name"
                        className="mt-1 block w-full"
                        value={data.name}
                        onChange={(e) => setData('name', e.target.value)}
                        required
                        isFocused
                        autoComplete="name"
                    />

                    <InputError className="mt-2" message={errors.name} />
                </div>

                <div>
                    <InputLabel htmlFor="email" value="Email" />

                    <TextInput
                        id="email"
                        type="email"
                        className="mt-1 block w-full"
                        value={data.email}
                        onChange={(e) => setData('email', e.target.value)}
                        required
                        autoComplete="username"
                    />

                    <InputError className="mt-2" message={errors.email} />
                </div>

                {mustVerifyEmail && user.email_verified_at === null && (
                    <div>
                        <p className="text-sm mt-2 text-gray-800">
                            Your email address is unverified.
                            <Link
                                href={route('verification.send')}
                                method="post"
                                as="button"
                                className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                            >
                                Click here to re-send the verification email.
                            </Link>
                        </p>

                        {status === 'verification-link-sent' && (
                            <div className="mt-2 font-medium text-sm text-green-600">
                                A new verification link has been sent to your email address.
                            </div>
                        )}
                    </div>
                )}

                <div className="flex items-center gap-4">
                    <PrimaryButton disabled={processing}>Save</PrimaryButton>

                    <Transition
                        show={recentlySuccessful}
                        enter="transition ease-in-out"
                        enterFrom="opacity-0"
                        leave="transition ease-in-out"
                        leaveTo="opacity-0"
                    >
                        <p className="text-sm text-gray-600">Saved.</p>
                    </Transition>
                </div>
                */}


                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

不慣れなものはこのように人のコードをベースに構築するものである。

formとsubmitを組み立てる

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';

import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import { Link, useForm, usePage } from '@inertiajs/react';

export default function Edit({ auth, mustVerifyEmail}) {
    const survey = usePage().props.survey;

    const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({
    });

    const submit = (e) => {
        e.preventDefault();

        patch(route('profile.update'));
    };
    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Edit Survey</h2>}
        >
            <Head title="Profile" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
                    <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                        <form onSubmit={submit} className="mt-6 space-y-6">
                            <div className="flex items-center gap-4">
                                <PrimaryButton disabled={processing}>Save</PrimaryButton>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

コピペの嵐でここまできた、さらにrouteを変更し、textを作って初期値にsurveysからtitleを挿入すると

import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import { Link, useForm, usePage } from '@inertiajs/react';

export default function Edit({ auth, mustVerifyEmail}) {
    const survey = usePage().props.survey;

    const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({
        'title': survey.title,
    });

    const submit = (e) => {
        e.preventDefault();

        patch(route('surveys.update'));
   };
    return (
        <AuthenticatedLayout
        user={auth.user}
        header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Edit Survey</h2>}
    >
            <Head title="Profile" />

            <div className="py-12">
                <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
                    <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
                       <div>
                            <InputLabel htmlFor="title" value="title" />

                            <TextInput
                                id="title"
                                className="mt-1 block w-full"
                                value={data.title}
                                onChange={(e) => setData('title', e.target.value)}
                                required
                                isFocused
                            />

                            <InputError className="mt-2" message={errors.title} />
                        </div>

                        <form onSubmit={submit} className="mt-6 space-y-6">
                            <div className="flex items-center gap-4">
                                <PrimaryButton disabled={processing}>Save</PrimaryButton>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

こうもなる。この時点でSurveyモデルにfillableを与えて

class Survey extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
    ];

controllerで更新すれば

use Illuminate\Http\RedirectResponse;

    public function update(Request $request, Survey $survey): RedirectResponse
    {
        $data = $request->all();
        $survey->update($data);
        return redirect(route('surveys.index'))
            ->with(['success' => 'Survey updated'])
        ;
    }


title程度はあっさり変更できるようになる

でまあ、ここにもbreadcrumbsが出るようにroutes/breadcrumbs.phpも更新

use App\Models\Survey;
Breadcrumbs::for('surveys.index', function(BreadcrumbTrail $trail)
{
    $trail->push(__('Surveys'), route('surveys.index'));
});
Breadcrumbs::for('surveys.show', function (BreadcrumbTrail $trail, Survey $survey) {
    $trail->parent('surveys.index');
    $trail->push($survey->title, route('surveys.show', $survey));
});
Breadcrumbs::for('surveys.edit', function (BreadcrumbTrail $trail, Survey $survey) {
    $trail->parent('surveys.index');
    $trail->push(__('Edit'). ': '. $survey->title, route('surveys.show', $survey));
});

まあこの辺の文字情報は適当にやってください


descriptionの更新

はっきりいったらこれはほぼ同じ手順でできるので諸々割愛。あとで調整しよう。textareaに関してはcomponentを置いといてもらえなかったみたいだし。

設定値の保存

今現在surveyのスキーマ設定は

    public function up(): void
    {
        Schema::create('surveys', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }

このようになっているから、これだけで十分なように見えるけど十分ではない。

というのも

        $elements = $survey->elements()->get()->map(function ($element) {
            return [
                'name'  => (string)$element->id,
                'title' => $element->title,
                'type'  => $element->type,
            ];
        })->toArray();
        $surveySetting = [
            'showQuestionNumbers' => 'off',
            'completedHtml'       => '',
            'elements'            => $elements,
        ];

        return Inertia::render('Surveys/Show', [
            'surveyModel'   => $survey,
            'surveySetting' => $surveySetting,
            'surveyData'    => $surveyData,
            'readOnly'      => $responseCount ? true : false,
        ]);

このようにハードコードされている部分があるからだ、これを変更するとなると、まあまあのもんである。たとえばshowQuestionNumbersに関してonとoffを切り替えたいという野心的な設計を考えたときどうだろう。

ここでsurveyjsの設定値を見てみると

おそろしい量のプロパティーを抱えており、これら全てtableのカラムを作るのははっきりいってそれはちょっと待てよって感じになるので、settingに関してはjsonで持っといた方がいいでしょう、多分。まあそれか好きな奴だけカラムにしてもいいかもしれないが

スキーマの変更

        Schema::create('surveys', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->json('settings')->nullable();
            $table->timestamps();
        });

json型で定義、で、初期値としてseedingする

まず、factory  
database/factories/SurveyFactory.php

class SurveyFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'title'       => $this->faker->sentence(),
            'description' => $this->faker->paragraph(),
            'settings'    => [],
        ];
    }
}

seed

    public function run(): void
    {
        \App\Models\Survey::factory()->create([
            'title' => 'デモ1',
            'description' => 'survey.js機能確認用',
            'settings' => ['showQuestionNumbers' => 'off'],
        ]);
    }

モデルでcast

    protected $casts = [
        'settings' => 'array',
    ];

で作りなおす

% ./vendor/bin/sail artisan migrate:fresh --seed

ハードコードしてる所を直す

        $surveySetting = [
//            'showQuestionNumbers' => 'off',
            'completedHtml'       => '',
            'elements'            => $elements,
        ];
        $surveySetting = array_merge($surveySetting, $survey->settings);

これで設定値をDBに置い出すことができたわけだ。

form

                        <div className="mt-3">
                            <InputLabel htmlFor="showQuestionNumbers" value="Show Question Numbers" />

                            <select
                                id="showQuestionNumbers"
                            >
                                <option value="on">On</option>
                                <option value="off">Off</option>

                            </select>
                        </div>

こんなのを追加する。ただ、現状では何もできないので

    const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({
        'title': survey.title,
        'description': survey.description,
        'settings': survey.settings,
    });

                            <select
                                id="showQuestionNumbers"
                                value={data.settings?.showQuestionNumbers || 'off'}
                            >
                                <option value="on">On</option>
                                <option value="off">Off</option>
                            </select>


このように増やし(オプショナル・チェイニング)てあげればとりあえずデフォルトの設定がjsonを見てくれるようになるだろう。

あとはonChangeのイベントが必要である。

<select
    id="showQuestionNumbers"
    value={data.settings?.showQuestionNumbers || 'off'}
        onChange={(e) => updateSetting('showQuestionNumbers', e.target.value)}
>
    <option value="on">On</option>
    <option value="off">Off</option>
</select>

updateSettingsという関数を使ってデーターをセットしている

    const updateSetting = (key, value) => {
        setData('settings', {
            ...data.settings,
            [key]: value
        });
    };

そうするとrequest dumpが

array:3 [▼ // app/Http/Controllers/SurveyController.php:112
  "title" => "デモ1"
  "description" => "survey.js機能確認用"
  "settings" => array:1 [▼
    "showQuestionNumbers" => "on"
  ]
]

こんな風味で取れるので、あとはつっこんでやるだけ。ちなみにvalidationは一切していない。へんなキーと値を送信するとsurvey.jsがぶっこわれる可能性もまあまああるが、所詮はsurvey.jsがぶっこわれるくらいなので、その場合は何とかしろって話ですね。

うまいこと保存できれば番号が付くだろう

さらに設定を増やす

このパターンは設定の増設に強いので、一つ増やしてみよう。

questionTitleLocation

まずapi docを参照する事

  • Default value: top

  • Accepted values: top , bottom , left

という事だから、まずdefault valueをseedで書いとく

class SurveySeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        \App\Models\Survey::factory()->create([
            'title' => 'デモ1',
            'description' => 'survey.js機能確認用',
            'settings' => [
                'showQuestionNumbers' => 'off',
                'questionTitleLocation' => 'top'
            ],
        ]);
    }
}

そしたらフォームを生やす

                        <div className="mt-3">
                            <InputLabel htmlFor="questionTitleLocation" value="Question Title Location" />

                            <select
                                id="questionTitleLocation"
                                value={data.settings?.questionTitleLocation || 'top'}
                                    onChange={(e) => updateSetting('questionTitleLocation', e.target.value)}
                            >
                                <option value="top">Top</option>
                                <option value="left">Left</option>
                                <option value="bottom">Bottom</option>
                            </select>
                        </div>

まあ実はこれだけなのです。Settingの値が正しければちゃんと動作する。これはleftにした例

とはいえ、やっぱり設定がおかしくなって微妙だわって場合のjsonリセットボタンなんかもあれば、それはそれでいいんだろうけど、まあ余裕があれば考えてみてくださいな。

次回

さすがにこの意味不明なアンケート要素じゃやっててもおもしろくないので、もう少し質問項目を揃えていこう。



いいなと思ったら応援しよう!