Livewire で Fomantic-UI のダイアログ(Modal)を使うには?
Fomantic-UI のダイアログの表示について。
①ダイアログの表示状態を管理するのは、呼び出し元(親)かダイアログ(子)のどちらが良い?
→ダイアログ(子)です。親に表示状態を管理させると、ダイアログが多くなった時に管理が面倒になりますので。ダイアログ(子)に visibleプロパティ を持たせました。visible を ture にすればダイアログが表示されます。また、ダイアログが閉じられた時には、visible が false に変更されます。
②ダイアログで approve 後の処理はどうする?
→ダイアログで approve が選択された場合、「どうするか」を、呼び出し元(親)からダイアログ(子)に伝える。
でいきましょう。
手順
Laravel と Livewire をインストールして、Fomantic-UI のダイアログ(Modal)を使う手順。
- プロジェクト名(modal)を決めて以下のコマンドを実行します。
curl -s https://laravel.build/modal | bash
インストール時にプロジェクト名のディレクトリが作成されます。
- インストールの最後に sudo でパスワードの入力を求められます。
↓下のメッセージが表示されてインストールは終わります。
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Get started with: cd modal && ./vendor/bin/sail up
- sail のエイリアスを定義します。
echo "alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'" >> ~/.bashrc
source ~/.bashrc
Laravel のインストールはここまで。
- 「sail up」でコンテナを起動します。
cd modal && sail up -d
- ララベルのトップディレクトリで、Livewireパッケージ をインストールします。
sail composer require livewire/livewire
- 次のコマンドを実行して、modalコンポーネント を生成します。
sail artisan make:livewire modal
$ sail artisan make:livewire modal
COMPONENT CREATED ?
CLASS: app/Http/Livewire/Modal.php
VIEW: resources/views/livewire/modal.blade.php
次の 2つ のファイルが生成されます。
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class Modal extends Component
{
public function render()
{
return view('livewire.modal');
}
}
<div>
{{-- Be like water. --}}
</div>
※因みに、renderメソッド を定義しなくても livewire.modal は呼び出されます。
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class Modal extends Component
{
}
- 生成された Modalコンポーネント のクラスとビューを次のように置き換えます。
app/Http/Livewire/Modal.php
<?php
namespace App\Http\Livewire;
class Modal extends \Livewire\Component
{
use LiveRelation;
public $visible = false;
public $onApprove;
public $feedback;
public $contact_me = true;
}
↑ダイアログクラスには 表示状態(visible)と 承認ハンドラ(onApprove)のプロパティを持たせます。
resources/views/livewire/modal.blade.php
<div>
@if ($visible)
<div class="ui fullscreen modal {{ $this->id }}">
<i class="close icon"></i>
<div class="header">
Update Your Settings
</div>
<div class="content">
<div class="ui form">
<h4 class="ui dividing header">Give us your feedback</h4>
<div class="field">
<label>Feedback</label>
<textarea wire:model="feedback"></textarea>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" wire:model="contact_me">
<label>It's okay to contact me.</label>
</div>
</div>
</div>
</div>
<div class="actions">
<div class="ui negative button">Cancel</div>
<div class="ui positive button">Send</div>
</div>
</div>
<script>
$('.{{ $this->id }}')
.modal({
onApprove:el => {
{!! $onApprove !!}
return true
},
onDeny:el => true,
onHidden:() => {
$('.{{ $this->id }}').remove()
@this.visible = false
}
})
.modal('show')
</script>
@endif
</div>
↑表示状態(visible)が false なら表示しません。
approveボタン が押された場合、承認ハンドラ(onApprove)を実行します。
ダイアログが非表示になるタイミングで、エレメントを削除して、表示状態(visible)を false に設定しています。
- formsコンポーネント のクラスとビューを作成します。
app/Http/Livewire/Forms.php
<?php
namespace App\Http\Livewire;
class Forms extends \Livewire\Component
{
use LiveRelation;
public $feedback1;
public $feedback2;
public function approve1(LiveData $data) {
\Log::debug($data);
$this->feedback1 = $data->modal1['feedback'];
}
public function approve2(LiveData $data) {
\Log::debug($data);
$this->feedback2 = $data->modal2['feedback'];
}
public function test() {
$this->feedback1 = 'もっさん';
$this->feedback2 = 'ショコラ';
}
}
resources/views/livewire/forms.blade.php
<div class="ui basic segment">
<button class="ui button" type="button" onClick="Livewire.find('modal1').visible = true">ダイアログ1</button>
<button class="ui button" type="button" onClick="Livewire.find('modal2').visible = true">ダイアログ2</button>
<button class="ui button" type="button" onClick="@this.test()">親のプロパティに値をいれる</button>
<button class="ui button" type="button" onClick="Livewire.syncBindings()">ダイアログにコピー</button>
<div>
{{ $feedback1 }} {{ $feedback2 }}
</div>
<livewire:modal name="modal1" onApprove="window.Livewire.find('{{ $this->id }}').approve1()" bind="feedback1:feedback" />
<livewire:modal name="modal2" onApprove="window.Livewire.find('{{ $this->id }}').approve2()" bind="feedback2:feedback" />
</div>
- LiveRelationトレイト を作成します。
配置先:app/Http/Livewire/LiveRelation.php
<?php
namespace App\Http\Livewire;
use Livewire\LivewireManager;
use Livewire\LifecycleManager;
use Livewire\Component;
trait LiveRelation
{
public string $bind = ''; // 『親のプロパティ:自分のプロパティ』で関連付ける。
public string $name = ''; // コンポーネント識別名
public bool $livedata = true; // メソッド呼び出し時のパラメーターに LiveData を自動で付ける。
public function boot() {
LiveData::$current_component = $this;
}
public function isChild($childId) {
return collect($this->previouslyRenderedChildren)->pluck('id')->search($childId);
}
public function dehydrate() {
if ('' != $this->bind) {
$vars = [];
foreach (explode(',',$this->bind) as $bind) {
[$property,$self_property] = explode(':',str_repeat($bind.':',2));
$method = 'get'.\Str::studly($self_property);
if (method_exists($this,$method))
$value = $this->{$method}();
else if (property_exists($this,$self_property))
$value = $this->{$self_property};
else
continue;
$vars[$property] = $value;
}
if ([] !== $vars)
$this->emitParent('updateBindings',$vars);
}
}
protected function getListeners() {
return array_merge(parent::getListeners(),['updateBindings','syncBindings','setProperties'],
['set'.\Str::studly($this->id) => 'setProperties'],
('' != $this->name) ? ['set'.\Str::studly($this->name) => 'setProperties']:[]);
}
public function updateBindings($vars) {
foreach ($vars as $property => $value) {
$method = 'update'.\Str::studly($property);
if (method_exists($this,$method))
$this->{$method}($value);
else if (property_exists($this,$property))
$this->{$property} = $value;
}
}
public function syncBindings(LiveData $data) {
if ('' != $this->bind) {
foreach (explode(',',$this->bind) as $bind) {
[$property,$self_property] = explode(':',str_repeat($bind.':',2));
$this->$self_property = $data->getParent()[$property];
}
}
}
public function setProperties(LiveData $data) {
foreach ($data() as $property => $value)
$this->{$property} = $value;
$data()->dirty = false;
}
public function emitSet(string $key, LiveData $data) {
if (!in_array($key,[$this->id,$this->name]))
$this->emit('set'.\Str::studly($key),$data);
}
public function emitDirtySet(LiveData $data) {
collect($data)
->filter(fn($component) => $component->dirty)
->each(fn($component) => $this->emitSet($component['id'],$data));
}
public function emitFind($key, $event, ...$params) {
$this->emit('emitFind',$key,$event,...$params);
}
public function emitParent($event, ...$params) {
$this->emit('emitParent',$this->id,$event,...$params);
}
public function mountLivewireTag(string $livewire_tag,$target='body') {
$this->emit('createComponent',render_blade($livewire_tag),$target);
}
public function mountComponent(string $name,array $params=[],$target='body') {
$this->emit('createComponent',\Livewire\Livewire::mount($name,$params)->html(),$target);
}
public function removeComponent(string $key = null) {
$key ??= $this->id;
$this->emit('removeComponent',$key);
}
public function createComponent(string $name,array $params) {
$id = str()->random(20);
return LifecycleManager::fromInitialRequest($name,$id)
->boot()
->initialHydrate()
->mount($params)
->instance;
}
public function renderComponent(Component $component,string $target='body') {
$response = LifecycleManager::fromInitialInstance($component)
->renderToView()
->initialDehydrate()
->toInitialResponse();
$this->emit('createComponent',$response->html(),$target);
}
}
LiveRelation のポイント
①LiveRelation の bootメソッド で、LiveData の カレントコンポーネント を設定するようにしました。
②emitFindメソッドを追加しました。JavaScript の emitFind を使います。
③emitParentメソッド を追加しました。JavaScript の emitParent を使います。
④mountLivewireTag、mountComponent、removeComponentメソッド を追加しました。
⑤createComponent、renderComponentメソッド を追加しました。
配置先:app/Http/Livewire/LiveData.php
<?php
namespace App\Http\Livewire;
use Illuminate\Database\Eloquent\Model;
use \Livewire\Wireable;
use \Livewire\Component;
use Illuminate\Support\Collection;
class ComponentData extends \ArrayObject {
public bool $dirty = false;
public function __toString() {
return var_export($this->getArrayCopy(),true);
}
public function offsetSet(mixed $key, mixed $value) {
parent::offsetSet($key,$value);
$this->dirty = true;
}
}
class LiveData extends Model implements Wireable,\IteratorAggregate
{
protected Collection $components;
// カレントコンポーネントは LiveRelation の boot で設定される。
public static Component $current_component;
// Model のメソッドをオーバーライド
public function resolveRouteBinding($value, $field = null) {
$this->components = collect($value)->map(fn($component) => new ComponentData($component));
return $this;
}
// Model のメソッドをオーバーライド
public function toArray() {
return $this->components->toArray();
}
public function getIterator() {
return $this->components->keyBy('name');
}
public function __construct(array $attributes = []) {
parent::__construct($attributes);
$this->components = collect();
}
public function __destruct() {
$this->components
->filter(fn($component) => $component->dirty)
->each(fn($component) => self::$current_component->emitSet($component['name'],$this));
}
public function __invoke(Component $component = null): mixed {
$component ??= self::$current_component;
return $this->getComponentData($component->id);
}
public function getComponentData(string $needle = null, string $column_key = 'id') {
$needle ??= self::$current_component->id;
foreach ($this->components->keyBy($column_key) as $var => $component)
eval("\$_{$var} = \$component;");
return ${'_'.$needle} ?? null;
}
public function getComponentDataByName(string $name) {
return $this->getComponentData($name,'name');
}
public function __get(mixed $key):mixed {
return $this->getComponentDataByName($key);
}
public function offsetGet(mixed $key):mixed {
return $this->getComponentDataByName($key) ?? $this->getComponentData($key);
}
public function getParent(string $componentId = null) {
$component = $this->getComponentData($componentId);
return isset($component['parentId']) ? $this->getComponentData($component['parentId']) : null;
}
public function getChildren(string $componentId = null) {
$component = $this->getComponentData($componentId);
return collect($component['childIds'])->map(fn($childId) => $this->getComponentData($childId));
}
// Wireable 対応
public function toLivewire() {
return $this->components;
}
// Wireable 対応
public static function fromLivewire($value) {
$this->resolveRouteBinding($value);
}
}
LiveData のポイント
①Modelクラスを継承し、resolveRouteBinding、toArrayメソッド をオーバーライドしてデータベースに関係なく使えるようにしました。
②getComponetメソッド だと「Component が取得できる」と勘違いしてしまうので、getComponentData に変更しました。getParent と getChildren も取得できるものは Component ではありません。
③Wireable に対応し(toLivewire、fromLivewireメソッド追加)、LiveData を emit で送れるようにしました。
④ArrayObject は \Log::dump すると「could not be converted to string」のエラーになってしまうので、ComponentDataクラス を用意しました。
⑤ComponentDataクラス にダーティフラグを持たせて、LiveDataオブジェクト削除時に、自動で他のコンポーネントのプロパティを更新するようにしました。
- public/liveext.js を作成します。
配置先:public/liveext.js
// window.Livewire は定義済みです。
const data = () => {
return window.livewire.all().map(el => {
const defer = new Map
Object.keys(el.__instance.data).forEach(key => defer[key] = el.__instance.getPropertyValueIncludingDefers(key))
return Object.assign(defer,{
id: el.__instance.id,
childIds: el.__instance.childIds,
parentId: parentId(el.__instance.id)
})
})
}
const parentId = key => {
let id = componentId(key)
return window.livewire.all().find(el => -1 != el.__instance.childIds.indexOf(id))?.__instance.id
}
const componentId = key => window.livewire.all().find(el => -1 != [el,el.__instance,el.__instance.id,el.__instance.data.name].indexOf(key))?.__instance.id
const parent = key => window.livewire.find(parentId(key))
const syncBindings = () => window.livewire.emit('syncBindings',data())
export default {data,parentId,componentId,parent,syncBindings}
window.livewire.parentId = parentId
window.livewire.data = data
window.livewire.componentId = componentId
window.livewire.find = new Proxy(window.livewire.find,{
apply(target,thisArg,argumentsList) {
let key = argumentsList[0]
let id = componentId(key)
return target.apply(thisArg,[id])
}
})
window.livewire.parent = parent
window.livewire.syncBindings = syncBindings
window.livewire.on('emitFind',(key,event,...params) => window.livewire.find(key).emitSelf(event,...params))
window.livewire.on('emitParent',(key,event,...params) => parent(key).emitSelf(event,...params))
window.livewire.on('createComponent',(html,target='body') => {
const el = document.createElement('div')
el.innerHTML = html
document.querySelector(target).appendChild(el.firstElementChild)
window.livewire.rescan()
})
window.livewire.on('removeComponent',key => {
let id = componentId(key)
document.querySelector(`[wire\\:id="${id}"]`).remove()
window.livewire.rescan()
})
window.livewire.hook('component.initialized',component => {
if (component.data.livedata) {
component.addAction = new Proxy(component.addAction,{
apply(target,thisArg,argumentsList) {
let action = argumentsList[0]
if (('method' in action.payload) && ('$' != action.payload.method.charAt(0)))
action.payload.params.push(data())
return target.apply(thisArg,[action])
}
})
}
})
liveext.js のポイント
①Livewire.parentId関数で 親コンポーネントのID を取得できます。
②getPropertyValueIncludingDefers で defer に設定された値も取得できるように対応しました。
③Livewire.componentId関数で name から id を引けるようにしました。
④Livewire.find関数で name から find できるように変更しました。
⑤Livewire.syncProperties関数で 子供のプロパティが親のプロパティに同期できるようにしました。
⑥emitFindイベントで 特定のコンポーネントにイベントを送信できるようにしました。
⑦JavaScript のメソッド呼び出し時に、Livewire.data() を自動でパラメーターに追加するようにしました。
⑧createComponent、removeComponent を追加しました。コンポーネントを動的に作成、削除することができます。
- resources/views/index.blade.php ファイル を作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.0/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.9.0/dist/semantic.min.js"></script>
<script type="module" src="/liveext.js"></script>
<livewire:styles />
</head>
<body>
<livewire:forms />
<livewire:scripts />
</body>
</html>
- routes/web.php にルートを追加します。
Route::get('/', fn() => view('index'));
- ブラウザで確認します。
tail で sendボタン を押したタイミングで通信していることを確認しましょう。
tail -f storage/logs/laravel.log
以上