Laravel + Inertia.js

Makale İçerik Listesi

Monolith Mimari vs. Modern SPA Ekosistemi

  1. Monolith yazılım mimarisinde "yekpare" bir proje yönetimi esas alınır. ör. Laravel framework'ü ile proje geliştirildiği zaman projede, bir route mekanizması vardır ve bu routelar controller üzerinden modeller ile database'ten veri akışını kullanıcıya görüntü şablonları ile aktarır. Authorization vs. herşeyi ile proje tek bir çerçeve içinde gelişir.
  2. Modern SPA uygulamasına ihtiyaç duyulan hallederse ise olay bir anda çok daha komplike bir hal almakta. Burada Laravel veya Rails gibi Backend Framework'leri Database'ten veri çekip, atanmış routelara veriyi aktarma görevini görevini üstlenmekle kalır.
    Modern SPA uygulamaları bunun haricinde bütün yükü üstlendiği için çoğu zaman buraya kadar olan veri aktarımı (API) ayrı bir proje olarak yürütülür. Frontend Framework'ü kullanıcıya gösterilecek ekranları ayrıca kendi router'ı ile ayarlar, uygulamanın state managementı ve görüntü şablonları, authentication gerektiren durumlarda authorization process'i hep Frontend Framework'üne bağlı olarak ayrıca yapılır.
    Yani proje; sadece API olan Backend'in ayrı, kullanıcı tarafındaki herşeyin ayrı düşünülmesi gereken ayrı bir projeye dönüşür.
  3. Yani bugün Monolith yazılımdan, sadece performans ve ekranlar arası belli interaktivite özelliklerini projeye kazandırmak adına Modern bir SPA uygulamasına geçiş arasında ciddi bir yapısal değişiklik ve ciddi bir komplikasyon katmanı işin içine girmektedir.

Inertia.js

  1. Inertia.js'in sloganı "Modern Monolith"tir. Amaç; SPA uygulamalarının sunduğu interaktivite imkanlarını, şimdiki kadar bir komplikasyon katmanı eklemeden yapabilmektir.
  2. Burada Inertia.js ne Backend Framework'ü, ne de bir Frontend Framework'ü yerine geçer.
  3. Tercih edilen Backend Framework'ünü ve Frontend Framework'ünü Backend'in imkanlarından mahrum etmeden tek bir proje içerisinde rahatça kullanma imkanı sağlayan bağlaç görevi görür.
  4. Inertia.js kullanılan bir projede Backend Framework'ü modelleri, controllerları, routeları ve authentication katmanı ile aynen kalır.
    Inertia.js route değişiklikleri sırasında; Backend Framework'ünün klasik olarak yaptığı gibi, veriyi alıp template içinde ön tarafta göstermesi yerine, araya girerek (Middleware/HandleInertiaRequests.php) sadece o route'un ihtiyacı olan veriyi ve kullanılması gereken Frontend Komponentini (sayfasını) ön tarafa bildirir ve aradan çekilir.
    Frontend Framework'lerinin temel özelliği olan dinamik komponent değişikliği sayesinde mevcut komponent ve yenisi yer değiştirir ve gerekli veriler yeni komponente prop olarak yüklenir.
  5. Frontend Framework'ü bu noktadan sonra ön tarafta kendi üstüne düşen interaktiviteyi sunmaya devam eder.

Inertia.js'in Ne Faydası var?

  1. Bir API oluşturmaya gerek yok... Backend Controllerlarından gelen veri direk kullanılır.
  2. Authentication katmanı Backend Frameworkü tarafından yapılmaya devam eder. OAuth gibi birşeye ihtiyaç kalmaz.
  3. Daha çok klasik Backend yazılımcıları için.
  4. Backend ağırlıklı Fullstack developerlar için.
  5. Tek başına proje yapanlar için... Logic tarafına Laravel üzerinden konsatre olmak = daha hızlı prod ready iş çıkartmak. Vue SPA için gerekli olan kurulumi router ve state managementa ihtiyacın yok.
  6. Inertia.js proje üstünde çok hafif bir katman. Kaynak kodu dahi 400+ LoC.

