Laravel Livewire

Makale İçerik Listesi

Kurulum

  1. Öncelikle yeni bir proje oluştur. laravel new livewire
  2. Composer install Livewire. Terminalde composer require livewire/livewire

İlk Livewire Komponentim

  1. Sayfaya hello-world adında bir livewire component'i ekleyeceğim <livewire:hello-world />
  2. Bu komponenti projeye eklemek için terminalde php artisan make:livewire hello-world
  3. Bunu yapınca 2 dosya oluşturuldu.
    1) Class dosyası: app/Livewire/HelloWorld.php içinde
    2) View dosyası: resources/views/livewire/hello-world.blade.php içinde
  4. Class dosyasından birşey yapmıyorum. View dosyasını da en basit haliyle normal blade dosyası olarak kullanabilirim.
    <div>
        Current time is {{ time() }}
    </div>
  5. Ama buna biraz daha interaction eklemek istersem bir buton'a wire: attribute'u üzerinden $refresh magic action ekleyebilirim. Bu sayede SADECE KOMPONENT refresh olur.
    <div>
        Current time is {{ time() }}
        <button wire:click="$refresh">Refresh</button>
    </div>

Actions

  1. Counter adında yeni bir component yaptım php artisan make:livewire counter
  2. En basit haliyle view dosyası şu şekilde 
    <div>
        Count: 1
    </div>
  3. Ama buradaki 1 değerinin statik değil dinamik olmasını istiyorum. O zaman bunu Class dosyasında bir property olarak yazmam lazım. 
    class Counter extends Component
    {
        public $count = 1;
        
        public function render()
        {
            return view('livewire.counter');
        }
    }

    Bu şekilde belirttiğimde artık view dosyasında da bu değişkene bakabiliyorum. 

    <div>
        Count: {{ $count }}
    </div>
  4. Burada dinamiklik yapmak istersek artık wire:click="methodAdı" diyerek belirttiğimiz metodu php Class üzerinde logic'i ile çalıştırabiliriz. Yani view: 
    <div>
        Count: {{ $count }}
        <button wire:click="increment">+</button>
    </div>

    Counter.php class dosyasında 

    public function increment()
    {
       $this->count++;
    }

    dediğimizde dinamiklik oluşuyor.

  5. Burada wire:click dedik ama bütün browser eventleri olabilir. wire:click, wire:mouseenter, change, submit, mouseleave, custom-event
  6. Ayrıca modifier'lar da ekleyerek kullanılabilir. Yani wire:click.window dediğimizde window level click dinlenir.
  7. Farklı bir modifier wire:click.throttle.1000ms Bu şekilde yaptığımda ne kadar basarsam basayım her saniyede bir click kabul eder.
  8. Passing parameters Bunu da yapabilirim. 
    <div>
        Count: {{ $count }}
        <button wire:click="increment(2)">+</button>
    </div>
    public function increment($by)
    {
       $this->count = $this->count + $by;
    }
  9.  Daha fazla bilgi için - doc link

Properties

  1. Livewire'da propertyler de çok önemli. Bunu yine Class içinde tanımlıyorum. 
    public $todos = [
       'Take out trash',
       'Do dishes',  
    ];
  2. Sonra View'da normal bir şekilde kullanabiliyorum. 
    <div>
        <ul>
            @foreach($todos as $todo)
                <li>{{ $todo }}</li>
            @endforeach
        </ul>
    </div>

Supercharged Property: Model

  1. Aslında Class içinde bir property
  2. Livewire ile 2-Way-Databinding yapmanı sağlıyor. Yani Backend'deki prop Frontend ile Sync. Birinde değiştirirsen, değişiklik diğerine yansıyor.
  3. Bunu yapmak için x bir elemente wire:model demen gerekiyor.
  4. Ayrıca modifierlarla da kullanılabiliyor. 
    <input type="text" wire:model="todo">  {{-- most basic --}}
    <input type="text" wire:model.live="todo"> {{-- her değişimde network request yapmasını istersen --}}
    <input type="text" wire:model.live.debounce.700ms="todo">  {{-- değişim sonrası pasifleşmeye delay --}}
    <input type="text" wire:model.live.throttle.5ms="todo">
    <input type="text" wire:model.change="todo">
    <input type="text" wire:model.blur="todo">

