種子と植物の連携
前回終了時のソースコード
Laravelを使ったアプリケーション開発のソースコードは以下のようにしてダウンロードすることができます。
$ git clone https://kiku3.tsbio.info/git/study-laravel.git study_laravel
この章開始時点のソースコードは chapt9 ブランチにあります。
$ git switch chapt9
ソースコードを自分で書いていく場合は、自分用のブランチをつくるとよいです。
$ git switch -c my9 chapt9
Laravelのコードはsrcディレクトリからの相対パスになっています。
エラーへの対処
-
Laravelのパッケージがない
composer install を実行してください。開発段階に応じて不足するパッケージがあるときは、再度 composer install が必要な場合があります。
-
ディレクトリのパーミッションが正しくない
tempnam()が実行できないなどのエラーが出るときは、storageとbootstrap/cacheディレクトリの所有者とパーミッションを確認してください(第1章を参照)
-
Vite manifest not found
開発中なら npm run dev、本番環境なら npm run buildを実行してください。
-
npm run devが動かない
npm install を実行してください。
種子をまいて植物に、植物から種子を回収する、というサイクルを実現する。
種子から植物へ
種まきをして、芽が出たら鉢に植えます。 鉢につけるラベルを作成するために、seedテーブルの情報からplantテーブルの新しいデータをつくります。 これを行うために、seedモジュールに「鉢植え」タブをつくり、plantationという機能をつくります。
ユーザーは次のことを行います。
- 種子の一覧で播いた種を探す(チェックをいれる)。
- 鉢植えのページにいって、種まきの日、鉢植えの日、育成者の情報を入力し、植物テーブルに新しいデータを追加する。
- 植物の一覧で追加されたもののラベルを印刷する。
2がplantationの機能で、種子のデータを植物のテーブルに追加します。 種子データ(Seedモデル)を植物データ(Plantリクエスト)に変換し、/plant/storeに送信します。
次の手順でplantation機能を実装します。
resources/views/seed/module.blade.phpに「鉢植え」のタブを追加する。routs/seed.phpに /seed/plantation へのルートを追加する。app/Http/Controllers/SeedController.blade.phpに plantation のビューを返すメソッドを追加する。resources/views/seed/plantation.blade.phpをつくって、seedモジュールのitemsを/plantにPOSTするしくみをつくる。resources/views/layouts/app.blade.phpを修正して、seedモジュールからplantモジュールにitemsを送信できるしくみをつくる(Javascript)。resources/views/plant/module.blade.phpを修正して、送られてきたitemsを表示する(Javascript)。
resources/views/seed/module.blade.php
x-module-nav の features 属性に渡す配列に、下記を追加する。
resources/views/seed/module.blade.php の一部
'plantation' => ['label' => '鉢植え', 'permission' => 'read'],
routs/seed.php
GETの配列に plantation を追加する。
app/Http/Controllers/SeedController.blade.php
plantationビューを返すメソッドを追加する。
app/Http/Controllers/SeedController.blade.php の一部
// 種のデータを植物モジュールに送るためのフォームをつくる
public function plantation(){
return $this->respond([
// seed/plantation.blade.php中で、plantのテーブルの情報が必要になる。
'plant_table' => new PlantTableDefinition(),
]);
}
resources/views/seed/plantation.blade.php
追加データをjsonで送って、結果を受け取るという部分は material/createと似ているので、それを元にして作成しました。 重要な部分を以下に説明します。
x-dataのtemplate(item)
引数のitemはseedテーブルのデータで、これを template で処理することで、plantの型にする。 initで、$watchを設定してあり、そこでitemsからentriesをつくるときに使われる。
x-dataのplantation()
entriesをplant.storeにpostする。 実際にこれを呼び出している部分は一か所だけなので、そちらで定義してもよいが、関数化しておくことで、動作検証がやりやすくなる。
selectModule('plant’).then()
layouts/app.blade.phpにあるselectModuleを修正して、htmxによるHTMLの差し替えが終わった後にプログラムを実行できるようにする。
新しいデータ(created)はplantモジュールのitemsに設定したいが、x-dataのスコープが異なるので直接変更することができない。 そこで、post-plantationというイベントを定義し、そのイベントに付属させる形でcreatedを渡す。
resources/views/seed/plantation.blade.php
@extends("$module_id.module")
@section('feature')
<x-feature id='{{ $feature_id }}'>
<div
x-data="Material.makeStorePanel({
required: @js($table->requiredColumns()),
/* plantテーブルに合わせた初期値をつくる */
template(item) {
return {
...item,
seed_id: item.id, // ここのitemはseedテーブルのレコード。そのidをplantテーブルのseed_idに設定する。
created_at: @js(date('ymd')),
owner: @js(auth()->user()->name),
}
},
plantation() {
/* plantモジュールに追加する。
*/
this.store('{{ route("web.plant.store") }}')
.then(data => {
/* 追加に成功したものをフォームから削除する console.log('succeeded ids:', Object.keys(data.succeeded));
*/
console.log('succeeded ids:', Object.keys(data.succeeded));
const ids = new Set(Object.keys(data.succeeded).map(Number));
this.entries = this.entries.filter(entry => ! ids.has(entry.id));
/* 追加に成功したもののitem.checkedをはずす console.log('succeeded ids:', Object.keys(data.succeeded));
*/
console.log('succeeded ids:', Object.keys(data.succeeded));
checkedItems.forEach(item => {
if(ids.has(item.id)) item.checked = false;
});
/* 鉢植えに成功したものを植物の一覧に送る
*/
const created = data.created.map(item => ({...item, checked : true}));
selectModule('plant').then(() => {
/* Alpine.js 側にカスタムイベントを送信
*/
window.dispatchEvent(new CustomEvent('post-plantation', {
detail: { message: 'Load!', items: created }
}));
}); // selectModule().then()を閉じる
}); // store().then()を閉じる
}, // plantation(){} を閉じる
init() {
this.$watch('items', (items) => {
this.entries = checkedItems.map((item) => this.template(item));
});
},
})"
>
@if(0) {{-- 動作確認用コード --}}
<div class='plantation-test'
x-init="
query.strain.value = 'Colsssss';
search().then(() => {
items.slice(0, 1).map(item => item.checked = true);
});
"
>plantation-test
query:
<template x-for="key in Object.keys(query)" :key="key">
<template x-if="query[key] !== undefined && query[key] !== ''">
<span x-text="key"></span>=<span x-text="query[key]"></span>
</template>
</template>
</div>
@endif
<p x-show="!hasChecked" class="pb-10 pl-4">
<a
href="{{ route("web.$module_id.index") }}"
@click.prevent="selectFeature('{{ "${module_id}-index" }}', event)"
>
先にラベルをつくるサンプルにチェックをいれてください。
</a>
</p>
{{-- 表型の入力フォーム 種から植物へデータコピー --}}
<div x-show="hasChecked">
<p class="pl-4">
鉢植えにするサンプルにチェックをいれてください。
「種播き・鉢植え」ボタンをクリックした後で、植物の一覧からラベルを作成します。
</p>
<p class="py-2 pl-4">同じ種類の種から複数の鉢植えをつくるときは、個体番号を編集してください。</p>
<p class="py-2 pl-4">
<button type='submit' title="チェックの入った種を植物リストに追加します。"
@click="plantation()"
>
種播き・鉢植え
</button>
</p>
<table class='material-list'>
<thead>
{{-- thead 1行目 見出し --}}
<tr class=''>
<th></th>
@foreach($plant_table->columns('create') as $col)
<x-material.table-th :column=$col />
@endforeach
</tr>
</thead>
<tbody>
<!-- 対象サンプル -->
<template x-for="entry in entries" :key="entry.id">
<tr>
<td>
<input type='checkbox' checked='checked'
x-model="entry.checked"
@change="if (!entry.checked) {
checkedItems.forEach(item => {if(item.id == entry.id) item.checked = false});
entries = entries.filter(e => e !== entry)
}"
>
<span x-text="entry.id"></span>
</td>
@foreach($plant_table->columns('create') as $key => $col)
<td class="{{ $col['class'] }}">
<input x-model="entry.{{ $key }}" placeholder="{{ $col['placeholder'] ?? '' }}"
@focus="$el.select()"
>
</td>
@endforeach
<td>
seed_id: <span x-text="entry.seed_id"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</x-feature>
@endsection
resources/views/layouts/app.blade.php
selectModuleを修正して、Promiseを返すようにします。
すでに指定されたモジュールのHTMLが存在する場合、Promiseはresolve()をすぐに実行します。 HTMLがなかった場合、htmxが動き、HTMLの差し込みが終わったあとhtmx:afterSettleイベントを発火します。 このイベントが起きたとき、resolve()が実行されるように、document.addEventListenerで設定します。
これらの変更によって、 selectModule(module_id).then(…) というようにして、htmxがHTMLの書き換えを終えたときに次のイベント post-plantation に移ることができるようになります。
resources/views/layouts/app.blade.php
@extends('layouts.html')
@section('body')
<header class='bg-blue-500 text-white py-2 px-2 flex justify-between items-start'>
<h1 class='text-xl leading-none'>{{ config('app.name') }}</h1>
<div class="flex items-baseline">
@auth
<span class='px-2 text-sm'>{{ auth()->user()->name }}</span>
<form method="POST" action="{{ route('logout') }}" class="inline">
@csrf
<button type="submit" class="text-sm text-black bg-gray-200 border-gray-400 px-2 hover:bg-gray-400 hover:text-white">
{{ __('ログアウト') }}
</button>
</form>
@endauth
@guest
<span class='px-2 text-sm'>ゲスト</span>
<form method="GET" action="{{ route('login') }}" class="inline">
<button type="submit" class="text-sm text-black bg-gray-200 border-gray-400 px-2 hover:bg-gray-400 hover:text-white">
{{ __('ログイン') }}
</button>
</form>
@endguest
</div>
</header>
<main id='modules' class='bg-yellow-100 px-2 min-h-screen'
x-data="{
activeModule: '{{ $module_id }}',
/* activeModuleの値を変更し、そのHTML要素がないときに取得する */
selectModule(moduleId){
/* タブのhref属性からurlを取得する */
const tab = document.getElementById(`${moduleId}-tab`);
if(!tab) return;
const url = tab.href;
/* URLを変更する。これによってブラウザーのリロードをしても、以前のページを取得できるようになる */
history.pushState({}, '', url);
/* activeModuleにidを設定 */
this.activeModule = moduleId;
return new Promise((resolve) => {
/* idで要素を取得 */
const panel = document.getElementById(moduleId);
/* 要素を取得できたときは resolve() を呼び出す -> thenに進む */
if(panel !== null) {
resolve();
return;
}
/* 要素を取得できなかったときにhtmxで部分HTMLを取得する */
// 奇妙に見えるがこれはhanderという関数を定義している。handlerはrevolve()をするだけ。
const handler = () => {console.log('handler was called'); resolve()};
// htmxの置き換えが終わったら、handlerが呼ばれる(=> revolve()を実行する)。
document.body.addEventListener('htmx:afterSettle', handler, { once: true });
htmx.ajax('GET', url, {
target:'main', /* 取得したHTMLの置き換え場所 */
swap: 'beforeend', /* 今回はdiv.featuresの子要素として追加するのでbeforeend */
headers: {
'HX-Target': 'module-' + moduleId,
}
});
}) // return new Promiseの終わり
} // selectModuleの終わり
}"
>
<nav class='py-2 px-2'>
<ul id='module-tabs' class='flex'>
@foreach([
{{-- bg-{{ $module_id }}はtailwindcssが見逃すのでbg-oligoとする --}}
'seed' => ['label' => '種子', 'active' => 'bg-seed', 'inactive' => 'hover:bg-seed-light'],
'plant' => ['label' => '植物', 'active' => 'bg-plant', 'inactive' => 'hover:bg-plant-light'],
'oligo' => ['label' => 'オリゴDNA', 'active' => 'bg-oligo', 'inactive' => 'hover:bg-oligo-light'],
] as $module => $info)
<li class='module-tab'
:class="{
'{{ $info['active'] }} text-white': activeModule === '{{ $module }}',
'{{ $info['inactive'] }} bg-white text-gray-600' : activeModule !== '{{ $module }}'
}"
>
<a id='{{ $module }}-tab' role='tab'
href='{{ route("web.{$module}.index") }}'
@click.prevent="selectModule('{{ $module }}', event)"
>
{{ $info['label'] }}
</a>
</li>
@endforeach
<li x-text="activeModule" class='module-tab bg-gray-400'></li>
</ul>
</nav>
@yield('module')
</main>
@endsection
resources/views/plant/module.blade.php
seed/plantation.blade.php で発火した post-plantation イベントを受け取るように修正します。 x-on:post-plantation.window はalpineのイベント設定の方法で、トップレベルのwindowオブジェクトにpost-plantation というイベントをとらえるように設定しています。
resources/views/plant/module.blade.php
@extends('layouts.app')
@section('module')
<x-module id='{{ $module_id }}'>
<!-- モジュールのメニュー -->
<x-module-nav
:features="[
'index' => ['label' => '一覧', 'permission' => 'read'],
'label' => ['label' => 'ラベル', 'permission' => 'read'],
'create' => ['label' => '追加', 'permission' => 'read'],
'edit' => ['label' => '編集', 'permission' => 'read'],
]"
/>
<div class='features'
x-data="Material.makeModulePanel({
query_template: @js($table->buildQueryDefaults()),
url: {
search: '{{ route("web.{$module_id}.search") }}',
store: '{{ route("web.$module_id.store") }}',
update: '{{ route("web.{$module_id}.index") }}',
},
})"
x-init="resetQuery();"
{{-- seedモジュールの鉢植えによってつくられた植物のデータを表示する --}}
x-on:post-plantation.window="
console.log('post-plantation event in plant/module was called', $event.detail.items);
append($event.detail.items);
"
>
@yield('feature')
</div>
</x-module>
@endsection
複数データの追加
普通の実験では種を一つ播いて、一つの植物を育てるということはなく、たくさん播いて、その中から選んだものを鉢植えにします。
個体番号だけが異なるものを一度に登録できるように PlantService の機能を拡張します。同じものは SeedService などでも必要になるので、Traitとしてつくります。
app/Traits/ExpandItem.php
<?php
namespace App\Traits;
trait ExpandItem
{
/**
* $items 配列の指定した $field (例: 'no') を展開し、$items の要素を増やして返す
*/
public function expandItems(array $items, string $field = 'no'): array
{
$expandedItems = [];
foreach ($items as $item) {
// 指定されたフィールドが存在しない、または空ならそのまま追加
if (empty($item[$field])) {
$expandedItems[] = $item;
continue;
}
// no などを分解して複数の文字列配列(例: ['A-1', 'A-2', 'A-3'])を取得
$expandedNos = $this->expandNo($item[$field]);
// 分解された no ごとに $item を複製して追加
foreach ($expandedNos as $no) {
$item[$field] = $no; // 'no' フィールドを書き換え
$expandedItems[] = $item;
}
}
return $expandedItems;
}
/**
* カンマ区切りや範囲指定 (A1:3 など) の文字列を分解する
*/
public function expandNo(string $no): array
{
$base = '';
$series = [];
foreach (explode(',', $no) as $part) {
$part = trim($part);
if (preg_match("/^(.*?)(\d+)(:?)(\d+)?$/", $part, $match)) {
$base = empty($match[1]) ? $base : $match[1];
$start = (int)$match[2];
$range = isset($match[4]) ? range($start, (int)$match[4]) : [$start];
$series = array_merge($series, array_map(fn($i) => $base . $i, $range));
} else {
$series[] = $part;
}
}
return $series;
}
}
app/Services/PlantService.php
<?php
namespace App\Services;
use App\Models\Plant;
use App\TableDefinitions\PlantTableDefinition;
use App\TableSearch\SearchFactory;
use App\Traits\ExpandItem; // 個体番号 #1:4,6 を #1, #2, #3, #4, #6に展開
class PlantService extends MaterialService
{
public function __construct(
protected Plant $model,
public PlantTableDefinition $table,
protected SearchFactory $searchFactory
){}
use ExpandItem;
public function create(array $items)
{
$expandedItems = $this->expandItems($items, 'no');
return parent::create($expandedItems);
}
}
植物から種子へ
鉢に植えた植物から種を回収します。 そのとき、世代番号を一つ増やします。 また、植物は枯れているでしょうから、同時にテーブルから削除できるようにします。
次の手順でharvest機能を実装します。
resources/views/plant/module.blade.phpに「収穫」のタブを追加する。routs/plant.phpに /plant/harvest へのルートを追加する。app/Http/Controllers/PlantController.blade.phpに harvest のビューを返すメソッドを追加する。resources/views/plant/harvest.blade.phpをつくって、plantモジュールのitemsを/seedにPOSTするしくみをつくる。resources/views/seed/module.blade.phpを修正して、送られてきたitemsを表示する(Javascript)。
resources/views/plant/module.blade.php
x-module-nav の features 属性に渡す配列に、下記を追加する。
resources/views/plant/module.blade.php の一部
'harvest' => ['label' => '収穫', 'permission' => 'read'],
routs/plant.php
GETの配列に harvest を追加する。
app/Http/Controllers/PlantController.blade.php
harvestビューを返すメソッドを追加する。
app/Http/Controllers/PlantController.blade.php の一部
// 植物のデータを種子モジュールに送るためのフォームをつくる
public function harvest(){
return $this->respond([
// plant/harvest.blade.php中で、seedのテーブルの情報が必要になる。
'seed_table' => new SeedTableDefinition(),
]);
}
resources/views/plant/harvest.blade.php
seed/plantation.blade.php を元にして作成しました。 重要な部分を以下に説明します。
x-dataのtemplate(item)
引数のitemのgenerationの数字部分を1増やして、plantの初期値にしています。
selectModule('seed’).then()
収穫後にデータを削除するオプションに対する処理を実装しています。
削除は複数のidを配列で送るようにしています。 とりあえず必要なのは plant モジュールだけですが、他でも必要になるかもしれないのでMaterialControllerとMaterialServiceに実装しました。
seedモジュールのitemsにcreatedを設定したいので、plantationと同じように post-harvestイベントをつくりました。
resources/views/plant/harvest.blade.php
@extends("$module_id.module")
@section('feature')
<x-feature id='{{ $feature_id }}'>
<div
x-data="Material.makeStorePanel({
required: @js($table->requiredColumns()),
template(item) {
const match = (item.generation || '').match(/^(.*?)(\d+)$/);
const g = match ? match[1] + (parseInt(match[2]) + 1) : item.generation;
return {
...item,
mother: item.id,
generation: g,
created_at: @js(date('ymd')),
owner: @js(auth()->user()->name),
}
},
discard: true,
harvest() {
this.store('{{ route("web.seed.store") }}')
.then(data => {
/* 1. 成功したもの(seed_id)をindex (items), plantation (rows) 表から削除する。
* 2. 成功したものをチェックをつけて、plant に送る
*/
// 追加に成功したものをフォームから削除する
const ids = new Set(Object.keys(data.succeeded).map(Number));
this.entries = this.entries.filter(entry => ! ids.has(entry.id));
// 追加に成功したもののitem.checkedをはずす console.log('checkoff ids:', ids);
checkedItems.forEach(item => {
if(ids.has(item.id)) item.checked = false;
});
// 追加に成功したものをデータベースから削除する
console.log('destroy', ids);
this.destroy('{{ route("web.$module_id.destroy")}}', Array.from(ids));
/* 種採りに成功したものを種子の一覧に送る */
const created = data.created.map(item => ({...item, checked : true}));
selectModule('seed').then(() => {
// Alpine.js 側にカスタムイベントを送信
window.dispatchEvent(new CustomEvent('post-harvest', {
detail: { message: 'Load!', items: created }
}));
}); // selectModule().then()を閉じる
}); // store().then()を閉じる
}, // harvest(){} を閉じる
init() {
this.$watch('items', (items) => {
this.entries = checkedItems.map((item) => this.template(item));
});
},
})"
>
@if(0) {{-- デバッグ用コード --}}
<div class='harvest-test'
x-init="
query.strain.value = 'Col';
search().then(() => {
items.map(item => item.checked = true);
});
"
>harvest-test
query:
<template x-for="key in Object.keys(query)" :key="key">
<template x-if="query[key] !== undefined && query[key] !== ''">
<span x-text="key"></span>=<span x-text="query[key]"></span>
</template>
</template>
</div>
@endif
<p x-show="!hasChecked" class="pb-10 pl-4">
<a
href="{{ route("web.$module_id.index") }}"
@click.prevent="selectFeature('{{ "${module_id}-index" }}', event)"
>
先にラベルをつくるサンプルにチェックをいれてください。
</a>
</p>
{{-- 表型の入力フォーム 植物から種子へデータコピー --}}
<div x-show="hasChecked">
<p class="pl-4">
種採りをするサンプルにチェックをいれてください。
「種採り」ボタンをクリックした後で、種子の一覧からラベルを作成します。
</p>
<p class="py-2 pl-4">同じ植物の種を複数の袋に分けるときは、個体番号を編集してください。</p>
<p class="py-2 pl-4">
<button type='submit' title="チェックの入った植物を種子リストに追加します。"
@click="harvest()"
>
種採り
</button>
<label>
<input type='checkbox' x-model='discard'>
種子を追加後植物を削除する
</label>
</p>
<table class='material-list'>
<thead>
{{-- thead 1行目 見出し --}}
<tr class=''>
<th></th>
@foreach($seed_table->columns('create') as $col)
<x-material.table-th :column=$col />
@endforeach
</tr>
</thead>
<tbody>
<!-- 対象サンプル -->
<template x-for="entry in entries" :key="entry.id">
<tr>
<td>
<input type='checkbox' checked='checked'
x-model="entry.checked"
@change="if (!entry.checked) {
checkedItems.forEach(item => {if(item.id == entry.id) item.checked = false});
entries = entries.filter(e => e !== entry)
}"
>
<span x-text="entry.id"></span>
</td>
@foreach($seed_table->columns('create') as $key => $col)
<td class="{{ $col['class'] }}">
<input x-model="entry.{{ $key }}" placeholder="{{ $col['placeholder'] ?? '' }}"
@focus="$el.select()"
>
</td>
@endforeach
<td>
mother: <span x-text="entry.mother"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</x-feature>
@endsection
resources/views/seed/module.blade.php
div.features に追加
resources/views/seed/module.blade.php の一部
{{-- plantモジュールの収穫によってつくられた種子のデータを表示する --}}
x-on:post-harvest.window="
console.log('post-harvest event in seed/module was called', $event.detail.items);
append($event.detail.items);
"
データ削除の実装
idの配列はCSVの出力でつくった BulkItemIdsRequest をそのまま使う。
app/Http/Controllers/MaterialController.php の一部
// 削除
public function destroy(BulkItemIdsRequest $request){
try{
return $this->json($this->service->destroy($request->validated('id')), 200);
}catch(\Throwable $e){
return $this->json(['message' => $e->getMessage()], 400);
}
}
app/Services/MaterialService.php の一部
// テーブルからレコードを削除する
function destroy(array $ids){
return ['destroid_items' => $this->model->destroy($ids)]; // 削除された件数が返る
}
交配して種子へ
交配に使った親株の情報をmotherとfatherにいれて、種子のデータをつくれるようにします。
機能を追加する手順は harvest とほぼ同じです。
resources/views/plant/module.blade.php
x-module-nav の features 属性に渡す配列に、下記を追加する。
resources/views/plant/module.blade.php の一部
'cross' => ['label' => '交配', 'permission' => 'read'],
routs/plant.php
GETの配列に cross を追加する。
app/Http/Controllers/PlantController.blade.php
crossビューを返すメソッドを追加する。
app/Http/Controllers/PlantController.blade.php の一部
// 植物のデータを種子モジュールに送るためのフォームをつくる
public function cross(){
return $this->respond([
'seed_table' => new SeedTableDefinition(),
]);
}
resources/views/plant/cross.blade.php
plant/cross.blade.php を元にして作成しました。 重要な部分を以下に説明します。
table – tbody – td 雄雌のselect
チェックが付いたitemから template を使って selectボックスを作ります。 x-modelでentryのmotherとfatherを指定し、optionのvalueをそれに設定します。
resources/views/plant/cross.blade.php
@extends("$module_id.module")
@section('feature')
<x-feature id='{{ $feature_id }}'>
<div
x-data="Material.makeStorePanel({
required: @js($table->requiredColumns()),
template(item) {
return {
...item,
mother: 0,
father: 0,
generation: 'F1',
create_at: @js(date('ymd')),
owner: @js(auth()->user()->name),
}
},
discard: true,
cross() {
this.store('{{ route("web.seed.store") }}')
.then(data => {
console.log('after cross:', data, checkedItems);
/* 1. 成功したもの(seed_id)をindex (items), plantation (rows) 表から削除する。
* 2. 成功したものをチェックをつけて、plant に送る
*/
// 追加に成功したものをフォームから削除する
console.log('succeeded ids:', Object.keys(data.succeeded));
const ids = new Set(Object.keys(data.succeeded).map(Number));
this.entries = this.entries.filter(entry => ! ids.has(entry.id));
// 追加に成功したもののitem.checkedをはずす
console.log('checkoff ids:', ids);
checkedItems.forEach(item => {
if(ids.has(item.id)) item.checked = false;
});
// 追加に成功したものをデータベースから削除する
console.log('destroy', ids);
this.destroy('{{ route("web.$module_id.destroy")}}', Array.from(ids));
/* 種採りに成功したものを種子の一覧に送る */
const created = data.created.map(item => ({...item, checked : true}));
selectModule('seed').then(() => {
// Alpine.js 側にカスタムイベントを送信
window.dispatchEvent(new CustomEvent('post-cross', {
detail: { message: 'Load!', items: created }
}));
}); // selectModule().then()を閉じる
}); // store().then()を閉じる
}, // cross(){} を閉じる
init() {
this.$watch('items', (items) => {
this.entries = checkedItems.map((item) => this.template(item));
});
},
})"
>
@if(1)
<div class='cross-test'
x-init="
query.strain.value = 'Col';
search().then(() => {
items.map(item => item.checked = true);
});
"
>cross-test
query:
<template x-for="key in Object.keys(query)" :key="key">
<template x-if="query[key] !== undefined && query[key] !== ''">
<span x-text="key"></span>=<span x-text="query[key]"></span>
</template>
</template>
</div>
@endif
<p x-show="!hasChecked" class="pb-10 pl-4">
<a
href="{{ route("web.$module_id.index") }}"
@click.prevent="selectFeature('{{ "${module_id}-index" }}', event)"
>
先に交配に使用した親植物にチェックをいれてください。
</a>
</p>
{{-- 表型の入力フォーム 植物から種子へデータコピー --}}
<div x-show="hasChecked">
<p class="pl-4">
種採りをするサンプルにチェックをいれてください。
「種採り」ボタンをクリックした後で、種子の一覧からラベルを作成します。
</p>
<p class="py-2 pl-4">同じ植物の種を複数の袋に分けるときは、個体番号を編集してください。</p>
<p class="py-2 pl-4">
<button type='submit' title="チェックの入った植物を種子リストに追加します。"
@click="cross()"
>
交配した種子を登録
</button>
<label>
<input type='checkbox' x-model='discard'>
種子を追加後植物を削除する
</label>
</p>
<table class='material-list'>
<thead>
{{-- thead 1行目 見出し --}}
<tr class=''>
<th></th>
<th>雌 (♀)</th>
<th>雄 (♂)</th>
@foreach($seed_table->columns('create') as $col)
<x-material.table-th :column=$col />
@endforeach
</tr>
</thead>
<tbody>
<!-- 対象サンプル -->
<template x-for="entry in entries" :key="entry.id">
<tr>
<td>
<input type='checkbox' checked='checked'
x-model="entry.checked"
@change="if (!entry.checked) {
checkedItems.forEach(item => {if(item.id == entry.id) item.checked = false});
entries = entries.filter(e => e !== entry)
}"
>
<span x-text="entry.id"></span>
</td>
{{-- 雌しべ側選択 --}}
<td>
<select x-model="entry.mother"
@change="
if($event.target.value == 0) return;
const selected = checkedItems.find(item => item.id == $event.target.value);
entry.strain = selected.strain;
"
>
<option value="0">選択してください</option>
<template x-for="parent in checkedItems" :key="parent.id">
<option :value="parent.id" x-text="[parent.strain, parent.generation, parent.no].join(' ')" />
</template>
</select>
</td>
{{-- 雄しべ側選択 --}}
<td>
<select x-model="entry.father">
<option value="0">選択してください</option>
<template x-for="parent in checkedItems" :key="parent.id">
<option :value="parent.id" x-text="[parent.strain, parent.generation, parent.no].join(' ')" />
</template>
</select>
</td>
@foreach($seed_table->columns('create') as $key => $col)
<td class="{{ $col['class'] }}">
<input x-model="entry.{{ $key }}" placeholder="{{ $col['placeholder'] ?? '' }}"
@focus="$el.select()"
>
</td>
@endforeach
<td>
♀: <span x-text="entry.mother"></span>
♂: <span x-text="entry.father"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</x-feature>
@endsection
resources/views/seed/module.blade.php
div.features に追加。 名前が異なるだけでやることは同じなので、resources/views/plant/cross.blade.php で発火させるイベントをpost-harvestにするほうがコードが増えなくてよいかもしれない。
resources/views/seed/module.blade.php の一部
{{-- plantモジュールの交配によってつくられた種子のデータを表示する --}}
x-on:post-cross.window="
console.log('post-cross event in seed/module was called', $event.detail.items);
append($event.detail.items);
"