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リセットボタンなんかもあれば、それはそれでいいんだろうけど、まあ余裕があれば考えてみてくださいな。
次回
さすがにこの意味不明なアンケート要素じゃやっててもおもしろくないので、もう少し質問項目を揃えていこう。