Kurulum

  1. Yeni bir Laravel projesi oluştur ve içine gir.
  2. Server-side adaptörünü kurmak için (ref.link) Composer üzerinden öncelikle Inertia.js'i projeye dahil et. 
    composer require inertiajs/inertia-laravel
  3. Temel template'imizi resources/views içerisinde oluştur. Laravel'de bu varsayılan olarak app.blade.php içine yazılması gerekir. Bu dosyada statik assetlerin ve en önemlisi @inertia direktifinin olması gerekiyor.
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
        <link href="{{ mix('/css/app.css') }}" rel="stylesheet" />
        <script src="{{ mix('/js/app.js') }}" defer></script>
      </head>
    <body>
    @inertia
    </body>
    </html>

    Eğer app.blade.php yerine başka bir dosya adı, başka bir klasör kullanmak istersen bunu Inertia::setRootView() fonsiyonundan değiştirebilirsin.

  4. Normal requestleri Inertia.js'in XHR requestlere dönüştürebilmesi için Inertia Middleware'ini kurmamız lazım. Terminale aşağıdaki komutu yazıyorum.
    php artisan inertia:middleware
  5. Middleware'i oluşturduktan sonra App\Http\Kernel dosyasında HandleInertiaRequest'i web middleware group'uma ekliyorum. 
    'web' => [
        // ...
        \App\Http\Middleware\HandleInertiaRequests::class,
    ],
  6. Client-side adaptörünü kurmak için (ref.link) (Vue 3'ü kullandığımızı düşünürsek)
  7. Öncelikle Vue 3'ü projeye yüklemeliyiz.()
    npm install vue@next
  8. Inertia.js'in Vue apatörü için 
    npm install @inertiajs/inertia @inertiajs/inertia-vue3
  9. Laravel projemiz içinde resources/js klasöründe bulunan bootstrap.js dosyasına ihtiyacımız yok. Silebiliriz.
  10. Aynı klasörde bulunan app.js dosyasında ise Inertia.js'in başlatabiliriz. 
    import { createApp, h } from 'vue'
    import { createInertiaApp } from '@inertiajs/inertia-vue3'
    
    createInertiaApp({
      resolve: name => require(`./Pages/${name}`),
      setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
          .use(plugin)
          .mount(el)
      },
    })

    burada gördüğümüz gibi Inertia.js web uygulamamızın sayfalarını resources/js/Pages içerisinde arayacak. O nedenle bu klasörü de oluşturmamız gerekiyor.

  11. Son olarak yaptığımız projeyi compile etmemiz gerekiyor. webpack.mix.js dosyasında app.js dosyasının Vue 3 ile compile edilmesi gerektiğini söylüyoruz .vue(3) ve browser cache problemi yaşamamak için .version() fonksiyonunu kullanıyoruz.
    const mix = require('laravel-mix');
    
    mix.js('resources/js/app.js', 'public/js')
        .vue(3)
        .postCss('resources/css/app.css', 'public/css', [
            //
        ])
        .version();
    
  12. NPM tarafında bir kaynak yüklemek gerekiyorsa npm install diyerek yüklüyoruz sonrasında da npx mix diyerek webpack mix'i çalıştırıyoruz.

Sayfa Oluşturma

  1. Inertia.js ile gösterilecek sayfalar Vue dosyası olarak resources/js/Pages klasöründe oluşturacağız (ör. Home.vue) 
    <template>
       <h1>Hello, {{ name }}</h1>
       <ul>
          <li v-for="framework of frameworks" v-text="framework"></li>
       </ul>
    </template>
    
    <script>
       export default {
          props: {
             name: String,
             frameworks: Array,
          }
       }
    </script>
  2. Routes dosyasında ana sayfanın Home.vue olacağını söylemem gerekiyor. Backend tarafından datayı yolladığımızda bunu kullanmak için Vue tarafında prop olarak almamız gerekiyor.
    use Illuminate\Support\Facades\Route;
    use Inertia\Inertia;
    
    Route::get('/', function() {
       return Inertia::render('Home', [
          'name' => 'H. Omer Sensoy',
          'frameworks' => ['Laravel', 'Vue', 'Inertia']
       ]);
    });
  3. Çalışırken değişiklikler sırasında sürekli compile etmesi için mix watch komutunu kullanıyorum. 
    npx mix watch

XHR Bağlantı Linkleri

  1. SPA uygulamasında sayfalar arası bağlantı yapmadan önce farklı ekranlar oluşturalım web.php 
    Route::get('/', function() {
       return Inertia::render('Home');
    });
    
    Route::get('/users', function() {
       return Inertia::render('Users');
    });
    
    Route::get('/settings', function() {
       return Inertia::render('Settings');
    });
  2. Home.vue, Users.vue ve Settings.vue dosyalarını resources/js/Pages klasöründe oluşturuyorum.
  3. Sayfalar arası bir bağlantı olması için Inertia'nın <Link> direktifini kullanmam gerekiyor.
    <nav>
       <ul>
          <li><a href="#">Home</a></li>
          <li><a href="#">Users</a></li>
          <li><a href="#">Settings</a></li>
       </ul>
    </nav>

    bağlantıyı <a> olarak verirsen olmaz. Inertia'nın araya girmesini istiyorsak Inertia'nın <Link> komponentini kullanmamız lazım. 

    <template>
        <nav>
           <ul>
              <li><Link href="/">Home</Link></li>
              <li><Link href="/users">Users</Link></li>
              <li><Link href="/settings">Settings</Link></li>
           </ul>
        </nav>
    </template>
    
    <script>
        import { Link } from '@inertiajs/inertia-vue3'
    
        export default {
           components: { Link },
        }
    </script>

Paylaşımlı Komponentler

  1. resources/js/Shared diye yeni bir klasör oluştur ve Navigation'ı orada bir component olarak oluştur. Nav.vue 
    <template>
        <nav>
           <ul>
              <li><Link href="/">Home</Link></li>
              <li><Link href="/users">Users</Link></li>
              <li><Link href="/settings">Settings</Link></li>
           </ul>
        </nav>
    </template>
    
    <script>
        import { Link } from '@inertiajs/inertia-vue3'
    
        export default {
           components: { Link },
        }
    </script>
  2. Bu şekilde navigasyon kendi komponentinde olduktan sonra bütün sayfalarda kullanabilirim ör. Home.vue 
    <template>
        <h1>Home</h1>
        <Nav />
    </template>
    
    <script>
        import Nav from '../Shared/Nav'
    
        export default {
           components: { Nav },
        }
    </script>

    aynısını Users.vue ve Settings.vue için de yap.