Cookbook: Todo Example

Class 

public $todo = '';
public $todos = [
  'Take out trash',
  'Do dishes',
];

public function add()
{
    $this->todos[] = $this->todo;
    //  $this->todo = '';
    $this->reset('todo');
}

View 

<form wire:submit="add">
    <input type="text" wire:model="todo">
    <input type="text" wire:model.live="todo">
    <input type="text" wire:model.live.debounce.5ms="todo">
    <input type="text" wire:model.live.throttle.5ms="todo">
    <input type="text" wire:model.change="todo">
    <input type="text" wire:model.blur="todo">
    <button type="submit" >Add</button>
</form>

<ul>
    @foreach($todos as $todo)
        <li>{{ $todo }}</li>
    @endforeach
</ul>

Lifecycle Hooks

  1. Componentlerin birçok lifecycle hookları var. Ama bunlardan en popülerleri mount ve updated
  2. MOUNT Class'e eklenir ve komponent mounted olduğu anda çalışır.
    public $todos = [];
    
    public function mount()
    {
       $this->todos = Todo::where('is_done', false)->get();
    }
  3. UPDATED Class'e eklenir ve komponent propertylerinde wire:model ile bağlı olan herhangi bir model değiştiğinde çalışır.
    public function updated($property, $value)
    {
       $this->$property = strtoupper($value);
       dd($property, $value);
    }
  4. Eğer UPDATED'ı BELLİ BİR PROPERTY'ye SPESİFİK olarak çalıştırmak istersen updatedPropertyName($value) syntax'i kullanabilirsiniz. (Bu durumda parametre olarak $property gelmiyor. Sadece $value geliyor. Zaten property'nin ne olduğunu biliyorsun.) 
    public function updatedTodo($value)
    {
       // $this->validate();
       $this->todo = strtoupper($value);
    }

