Laravel المتقدم

أنماط Livewire المتقدمة

18 دقيقة الدرس 37 من 40

أنماط Livewire المتقدمة

Livewire هو إطار عمل كامل لـ Laravel يجعل بناء واجهات ديناميكية بسيطًا. في هذا الدرس، سنستكشف أنماط Livewire المتقدمة بما في ذلك المكونات المتداخلة، والميزات في الوقت الفعلي، وتحميلات الملفات، والتكامل مع Alpine.js لتحسين التفاعلية.

اتصال المكونات المتداخلة

يمكن أن تكون مكونات Livewire متداخلة وتتواصل مع بعضها البعض من خلال طرق مختلفة:

<?php namespace App\Http\Livewire; use Livewire\Component; use App\Models\Product; class ProductList extends Component { public $selectedProduct = null; // الاستماع للأحداث من المكونات الفرعية protected $listeners = [ 'productSelected' => 'handleProductSelected', 'productDeleted' => 'refreshList', ]; public function handleProductSelected($productId) { $this->selectedProduct = Product::find($productId); // إصدار حدث إلى مكونات أخرى $this->emit('productDetailsLoaded', $this->selectedProduct->id); } public function refreshList() { $this->selectedProduct = null; // تحديث مكون فرعي محدد $this->emit('refreshComponent', 'product-card'); } public function render() { return view('livewire.product-list', [ 'products' => Product::with('category')->latest()->get(), ]); } } // المكون الفرعي namespace App\Http\Livewire; use Livewire\Component; use App\Models\Product; class ProductCard extends Component { public Product $product; public bool $showDetails = false; protected $listeners = ['refreshComponent']; public function selectProduct() { // إصدار إلى المكون الأصلي $this->emitUp('productSelected', $this->product->id); } public function deleteProduct() { $this->product->delete(); // إصدار إلى جميع المكونات $this->emitSelf('productDeleted'); // عرض رسالة نجاح $this->dispatchBrowserEvent('product-deleted', [ 'message' => 'تم حذف المنتج بنجاح!', ]); } public function refreshComponent() { // تحديث بيانات هذا المكون $this->product->refresh(); } public function render() { return view('livewire.product-card'); } }
ملاحظة: استخدم emit() للبث إلى جميع المكونات، emitUp() للإرسال إلى الأصل، emitSelf() للإرسال إلى نفس المكون، وemitTo() للإرسال إلى مكون محدد بالاسم.

ربط Wire:model العميق

تقنيات ربط البيانات المتقدمة لهياكل البيانات المتداخلة المعقدة:

<?php namespace App\Http\Livewire; use Livewire\Component; class OrderForm extends Component { public $order = [ 'customer' => [ 'name' => '', 'email' => '', 'address' => [ 'street' => '', 'city' => '', 'country' => '', ], ], 'items' => [], 'shipping_method' => 'standard', 'notes' => '', ]; public $products = []; protected $rules = [ 'order.customer.name' => 'required|min:3', 'order.customer.email' => 'required|email', 'order.customer.address.street' => 'required', 'order.customer.address.city' => 'required', 'order.customer.address.country' => 'required', 'order.items.*.product_id' => 'required|exists:products,id', 'order.items.*.quantity' => 'required|integer|min:1', ]; public function mount() { $this->products = Product::all(); } public function addItem() { $this->order['items'][] = [ 'product_id' => '', 'quantity' => 1, 'price' => 0, ]; } public function removeItem($index) { unset($this->order['items'][$index]); $this->order['items'] = array_values($this->order['items']); } // التحقق في الوقت الفعلي لحقل محدد public function updated($propertyName) { $this->validateOnly($propertyName); } // حساب الإجمالي في الوقت الفعلي public function getCalculatedTotalProperty() { return collect($this->order['items'])->sum(function ($item) { return ($item['quantity'] ?? 0) * ($item['price'] ?? 0); }); } public function submitOrder() { $this->validate(); $order = Order::create([ 'customer_data' => $this->order['customer'], 'shipping_method' => $this->order['shipping_method'], 'notes' => $this->order['notes'], 'total' => $this->calculatedTotal, ]); foreach ($this->order['items'] as $item) { $order->items()->create($item); } session()->flash('success', 'تم إنشاء الطلب بنجاح!'); return redirect()->route('orders.show', $order); } public function render() { return view('livewire.order-form'); } }

عرض Blade مع ربط wire:model العميق:

<!-- resources/views/livewire/order-form.blade.php --> <div> <form wire:submit.prevent="submitOrder"> <!-- معلومات العميل --> <div class="section"> <h3>معلومات العميل</h3> <input type="text" wire:model.defer="order.customer.name" placeholder="الاسم الكامل"> @error('order.customer.name') <span class="error">{{ $message }}</span> @enderror <input type="email" wire:model.lazy="order.customer.email" placeholder="البريد الإلكتروني"> @error('order.customer.email') <span class="error">{{ $message }}</span> @enderror <!-- حقول العنوان مع الربط العميق --> <input type="text" wire:model="order.customer.address.street" placeholder="عنوان الشارع"> <input type="text" wire:model="order.customer.address.city" placeholder="المدينة"> <select wire:model="order.customer.address.country"> <option value="">اختر الدولة</option> <option value="us">الولايات المتحدة</option> <option value="uk">المملكة المتحدة</option> </select> </div> <!-- عناصر الطلب --> <div class="section"> <h3>عناصر الطلب</h3> @foreach($order['items'] as $index => $item) <div class="item-row" wire:key="item-{{ $index }}"> <select wire:model="order.items.{{ $index }}.product_id"> <option value="">اختر المنتج</option> @foreach($products as $product) <option value="{{ $product->id }}">{{ $product->name }}</option> @endforeach </select> <input type="number" wire:model="order.items.{{ $index }}.quantity" min="1"> <button type="button" wire:click="removeItem({{ $index }})"> إزالة </button> </div> @endforeach <button type="button" wire:click="addItem">إضافة عنصر</button> </div> <!-- الإجمالي --> <div class="total"> الإجمالي: ${{ number_format($this->calculatedTotal, 2) }} </div> <button type="submit" wire:loading.attr="disabled"> <span wire:loading.remove>إرسال الطلب</span> <span wire:loading>جارٍ المعالجة...</span> </button> </form> </div>
نصيحة: استخدم wire:model.defer لتجميع التحديثات وتقليل طلبات الخادم، wire:model.lazy للتحديث عند الضبابية، وwire:model (أو wire:model.live في الإصدار 3) للتحديثات في الوقت الفعلي.

التحميل المؤجل مع العناصر النائبة

تحسين الأداء المتصور عن طريق تحميل المكونات الثقيلة بعد عرض الصفحة الأولي:

<?php namespace App\Http\Livewire; use Livewire\Component; use App\Models\Report; class AnalyticsDashboard extends Component { public $readyToLoad = false; public $selectedPeriod = '30days'; // تحميل البيانات فقط عندما يصبح المكون مرئيًا public function loadData() { $this->readyToLoad = true; } public function render() { return view('livewire.analytics-dashboard', [ 'analytics' => $this->readyToLoad ? $this->getAnalytics() : null, ]); } private function getAnalytics() { // استعلام مكلف sleep(2); // محاكاة حساب ثقيل return [ 'revenue' => Report::revenue($this->selectedPeriod), 'orders' => Report::orders($this->selectedPeriod), 'customers' => Report::customers($this->selectedPeriod), 'chart_data' => Report::chartData($this->selectedPeriod), ]; } }

عرض Blade مع عنصر نائب للتحميل:

<!-- resources/views/livewire/analytics-dashboard.blade.php --> <div> <h2>لوحة تحكم التحليلات</h2> <select wire:model="selectedPeriod" wire:change="loadData"> <option value="7days">آخر 7 أيام</option> <option value="30days">آخر 30 يومًا</option> <option value="90days">آخر 90 يومًا</option> </select> @if($readyToLoad) @if($analytics) <div class="stats-grid"> <div class="stat-card"> <h3>الإيرادات</h3> <p>${{ number_format($analytics['revenue'], 2) }}</p> </div> <div class="stat-card"> <h3>الطلبات</h3> <p>{{ $analytics['orders'] }}</p> </div> <div class="stat-card"> <h3>العملاء</h3> <p>{{ $analytics['customers'] }}</p> </div> </div> @else <div wire:loading> <div class="spinner"></div> <p>جارٍ تحميل التحليلات...</p> </div> @endif @else <div class="placeholder"> <button wire:click="loadData">تحميل التحليلات</button> </div> @endif </div>

الاستطلاع للتحديثات في الوقت الفعلي

قم بتحديث بيانات المكون تلقائيًا على فترات زمنية:

<?php namespace App\Http\Livewire; use Livewire\Component; use App\Models\Order; class OrderMonitor extends Component { public $pollingInterval = 5000; // ميلي ثانية public $isPolling = true; public function togglePolling() { $this->isPolling = !$this->isPolling; } public function render() { return view('livewire.order-monitor', [ 'pendingOrders' => Order::pending()->latest()->take(10)->get(), 'activeOrders' => Order::active()->count(), ]); } }
<!-- عرض Blade مع الاستطلاع --> <div wire:poll.{{ $isPolling ? $pollingInterval : 'keep-alive' }}ms> <div class="header"> <h2>مراقب الطلبات</h2> <button wire:click="togglePolling"> {{ $isPolling ? 'إيقاف مؤقت' : 'استئناف' }} المراقبة </button> <span class="badge">{{ $activeOrders }} نشط</span> </div> <div class="orders-list"> @foreach($pendingOrders as $order) <div class="order-card" wire:key="order-{{ $order->id }}"> <h4>الطلب #{{ $order->id }}</h4> <p>{{ $order->customer_name }}</p> <span class="status">{{ $order->status }}</span> <time>{{ $order->created_at->diffForHumans() }}</time> </div> @endforeach </div> <div wire:loading.delay> <span class="updating-indicator">جارٍ التحديث...</span> </div> </div> <!-- استطلع إجراءً محددًا بدلاً من المكون بأكمله --> <div> <button wire:click="checkNewOrders" wire:poll.5s="checkNewOrders"> تحقق من الطلبات الجديدة </button> </div>
تحذير: يمكن أن يزيد الاستطلاع من حمل الخادم بشكل كبير. استخدمه باعتدال وفكر في WebSockets (Laravel Echo + Pusher/Soketi) للتحديثات في الوقت الفعلي عالية التردد.

تحميلات الملفات مع التقدم

التعامل مع تحميلات الملفات مع تتبع التقدم في الوقت الفعلي والتحقق من الصحة:

<?php namespace App\Http\Livewire; use Livewire\Component; use Livewire\WithFileUploads; use Illuminate\Support\Facades\Storage; class DocumentUploader extends Component { use WithFileUploads; public $document; public $documents = []; public $uploadProgress = 0; protected $rules = [ 'document' => 'required|file|max:10240|mimes:pdf,doc,docx', // 10 ميغابايت كحد أقصى ]; // التحقق في الوقت الفعلي أثناء التحميل public function updatedDocument() { $this->validate([ 'document' => 'required|file|max:10240|mimes:pdf,doc,docx', ]); } public function save() { $this->validate(); $filename = $this->document->getClientOriginalName(); $path = $this->document->storeAs( 'documents', $filename, 'public' ); // حفظ في قاعدة البيانات auth()->user()->documents()->create([ 'filename' => $filename, 'path' => $path, 'size' => $this->document->getSize(), 'mime_type' => $this->document->getMimeType(), ]); // مسح إدخال الملف $this->document = null; $this->uploadProgress = 0; session()->flash('message', 'تم تحميل المستند بنجاح!'); } public function deleteDocument($documentId) { $document = auth()->user()->documents()->findOrFail($documentId); Storage::disk('public')->delete($document->path); $document->delete(); $this->emit('documentDeleted'); } public function render() { return view('livewire.document-uploader', [ 'uploadedDocuments' => auth()->user()->documents()->latest()->get(), ]); } }
<!-- عرض Blade مع تقدم التحميل --> <div> <form wire:submit.prevent="save"> <div class="upload-area"> <input type="file" wire:model="document" id="document" accept=".pdf,.doc,.docx"> <label for="document"> <span wire:loading.remove wire:target="document"> اختر مستندًا </span> <span wire:loading wire:target="document"> جارٍ التحميل... </span> </label> @error('document') <span class="error">{{ $message }}</span> @enderror </div> <!-- تقدم التحميل --> <div wire:loading wire:target="document" class="progress-bar"> <div class="progress-fill" x-data="{ progress: 0 }" x-on:livewire-upload-progress="progress = $event.detail.progress" :style="`width: ${progress}%`"> </div> <span x-text="`${progress}%`"></span> </div> @if($document) <div class="file-preview"> <p>المحدد: {{ $document->getClientOriginalName() }}</p> <p>الحجم: {{ round($document->getSize() / 1024, 2) }} كيلوبايت</p> </div> <button type="submit" wire:loading.attr="disabled" wire:target="document,save"> تحميل المستند </button> @endif </form> <!-- قائمة المستندات المحملة --> <div class="documents-list"> @foreach($uploadedDocuments as $doc) <div class="document-item" wire:key="doc-{{ $doc->id }}"> <span>{{ $doc->filename }}</span> <button wire:click="deleteDocument({{ $doc->id }})" wire:confirm="هل أنت متأكد من أنك تريد حذف هذا المستند؟"> حذف </button> </div> @endforeach </div> </div>

تكامل Alpine.js

ادمج تفاعلية الخلفية لـ Livewire مع Alpine.js للتفاعلات الأمامية السلسة:

<?php namespace App\Http\Livewire; use Livewire\Component; class SearchableDropdown extends Component { public $search = ''; public $selected = null; public function selectItem($itemId) { $this->selected = $itemId; $this->emit('itemSelected', $itemId); } public function render() { $items = $this->search ? Item::where('name', 'like', "%{$this->search}%")->take(10)->get() : []; return view('livewire.searchable-dropdown', ['items' => $items]); } }
<!-- Alpine.js + Livewire مجتمعان --> <div x-data="{ open: false }" @click.away="open = false"> <div class="dropdown"> <input type="text" wire:model.debounce.300ms="search" @focus="open = true" placeholder="ابحث عن العناصر..."> <div x-show="open" x-transition class="dropdown-menu"> @if($items->isNotEmpty()) @foreach($items as $item) <div class="dropdown-item" wire:click="selectItem({{ $item->id }})" @click="open = false"> {{ $item->name }} </div> @endforeach @else <div class="no-results"> لم يتم العثور على عناصر </div> @endif </div> </div> </div> <!-- مودال مع رسوم متحركة Alpine.js --> <div x-data="{ showModal: @entangle('showModal') }"> <button @click="showModal = true">فتح المودال</button> <div x-show="showModal" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform scale-90" x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100 transform scale-100" x-transition:leave-end="opacity-0 transform scale-90" class="modal-overlay"> <div class="modal-content"> <h2>عنوان المودال</h2> <p>محتوى المودال هنا</p> <button wire:click="save" @click="showModal = false"> حفظ وإغلاق </button> </div> </div> </div>
نصيحة: استخدم @entangle() لإنشاء ربط بيانات ثنائي الاتجاه بين Livewire و Alpine.js. يتيح هذا لـ Alpine قراءة وتحديث خصائص Livewire دون إجراء طلبات خادم.
تمرين 1: قم ببناء مكون قائمة المهام حيث يدير المكون الأصلي قائمة المهام وتعرض المكونات الفرعية عناصر المهام الفردية. قم بتنفيذ: إضافة مهمة (أصلي)، تبديل الاكتمال (الفرعي يصدر إلى الأصلي)، حذف مهمة (الفرعي يصدر إلى الأصلي)، وتصفية البحث في الوقت الفعلي.
تمرين 2: أنشئ نموذجًا متعدد الخطوات مع تحميل مؤجل لكل خطوة. الخطوة 1: المعلومات الشخصية، الخطوة 2: العنوان (يتم التحميل عند النقر)، الخطوة 3: الدفع (يتم التحميل عند النقر). أضف شريط تقدم النموذج والتحقق من الصحة الذي يمنع التقدم إذا كانت الخطوة الحالية بها أخطاء.
تمرين 3: قم ببناء محمل معرض الصور الذي يسمح بتحميلات ملفات متعددة مع أشرطة التقدم الفردية. اعرض الصور المصغرة بعد التحميل، واسمح بإعادة الترتيب بالسحب والإفلات (Alpine.js)، والحذف الجماعي. أضف مودال معاينة الصورة مع رسوم متحركة Alpine.js عند النقر على صورة مصغرة.

الملخص

في هذا الدرس، تعلمت:

  • اتصال المكونات المتداخلة بالأحداث والمستمعين
  • ربط wire:model العميق لهياكل البيانات المعقدة
  • أنماط التحميل المؤجل لتحسين الأداء
  • الاستطلاع في الوقت الفعلي للتحديثات المباشرة
  • تحميلات الملفات مع تتبع التقدم
  • تكامل Alpine.js لتحسين التفاعلية الأمامية