Progress Bar

  1. Sayfalar arası geçiş artık tamamen Frontend tarafında yapıldığı için uzun süren işlemlerde tarayıcı tarafında işlem yapıldığını gösteren hiçbirşey yok. Bunun için Progress indicator kullanabiliriz.
  2. Progress indicator'u projemize dahil etmek için yapmamız gereken öncelikle yüklemek 
    npm install @inertiajs/progress
  3. Yükledikten sonra uygulama ayağa kalkarken progress indicator'u da çalıştırmamız gerekiyor. 
    import { createApp, h } from 'vue'
    import { createInertiaApp } from '@inertiajs/inertia-vue3'
    + import { InertiaProgress } from '@inertiajs/progress'
    
    createInertiaApp({
        resolve: name => require(`./Pages/${name}`),
        setup({ el, App, props, plugin }) {
            createApp({ render: () => h(App, props) })
                .use(plugin)
                .mount(el)
        },
    });
    
    + InertiaProgress.init();

Inertia ile Post Request

  1. web.php dosyasında bir POST request route'u oluşturduk. 
    ...
    Route::post('/logout', function () {
        dd('Logged out user');
    });
  2. Ama bu route'a navigation içerisinde <Link> ile yönlendirme yapmak istesek o zaman GET requesti yapmaya çalışacak... Hata verir! Bunun olmaması için method="post" diye attribute eklememiz lazım. 
    <Link href="/logout" method="post">Logout</Link>
  3. Ama Link neticede <a> için bir wrapper olduğundan CMD + Click yaptığında ayrı bir sekmede sayfayı açmaya çalışır ve yine hata verir. O nedenle Link'i nasıl <a> olarak mı başka birşey olarak mı render etmek istediğimizi de belirtebiliriz. ör. Button 
    <Link href="/logout" method="post" as="button">Logout</Link>
  4. Eğer data da göndermek istersen :data attribute'unu kullanabilirsin. 
    <Link href="/logout" method="post" :data="{foo:'bar'}" as="button">Logout</Link>

Sayfada Kaldığın Yeri Hatırlamak

  1. Scroll management hakkında Inertia.js'in kendi sitesindeki dokumantasyon - link
  2. Link'e basınca scroll alanını hatırlama hakkında Inertia.js'in kendi dokumantasyonu - link
  3. Eğer <Link> komponentinde sayfa yüklendiğinde kaldığı yeri hatırlamak istersen preserve-scroll attribute'unu eklemen lazım. 
    <Link href="/" preserve-scroll>Home</Link>

Navigasyon'da Aktif olanı Belirtmek

  1. Aktif linkler hakkında Inertia.js'in kendi dokumantasyonu - link
  2. Kullanıcının uygulamayı kullanırken navigasyon seçeneklerden hangisinde olduğunun gösterilmesi çok temel bir ihtiyaç. Bunu Inertia.js'te URI veya komponent bazından kontrol edip ona göre stillendirme yapabiliriz. 
    import { Link } from '@inertiajs/inertia-vue'
    
    // URL tam olarak böyleyse aktive et
    <Link href="/users" :class="{ 'active': $page.url === '/users' }">Users</Link>
    
    // Komponent tam olarak böyleyse aktive et
    <Link href="/users" :class="{ 'active': $page.component === 'Users/Index' }">Users</Link>
    
    // URL bu şekilde başlıyorsa aktive et (/users, /users/create, /users/1, etc.)
    <Link href="/users" :class="{ 'active': $page.url.startsWith('/users') }">Users</Link>
    
    // Komponent bu şekilde başlıyor ise aktive et (Users/Index, Users/Create, Users/Show, etc.)
    <Link href="/users" :class="{ 'active': $page.component.startsWith('Users') }">Users</Link>

Layout Dosyaları

  1. Ekranlar arasında aynı öğeleri tek bir komponente dönüştürsek dahi, bunları kalkıp her ekranda tekrar tekrar çağırıp, tekrar tekrar render etmek tabii ki doğru bir pratik değildir. Çözümü Layout Files. Shared/ klasörü içerisinde Layout.vue adında yeni bir ana şablon dosyası düzenliyorum. 
    <template>
        <Nav />
        <slot />
    </template>
    <script>
    import Nav from './Nav'
    
    export default {
       components: { Nav },
    }
    </script>
  2. Layout dosyasında <slot /> olarak belirlediğim yere, kullanıldığı sayfada <Layout></Layout> içinde ne varsa o içerik olarak gelir. Örneğin Home.vue dosyasında:
    <template>
        <Layout>
            <h1>Home</h1>
        </Layout>
    </template>
    <script>
    import Layout from '../Shared/Layout'
    
    export default {
       components: { Layout },
    }
    </script>