Page Components

    1. PAGE COMPONENT MANTIĞI
      Şu ana kadarki bilgimizle yaptığımız şey... Öncelikle bir route'umuz var. 
      Route::get('/', [VisitorController::class, 'index']);

       

      Sonra bunun Controller'ı var VisitorController ve onun içinde index fonksiyonu. 

      public function index()
      {
         return view('todo-index');
      }

      Sonra da todo-index.blade.php dosyası var ve onun içine eklediğimiz todo component'imiz var.

      <!DOCTYPE html>
      <html lang="tr">
          <head>
              <meta charset="utf-8">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <title>Livewire Öğrenme</title>
          </head>
          <body>
              <livewire:counter />
          </body>
      </html>
      

      Ve bütün bunlara ek olarak componentin controller logic'ini taşıyan Livewire component class dosyası

    2. (Counter.php

) ve onun görüntüsü blade dosyası (counter.blade.php) dosyaları var. Bu noktada Laravel Controller, Laravel Controller function ve Laravel View dosyalarını bypass ederek direkt Livewire componenti page component yapsak ne kaybederiz? Cevap: Hiçbirşey!

  1. X bir view içerisine livewire componentleri atabildiğin gibi, tüm route'u da direkt bir page Component olarak gösterebiliriz. Bunu route dosyasında şu şekilde gösteriyoruz.
    Route::get('/', Todo::class);
    Route::get('/counter', Counter::class);
  2. Ama bu şekilde yaptığımızda Component'in içine oturacağı bir shell blade dosyası olmadığı için hata verir. Bunu aşmak için en basit bir boilerplate'i php artisan livewire:layout commandi ile yapabiliriz. (veya kendimiz de views/components/layouts/app.blade.php) dosyasını oluşturabiliriz. Page Component shell'i olan default app.blade.php dosyası şu şekilde: 
    <!DOCTYPE html>
    <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
            <title>{{ $title ?? 'Page Title' }}</title>
        </head>
        <body>
            {{ $slot }}
        </body>
    </html>

    {{ $slot }} kısmı componentin oturacağı alan. Bunu istediğim gibi güncelleyebilirim.

  3. PHP Attributeları ile her route'a özelleştirme yapmak
    Mesela Layout blade dosyasında komponentler üstü bir özelliştirme yapmam gerekiyor. {{ $slot }} kısmı, komponentin etki alanı. Ama o alanın dışındaki $title'a da komponent'in php attributeları ile etki edebilirim. Bunun için class'ın dışına #[Title('Counter')] ile bir attribute belirlemem yeterli.
    #[Title('Counter')]
    
    class Counter extends Component
    {

    Dediğimde sayfada komponent slot'unun dışında $title'a da ekleyebiliyorum.

Page Component'larda Slot Dışına Erişim

  1. app.blade.php layout'u içine eklenmiş Page Componentlerim var. Ve componentler slot içinde yaşıyor. Ama her sayfada mesela metadata kısmını değiştirmek istiyorum. Bunu nasıl yapabilirim?

1. YOL: Layout dosyasında @yield() Component dosyasında @section kullanarak

  1. Layout dosyasında @yield ile alan belirle:
    app.blade.php
    <head>
        <title>@yield('title', 'Default Title')</title>
        <meta name="description" content="@yield('meta_description', 'Default description')">
    </head>
    <body>
        {{ $slot }} <!-- Livewire components get injected here -->
    </body>
  2. Component view dosyasında @section ile değer ilet
    @extends('layouts.app')
    
    @section('title', 'Dynamic Page Title')
    @section('meta_description', 'This is a dynamic meta description for SEO.')
    
    <div>
        <h1>My Livewire Component</h1>
        <p>Content goes here...</p>
    </div>
  3. Bu yöntem statik kalacak şeyler için en temiz çözüm. Ama bazen de bu değerin değişken olmasını isteyebilirim.
    (Ör. sipariş geldiğinde title değişsin istiyorum.) O zaman Livewire için property olarak eklemek gerekir. 
    namespace App\Http\Livewire;
    
    use Livewire\Component;
    
    class MyComponent extends Component
    {
        public $title = "Dynamic Page Title";
        public $metaDescription = "This is a dynamic meta description for SEO.";
    
        public function render()
        {
            return view('livewire.my-component')
                ->with([
                    'title' => $this->title,
                    'metaDescription' => $this->metaDescription,
                ]);
        }
    }

    bu durumda app.blade.php dosyasında da değişken olarak göstermem gerekir ve duruma göre değişebilir hale gelir. 

    <head>
        <title>{{ $title ?? 'Default Title' }}</title>
        <meta name="description" content="{{ $metaDescription ?? 'Default description' }}">
    </head>
    <body>
        {{ $slot }}
    </body>
  4. Bu iş ayrıca @stach @push ile de yapılabiliyor. O durumda component yüklendiğinde o da görünüyor. Daha kompleks bir senaryoda kullanılabilir.

Antipattern warning: DB as Prop

  1. Örneğin POST'ları göstereceğin bir komponent var ve şunu yapmak mantıklı geliyor. 
    class ShowPosts extends Component
    {
       public $posts;
    
       public function mount()
       {
          $this->posts = Post::all();
       }
    
       public function render()
       {
          return view('livewire.show-posts');
       }
    }

    ama bunu yapınca mount anında çektiğin Posts'u hard-coded olarak kabul ediyorsun ve sadece bir property'ye hapsederek onunla çalışıyorsun.
    Halbuki senden başkası bir değişiklik yaparsa o zaman bu gerçeği yansıtmıyor oluyor. Direkt property olarak almak yerine computed property olarak alabilirsin veya direkt render içinde view'e ekle: 

    class ShowPosts extends Component
    {
       public function render()
       {
          return view('livewire.show-posts', [
             'posts' => Post::all(),
          ]);
       }
    }

    Böyle olunca livewire her component render ettiği anda DB'den taze olarak Post'ların son halini çeker.

Tables

  1. Postların olduğu bir sayfa yapıyoruz. 
    class ShowPosts extends Component
    {
       public function render()
       {
          return view('livewire.show-posts', [
             'posts' => Post::all(),
          ]);
       }
    }

    View sayfasında tablo yapıyorum. Ama çok önemli!!! Ne zaman foreach'le birşeyleri listelersem listeleyeyim... mutlaka key vermem lazım. 

    <table>
       <tbody>
          @foreach($posts as $post)
             <tr wire:key="{{ $post->id }}">
                <td>{{ $post->title }}</td>
                <td>{{ str($post->content)->words(8) }}</td>
                <td><button type="button" 
                  wire:click="delete({{$post->id}})"
                  wire:confirm="Are you sure you want to delete this post?">
                     Delete</button>
                </td>
             </tr>
          @endforeach
       </tbody>
    </table>

    Componentte delete fonksiyonunu oluştur.

    public function delete(Post $post)
    {
       $post->delete();
    }

    GÜVENLİK AÇIĞI: Kullanıcı browser üzerinden deleteId'yi değiştirebilir. Yanlış birşeylerin silinmesini önlemek için normal bir controller'da yaptığın gibi validation yapman lazım. Bu kişi buna yetkili mi???

Forms

  1. Çoğu uygulama baktığında aslında birçok form ile etkileşime giren sayfalardan ibarettir 
    <form wire:submit="save">
       <label>
          <span>Title</span>
          <input type="text" wire:model="title">
          @error('title')<em>{{ $message }}</em>@enderror
       </label>
       <label>
          <span>Content</span>
          <textarea wire:model="content"></textarea>
          @error('content')<em>{{ $message }}</em>@enderror
       </label>
    
       <button type="submit">Save</button>
    </form>
  2. CreatePost Page componenti 
    class CreatePost extends Component
    {
       public $title = '';
       public $content = '';
    
       public function save()
       {
          $this->validate([
             'title' => 'required',
             'content' => 'required',
          ]);
    
          Post::create([
             'title' => $this->title,
             'content' => $this->content,
          ]);
       }
    }

    veya validation'daki validation notlarını property declaration da yapabiliriz. Bu durumda fonksiyon içinde sadece $this->validate() dememiz yeterli olur.

    class CreatePost extends Component
    {
       #[Rule('required', as: 'Makale başlığı', message: __('myapp.validations.title_required'))]
       #[Rule('min:4', message: __('myapp.validations.too_short'))]
       public $title = '';
    
       #[Rule('required')]
       public $content = '';
    
       public function save()
       {
          $this->validate();
    
          Post::create([
             'title' => $this->title,
             'content' => $this->content,
          ]);
       }
    }

Alpine.js

  1. Alpine.js aslında Livewire'ın çalışabilmesi için hazırlanmış bir Frontend Framework.
  2. Livewire'dan farkı pure javascript... pure frontend... tamamen ön tarafta çalışıyor.
  3. Zaten Livewire içinde olduğu, Livewire sayfada varsa direkt... yoksa cdn'den alpine.js'i çektiğinde çalışıyor. 
    <script src="//unpkg.com/alpinejs" defer></script>
    
    <div x-data="{ count: 10}">
        <span x-text="count"></span>
        <button x-on:click="count++">+</button>
    </div>

     

  4. Alpine.js ile Livewire componentin ile direkt bağ kurabilirsin 

    Current Title: <span x-text="$wire.title"></span>

    Livewire componentinin bütün proplarına $wire. üzerinden erişim sağlayabilirsin.

    Current Title: <span x-text="$wire.title.toUpperCase() + ' Title'"></span>

    Başka interaction türleri 

    <button x-text="$wire.title = ''">Clear Title</span>

    Hatta Livewire componentindeki metodları bile çağırabilirsin. 

    <button x-text="$wire.save()">Save Form</span>
  5. Örneğin bir karakter counter yapılabilir. 
    <textarea wire:model="content"></textarea>
    <small>Characters: <span x-text="$wire.content.length"></span></small>

Kaynak

  1. Livewire Screencasts - link
  2. Livewire Documentation - link
  3. Alpine.js - link