Ekranlar arası Paylaşılan Veri

  1. Bazen ekranlar arası paylaşılan veriler olacaktır. Peki o durumlarda ne yapacağız? Mesela root ekrandayız. Buraya controller'dan bir veri yolladık...  Home.vue'da bu veriyi prop olarak almamız ve Layout'a vermemiz, sonra Layout dosyasında prop olarak kabul edip kullanmamız bir çözüm... kötü bir çözüm. Daha da beteri başka bir ekrana geçtiğimizde bu veri tamamen kaybolur. Ya herşeyi o ekran için de yapmamız gerekir... ya da veri paylaşılmaz.
  2. Ekranlar arası paylaşılan veriler için HandleInertiaRequests.php Middleware'i kullanılıyor. 
    public function share(Request $request)
    {
       return array_merge(parent::share($request), [
          'auth' => [
             'user' => ['username' => 'John Doe']
           ]
       ])
    }
  3. Bunu kullanmak istiyorsan Inertia'nın $page komponentini kullanabilirsin 
    <p>Welcome {{ $page.props.auth.user.username }}</p>
  4. Eğer ekranda birden fazla yerde kullanılıyorsa o zaman veriyi computed prop yapabilirsin. 
    <template>
        <Layout>
            <h1>Home - Welcome {{ username }}</h1>
        </Layout>
    </template>
    <script>
    import Layout from '../Shared/Layout'
    
    export default {
       components: { Nav },
       computed: {
            username() {
                return this.$page.auth.user.username;
            }
       }
    }
    </script>
  5. Veya daha da kullanışlı bir örnek Flash Messages 
    class HandleInertiaRequests extends Middleware
    {
        public function share(Request $request)
        {
            return array_merge(parent::share($request), [
                'flash' => [
                    'message' => fn () => $request->session()->get('message')
                ],
            ]);
            // veya
            return array_merge(parent::share($request), [
                'flash' => function () use ($request) {
                    return [
                        'message' => $request->session()->get('message'),       
                    ];
                }
            ]);
        }
    }
  6. Flash message'ı bu şekilde her zaman veri olarak yükledikten sonra Vue tarafında varsa göstermek için 
    <template>
      <main>
        <header></header>
        <content>
          <div v-if="$page.props.flash.message" class="alert">
            {{ $page.props.flash.message }}
          </div>
          <slot />
        </content>
        <footer></footer>
      </main>
    </template>
  7. Örneğin kullanıcı kaydından sonra flash message oluşturmak için 
    public function store()
    {
       User::create(
          Request::validate([
             'name' => ['required', 'max:50'],
             'email' => ['required', 'email', 'max:50'],
             'role' => ['required', 'max:50'],
          ])
       )
       
       return Redirect::route('users.index')->with(['message' => 'New user successfully created']);
    }
  8. Shared data konusunda Inertia.js'in kendi dokumantasyonu için - link

Global Component

  1. Bazı komponentlere her zaman ihtiyaç duyulur (ör. Link) Bu gibi durumlarda her sayfada import edip, register etmek yerine bunları global komponent olarak belirleyebiliriz.
  2. Bunu yapmak için app.js içerisinde App'imizi oluştururken .component() metodunu kullanabiliriz. 
    
    + import { createInertiaApp, Link } from "@inertiajs/inertia-vue3";
    import { InertiaProgress } from "@inertiajs/progress";
    
    createInertiaApp({
       resolve: name => import(`./Pages/${name}`),
       setup({ el, App, props, plugin }) {
          createApp({ render: () => h(App, props)})
            .use(plugin)
    +        .component('Link', Link)
            .mount(el);
       }
    })
    
  3. Birden fazla component register etmek istersen, istediğin kadar .component() metodunu çağırabilirsin.
  4. Bu şekilde global registration yaptıktan sonra sayfalarında hiç import-register etmeden direk bu komponentleri kullanabilirsin.

Persistent Layouts

  1. Layout.vue şu anda her sayfanın içinde root child konumunda. Yani her sayfa değişiminde aslında layout silinip baştan yükleniyor. Halbuki layout'un sayfalardan gerçekten bağımsız bir nesne olmasını istersem PERSISTENT LAYOUT özelliğini kullanmam lazım.
  2. Yapması çok kolay... sayfanın ayarlarını yaparken sadece layout olarak hangi komponenti kullanmak istediğimi söylemem yeterli. ör. Home.vue dosyasındayım. 
    <template>
    -    <Layout>
        <h1>Home</h1>
    -    </Layout>
    </template>
    <script>
    import Layout from '../Shared/Layout'
    
    export default {
    +    layout: Layout
    }
    </script>
  3. Üçüncü bir adım yok... bu kadar basit! 🤩
  4. Persistent layout konusunda Inertia.js official docs - link

Persistent Layout'u Global olarak belirtmek

  1. Her sayfa componentinde layout'u belirtmek yerine bunu global olarak da tanımlayabiliriz.
  2. Bunu yapmak için app.js dosyasında createInertiaApp() fonksiyonunda resolve ediyorken direk sayfa komponentini require etmek yerine onu bir değişkene atayıp .layout özelliğine persist edecek Layout komponentini tanımlayabiliriz. 
    import Layout from './Shared/Layout';
    ...
    createInertiaApp({
        resolve: name => {
            let page = require(`./Pages/${name}`).default;
            return page;
        },
  3. Öncekinden farklı olarak require fonksiyonundan gelen promise'de .default componenti aldığımızı belirtmemiz lazım.
  4. Ayrıca bazı sayfalarda persistent layout olarak farklı bir layout belirtmek isteyebiliriz. Bu gibi durumlarda safya komponentinin içinden layout'u belirtmek için resolve fonksiyonunda "layout belirtilmişse layout'u kullan yoksa bu Layout'u kullan diyebiliriz. 
    import Layout from './Shared/Layout';
    ...
    createInertiaApp({
        resolve: name => {
            let page = require(`./Pages/${name}`).default;
            /*if(!page.layout) {
                page.layout = Layout;
            }*/
            page.layout ??= Layout;
            return page;
        },
  5. Bunu yaptıktan sonra tabii ilgili sayfa komponentlerinden layout importları kaldırmamız gerekir.

Güvenlik

  1. Normalde Laravel uygulamasında veritabanından çekilen bilgileri filtrelemeden blade sayfasına aktarmak bir problem olmaz çünkü client-side'da görülmez. Ama Inertia.js ile controller'da çekip Vue'ya prop olarak aktardığımız verilerin hepsi XHR ile client-side'a aktarılır. O nedenle sadece ve sadece kullanıcı için elzem olan bilgileri paylaşmak gerekiyor. 
    public function index()
    {
       $users = Users::all();
    
       return Inertia::render('Users/Index', [
          'users' => $users->map->only('id','name','email','role')
       ])
    }
  2. Hatta verinin adının veritabanındakinden farklı olmasını da isteyebiliriz o zaman da 
    return Inertia::render('Users', [
          'users' => User::all()->map(fn($user) => [
             'name' => $user->name,  
          ])
    ]);

    arrow function Php 7.4'te çalışan bir özellik.

  3. Üsttekini PHP 7.3'de yapmak istersek 
    private function getMakamlar()
    {
        return Makam::orderBy('name')->get()->map(function($makam) {
            return [
                'name' => 'Makam: ' . $makam->slug,   
            ];
        });
    }
  4. Pagination yaptığın bir veri seti için sadece belli verileri almak istersen 
    private function getMeskler()
    {
        return Mesk::orderBy('date', 'desc')
            ->where('is_visible', true)
            ->paginate(50)
            ->through(function ($mesk) {
                return [
                    'uri' => $mesk->date,
                    'date' => Carbon::parse($mesk->date)->translatedFormat('j F Y'),
                ];
            });
    }

Formlarla Çalışmak

  1. Inertia.js kullanarak örneğin kullanıcı kaydettiğimiz senaryoyu düşünelim... routes/web.php'de öncelikle create view ve post process routelarımızı belirtelim 
    Route::get('users/create', [UsersController::class, 'create']);
    Route::post('users', [UsersController::class, 'store']);
  2. UsersController.php üzerinde öncelikle kayıt formunun görüneceği metodu oluşturuyorum.
    public function create()
    {
       return Inertia::render('Users/Create);
    }
  3. Ve bu route'a gelindiğinde gösterilecek /Users/Create.vue dosyasını oluşturuyorum. 
    <template>
        <form @submit.prevent="submit">
            <label for="name">Name:</label>
            <input id="name" v-model="form.name" type="text" />
    
            <label for="email">Email:</label>
            <input id="email" v-model="form.email" type="email" />
    
            <label for="role">Role:</label>
            <select id="role" v-model="form.role">
                <option />
                <option>Admin</option>
                <option>Owner</option>
                <option>Guest</option>
            </select>
    
            <button type="submit">Create</button>
        </form>
    </template>
    <script>
    import { reactive } from 'vue';
    import { Inertia } from '@inertiajs/inertia'
    import { Layout } from '../Shared/Layout'
    
    export default {
        layout: Layout,
        setup() {
            const form = reactive({
                name: null,
                email: null,
                role: null,
            })
    
            function submit() {
                // submit handler...
            }
            return { form, submit }
        }
    }
    </script>
  4. Normalde submit() handler axios ile bu request yapıldığında nasıl olurdu? 
    submit() {
        axios.post("/users", this.form)
            .then(response => window.location.href = '/users')
            .catch(error => {
              this.errors = error;
            });
    }
  5. Inertia.js ile olduğunda burada post request bu kadar.
    submit() {
       Inertia.post('/users', form)
    }
  6. Post request yaptıktan sonraki adımları (error handling, redirecting) client-side'da değil, backendde yapıyorsun. UsersController.php içerisinde 
    public function store()
    {
       User::create(
          Request::validate([
             'name' => ['required', 'max:50'],
             'email' => ['required', 'email', 'max:50'],
             'role' => ['required', 'in:Owner,Admin,Guest'],
          ])
       )
       
       return Redirect::route('users.index')->with(['message' => 'New user successfully created']);
    }
  7. Inertia.js Formlar hakkında official doc. - link
  8. Bir form submission'dan sonra flash message kullanımı hakkında - link

Form Helper

  1. Formlarla çalışmak çok olacak bir işlem olduğundan Inertia içerisinde bir de Form Helper bulunmakta. Form Helper'ı kullanarak Vue tarafındaki template çok daha basit bir hale dönüşüyor. 
    <template>
      <form @submit.prevent="form.post('/users')">
        <!-- name -->
        <label for="name">Name:</label>
        <input id="name" v-model="form.name" type="text" />
        <div v-if="form.errors.name">{{ form.errors.name }}</div>
    
        <!-- email -->
        <label for="email">Email:</label>
        <input id="email" v-model="form.email" type="email" />
        <div v-if="form.errors.email">{{ form.errors.email }}</div>
    
        <!-- role -->
        <label for="role">Role:</label>
        <select id="role" v-model="form.role">
            <option />
            <option>Admin</option>
            <option>Owner</option>
            <option>Guest</option>
        </select>
        <div v-if="form.errors.role">{{ form.errors.role }}</div>
    
        <!-- submit -->
        <button type="submit" :disabled="form.processing">Login</button>
      </form>
    </template>
    
    <script>
    import { useForm } from '@inertiajs/inertia-vue3'
    
    export default {
      setup () {
        const form = useForm({
          name: null,
          email: null,
          role: null,
        })
    
        return { form }
      },
    }
    </script>

     

  2. Form Helper hakkında official docs - link

Sayfa Verisini Hatırlamak

  1. ör. Form olan bir sayfada alanları doldurduktan sonra başka bir sayfaya gitsek ve dönsek oradaki form değerleri silinmiş olur. Bunu tutmak istediğimiz durumlarda useRemember() methodunu kullanarak nelerin history içerisine alınarak unutulmaması gerektiğini söyleyebiliriz. 
    import { useRemember } from '@inertiajs/inertia-vue3'
    
    export default {
      setup() {
        const form = useRemember({
            first_name: null,
            last_name: null,
        })
    
        return { form }
      },
    }
  2. Inertia.js official docs - link

Code Splitting ve Dynamic Imports

  1. Uygulamamız iyice büyürse bütün uygulamayı hep app.js içerisine koymak... 1) cache'lemeyi sileceği için değişiklik olduktan sonra ziyaretçi uygulamaya geldiğinde bütün uygulamayı tekrar yükletmek zorunda olur. 2) uygulamaya giren kişi daha ilk girdiği anda bütün sayfaları yüklemek zorunda kalır... hem de belki de o sayfalara hiç girmeyecekken.
  2. Bunun için yapılabilecek 2 optimizasyon var. 1) Webpack tarafında 2) Inertia (Vue komponent) tarafında.
  3. Webpack tarafında yapılabilecek şey VENDOR EXTRACTION. Webpack tarafında 
    mix.js('resources/js/app.js', 'public/js')
        .extract()
    	.vue(3)
    	.postCss('resources/css/twapp.css', 'public/css', [
    		require("tailwindcss"),
    	])
  4. extract() fonksiyonu common vendor dependency'leri app.js kodumun dışına almamı sağlar. Böylece app.js güncellendiğinde vendor.js'in cache'li versiyonu hala çalışır durumda kalır ve tekrar yüklemek zorunda kalmayız. Buna simple vendor extraction deniyor.
  5. Bu şekilde webpack dosyamı düzenleyip npx mix dediğimde  2 tane daha dosyam oluşur. manifest.js (webpack manifest dosyası) ve vendor.js (vendor dependency dosyası)
  6. Bunu yapınca tabii ki app.blade.php dosyasındaki script referanslarını da tekrar düzenlemem gerekiyor. 
    <script src="{{ mix('/js/manifest.js') }}" defer></script>
    <script src="{{ mix('/js/vendor.js') }}" defer></script>
    <script src="{{ mix('/js/app.js') }}" defer></script>
  7. Şimdi de Vue componentleri için code splitting nasıl yapılıyor ona bakalım...
  8. app.js dosyasında async olarak name fonksiyonunu çalıştırıyorum. Ve import işlemi tamamlandığında await ile gerekli komponenti alıyorum.
    createInertiaApp({
        resolve: async name => {
            let page = (await import(`./Pages/${name}`)).default;
            page.layout ??= AppShell;
            return page;
        },
  9. Code splitting ve dynamic imports hakkında Laracast dersi için - link

Sayfalara Meta Bilgisi Yazmak

  1. Vue sayfalarına title ve meta bilgisi eklemek istersen Inertia.js'in HEAD komponentini sayfalara eklememiz ve kullanmamız yeterli. 
    <template>
      <Head>
        <title>Mûsikî Meşkleri</title>
      </Head>
       ...
    </template>
    
    <script>
    import { Head } from '@inertiajs/inertia-vue3';
    ...
    export default {
      components: {
        Head,
  2. Eğer default bir metatag'imiz olmasını istersek bunu Layout dosyasında belirtebiliriz. Bu durumda Sayfa komponentinde Head kullanılmamışsa Layout'ta bulunan Head kullanılır.
  3. Title tag'i eklediğimiz gibi meta tagleri de bu şekilde ekleyebiliriz.
  4. Ancak varsayılan vs. sayfada eklenen dediğimiz zaman bir sorun oluyor. Title sayfada zaten en fazla bir tane olabilir.  O nedenle onda sorun olmaz ama metatag eklerken Layout dosyasında ve Sayfada dediğimiz zaman aynı metatag'den iki tane sayfada çıkabilir.
  5. Bu sorun olmaması için meta tag'e head-key diye bir attribute koymamız yeterli. Aynı head-key hem Layout hem de sayfa komponentinde kullanıldığında Inertia hangi tag'in hangisinin aynısı olduğunu anlar. 
    <Head>
      <title>Mûsikî Meşkleri</title>
      <meta type="description" content="Tasavvuf musikisi meşkleri" head-key="description">
    </Head>
  6. Sayfalarda title'ın önü veya sonu sabitse bunun da ne olduğunu app.js içerisinde createInertiaApp fonksiyonunda title'ı belirterek ayarlayabiliriz.
    createInertiaApp({
        resolve: name => {
            let page = require(`./Pages/${name}`).default;
            page.layout ??= AppShell;
            return page;
        },
        setup({ el, App, props, plugin }) {
            createApp({ render: () => h(App, props) })
                .use(plugin)
                .mount(el)
        },
        title: title => `Dilbeyti - ${title}`,
    });
  7. Muhtemelen her sayfada <Head> komponentine ihtiyacımız olacak. Onu da her seferinde import etmemek için app.js içerisinde direk yükleyebiliriz. 
    createInertiaApp({
       ...
       setup({ el, App, props, plugin }) {
            createApp({ render: () => h(App, props) })
                .use(plugin)
                .use(store)
                .component("Head", Head)
                .mount(el)
        },
        ...

Paginator Uygulaması

  1. Normalde Laravel'de bir veri setine pagination yapmak istersek direk paginate() fonksiyonunu kullanırız. Ama bunu söylediğimde artık sadece bir veri seti değil, onunla beraber pagination bilgileri de gelir.
  2. Öncelikle ilk yapmamız gereken şey ör. $users = Users::paginate(15); diyor ve Vue sayfasına veri olarak atıyorsak artık Vue tarafında bunu prop olarak kabul ederken Array değil, Object olarak almalıyız ve o anki sayfanın listesi de users.data içerisinde bulunur.
  3. Paginator'u sayfaya çizmek için de ihtiyacımız olan liste users.links içindedir. Sayfada listenin altında paginator'u bu links listesinden oluşturabilirim. 
    <div>
       <Link v-for="link in meskler.links" :href="link.url" v-html="link.label" />
    </div>
  4. Ancak en sonda ve en başta paginator.previous ve paginator.next adında linkler var. Bunlar 1. sayfadayken ve sonuncu sayfadayken aslında çalışmaz. Bu durumlarda Link render etmek yerine başka birşey render etmek daha doğru olur. 
    <div>
      <template v-for="link in meskler.links">
        <Link v-if="link.url" :href="link.url" v-html="link.label" />
        <span v-else v-html="link.label"></span>
      </template>
    </div>

    Eğer link elementinin url'si varsa o zaman Inertia Link componenti render eder, yoksa SPAN render eder. BU 1. YOLdu.

  5. 2. YOL ise dinamik Vue component'i ile yapmak. Dinamik Vue Componenti demek, belli koşula göre componentin ne olmasına karar verdiğin, değişken komponent demektir. Dinamik Vue Componenti olarak yapmak istediğimizde de şu şekilde olur 
    <Component :is="link.url ? 'Link' : 'span'"
       v-for="link in meskler.links"
       :href="link.url"
       v-html="link.label"
       class="px-1"
       :class="link.url ? '' : 'text-gray-400'"></Component>
  6. Şimdi paginator'u bir Vue komponenti haline getirelim. Pagination.vue dosyası şu şekilde olur.
    <template>
      <div>
        <Component :is="link.url ? 'Link' : 'span'"
                   v-for="link in links"
                   :href="link.url"
                   v-html="link.label"
                   class="px-1"
                   :class="link.url ? '' : 'text-red-400'"></Component>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        links: Array,
      }
    }
    </script>
  7. Buna bağlı olarak sayfada o zaman şu şekilde çağırmam gerekecek 
    <template>
       ...
       <Pagination :links="meskler.links" class="mt-6"></Pagination>
       ...
    </template>
    
    <script>
    import Pagination from "../../Shared/Pagination";
    
    export default {
      components: {
        Pagination,
        ...
    </script>
  8. Ama şu ana kadarki işlemlerle pagination yapılan verinin tamamı gelir. Halbuki sadece belli veriler görülsün istiyorum. O zaman controllerda bunu map() yerine through() diyerek yapacağım. 
    private function getMeskler()
    {
        return Mesk::orderBy('date','desc')
            ->where('is_visible',true)
            ->paginate(20)
            ->through(function($mesk) {
                return [
                    'uri' => $mesk->miladi,
                    'date' => Carbon::parse($mesk->miladi)->translatedFormat('j F Y'),
                    'hicri_date' => $mesk->hicri,
                ];
            });
    }
  9. Son olarak ta Current Page'i paginator'da biraz daha farklı stillendirelim. Paginator.vue dosyasında 
    <Component :is="link.url ? 'Link' : 'span'"
       v-for="link in links"
       :href="link.url"
       v-html="link.label"
       class="px-1"
       :class="{'text-gray-500': !link.url, 'font-bold underline': link.active}"></Component>

Filtrelemek

  1. Şimdiye kadar sayfalara veri en başta controller'dan gönderiliyordu. Şimdi sayfada beliren veri kaynağını isteğimize göre filtrelemey görelim. (ör. bir veri seti geliyor... üstte arama kutusu var. Arama kutusuna basıldığında mevcut veri seti ona göre eş zamanlı filtrelensin)
  2. Filtre inputunu hazırlıyorum. 
    <input type="search"
                   v-model="search"
                   placeholder="Meşkler içinde ara..." 
                   class="p-2 border-gray-200 border rounded">
  3. Vue tarafında bu alanı izlemek ve kontrol etmek adına referans olarak alıyorum ve watcher ekliyorum. 
    <script setup>
    import { ref, watch } from "vue";
    ...
    let search = ref('');
    watch(search, value => {
       console.log(value);
    })
    ...
    </script>
  4. Watcher'da değişiklik olduğunda Backend tarafına yeni bir Ajax requesti atmam gerekecek. Şimdiye kadar bu tarz requestleri sayfa geçişleri için <Link> componenti ile yapıyorduk. Halbuki program içinde de Manuel visit olarak yapabiliriz. Bunun için Inertia'yı import ediyoruz ve kullanıyoruz. 
    <script setup>
    import { ref, watch } from "vue";
    ...
    import { Inertia } from "@inertiajs/inertia";
    ...
    let search = ref('');
    watch(search, value => {
      Inertia.get('/meskler', {search: value});
    })
    ...
    </script>
  5. Şimdi Backend tarafında controller ile "search değeri varsa" ya göre gelenleri filtrelemem lazım. Bunun için backend'de query yazarken query() ve when() (Laravel docs - when()) conditionallarını kullanarak sorgumu güncelliyorum.
    return Mesk::query()
        ->when(request('search'), function($query, $search) {
            $query->where('date', 'like', "%{$search}%");
        })
        ->paginate(50)
        ->through(function ($mesk) {
            return [
                'date' => Carbon::parse($mesk->date)->translatedFormat('j F Y'),
            ];
        });

    when($sorgu_değeri, doğru olursa/varsa uygulanacak fonksiyon)... Buradaki fonksiyonda 1. parametre Query'nin kendisi, 2. parametre(opsiyonel) sorgu değeridir. İçeride tekrar kullanılacaksa burada yazmak faydalı oluyor. 

  6. Normalde pagination içerisindeki ileri/geri sayfa linklerinde diğer query stringleri yer almaz. Bunların da kalmasını istersek burada paginate ettikten sonra ayrıca withQueryString() metodunu eklememiz yeterli oluyor. Laravel Pagination Query String
    return Mesk::query()
        ->when(request('search'), function($query, $search) {
            $query->where('date', 'like', "%{$search}%");
        })
        ->paginate(50)
        ->withQueryString()
        ->through(function ($mesk) {
            return [
                'date' => Carbon::parse($mesk->date)->translatedFormat('j F Y'),
            ];
        });
  7. Backend tarafını bu şekilde ayarladığımızda, watcher değişikliği fark ettiğinde tekrar bu requesti çalıştıracaktır. Ama backend'e bu şekilde tekrar başvurduğumuzda bu sefer sayfanın değişken alanı tekrar render edeceği için arama alanına yazdığımız herşey sıfırlanır. Bunun yaşanmaması için Inertia.get(url, parametre, opsiyonlar) opsiyonlar kısmına preserveState: true dememiz gerekir. 
    watch(search, value => {
      Inertia.get('/meskler', {search: value}, {
        preserveState: true,
      });
    })
  8. Çalışıyor... ama sorun var. Nedir? Ziyaretçi direk içinde arama terimi olan bir sayfa açmış olsa... O zaman da herşey çalışır ama arama alanı boş çıkar... Çünkü Vue tarafında search değerini boş olarak başlatmıştık. let search = ref(''); Backend tarafından arama terimini sayfaya prop olarak yollarsak, Vue tarafında bunu prop olarak kabul edip, search değerini boş başlatmak yerine o değerle başlatabiliriz. Backend Controller'da: 
    use Illuminate\Support\Facades\Request;
    ...
    return Inertia::render('Mesk/Index', [
       'meskler' => $this->getMeskler(),
       'filters' => Request::only(['search'])
    ]);

    Vue tarafında prop olara al ve search değişkenini o değerle başlat 

    let props = defineProps({
      meskler: Object,
      filters: Object,
    });
    
    let search = ref(props.filters.search);
  9. Son sorun ise arama alanında yaptığım her istek aslında History state'e de kayıt edildiğinden, yazdığım her karakter history state'te bir adım olarak kaydediliyor ve tarayıcının back butonuna bastığımda bir önceki ekran yerine bir önceki karakterli terime gidiyor. Bunu düzenlemem için Inertia.get() opsiyonları içinde replace: true demem lazım. Inertia docs - Manuel Visits, Browser History 
    watch(search, value => {
      Inertia.get('/meskler', {search: value}, {
        preserveState: true,
        replace: true,
      });
    })

Referanslar

  1. Inertia.js resmi websitesi - link
  2. Laracon Jonathan Reinink 1. Sunum 2020 - link
  3. Laracast Inertia.js eğitimi - link
  4. Laracon Jonathan Reinink 2. Sunum Yaz 2021 - link
  5. Laracast Inertia.js Jeffrey ilk eğitim videosu May 2021 - link
  6. Inertia.js in action - demo site Ping CRM
  7. Ping CRM Github Sourcecode - link
  8. Vue 3 Installation - link
  9. Splade.dev The Magic of Inertia.js with Blade - link