トップページ
Laravel学習サイトLaravelやるばい

目次

チャットアプリでAjaxの使い方を知ろう

今回は今までの復習をかねてAjaxを使ったチャットアプリを作ります。

第6回の記事の難易度が高いのですが難易度が高いと感じた人はこの記事を読んだ方がいいです。


jQueryを使う理由と使うメリット

ネットを調べると死んだ技術みたいに言われていますがそうでもないです。

前職と現職のフロントエンドにjQueryを使っています。

私が思うjQueryを使うメリットはJavaScriptとコードの書き方が似ている点です。

だからjQueryに慣れてJavaScript→TypeScriptに発展させることができます。

いきなりNext.jsを勉強する人がいますが学習時間がかかりすぎるのでおすすめしないです。


Ajaxとは

ざっくり言って画面読み込みをせずに処理をしたい時に使う技術です。

仕事をしていて色んな場面で使うので「フロントエンドでどの技術を一番使いますか?」と聞かれたらAjaxと答えます。

だからAjaxを知って欲しくてこの記事を書いています。


テーブルに保存する処理

今まではコントローラーで行っていましたがテーブルにアクセスするのはモデルの役割です。

だから今回の記事からはテーブルを経由する処理はモデルで行います。


アプリの仕様

Ajaxを使うのとテーブルにアクセスする処理をモデルで行うのに焦点を当てているので仕様を簡素にします。

とりあえずチャットができればいいのでログイン機能は入れずに1ページでチャットを完結させます。

アプリ名をchatとします。


最初の表示

welcome.blade.phpの記述を下記に変更します、コピペした方が間違いがないと思います。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

        <!-- Styles -->
        <style>
            /* ! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com */*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::after,::before{--tw-content:''}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:Figtree, sans-serif;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*, ::before, ::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.ml-4{margin-left:1rem}.mt-16{margin-top:4rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.-mt-px{margin-top:-1px}.mr-1{margin-right:0.25rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-16{height:4rem}.h-7{height:1.75rem}.h-6{height:1.5rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-auto{width:auto}.w-16{width:4rem}.w-7{width:1.75rem}.w-6{width:1.5rem}.w-5{width:1.25rem}.max-w-7xl{max-width:80rem}.shrink-0{flex-shrink:0}.scale-100{--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.gap-6{gap:1.5rem}.gap-4{gap:1rem}.self-center{align-self:center}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-dots-darker{background-image:url("data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.22676 0C1.91374 0 2.45351 0.539773 2.45351 1.22676C2.45351 1.91374 1.91374 2.45351 1.22676 2.45351C0.539773 2.45351 0 1.91374 0 1.22676C0 0.539773 0.539773 0 1.22676 0Z' fill='rgba(0,0,0,0.07)'/%3E%3C/svg%3E")}.from-gray-700\/50{--tw-gradient-from:rgb(55 65 81 / 0.5);--tw-gradient-to:rgb(55 65 81 / 0);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.via-transparent{--tw-gradient-to:rgb(0 0 0 / 0);--tw-gradient-stops:var(--tw-gradient-from), transparent, var(--tw-gradient-to)}.bg-center{background-position:center}.stroke-red-500{stroke:#ef4444}.stroke-gray-400{stroke:#9ca3af}.p-6{padding:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.text-center{text-align:center}.text-right{text-align:right}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-sm{font-size:0.875rem;line-height:1.25rem}.font-semibold{font-weight:600}.leading-relaxed{line-height:1.625}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128 / var(--tw-text-opacity))}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgb(0 0 0 / 0.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-gray-500\/20{--tw-shadow-color:rgb(107 114 128 / 0.2);--tw-shadow:var(--tw-shadow-colored)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.selection\:bg-red-500 *::selection{--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.selection\:text-white *::selection{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.selection\:bg-red-500::selection{--tw-bg-opacity:1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.selection\:text-white::selection{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39 / var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81 / var(--tw-text-opacity))}.focus\:rounded-sm:focus{border-radius:0.125rem}.focus\:outline:focus{outline-style:solid}.focus\:outline-2:focus{outline-width:2px}.focus\:outline-red-500:focus{outline-color:#ef4444}.group:hover .group-hover\:stroke-gray-600{stroke:#4b5563}.z-10{z-index: 10}@media (prefers-reduced-motion: no-preference){.motion-safe\:hover\:scale-\[1\.01\]:hover{--tw-scale-x:1.01;--tw-scale-y:1.01;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}}@media (prefers-color-scheme: dark){.dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.dark\:bg-gray-800\/50{background-color:rgb(31 41 55 / 0.5)}.dark\:bg-red-800\/20{background-color:rgb(153 27 27 / 0.2)}.dark\:bg-dots-lighter{background-image:url("data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.22676 0C1.91374 0 2.45351 0.539773 2.45351 1.22676C2.45351 1.91374 1.91374 2.45351 1.22676 2.45351C0.539773 2.45351 0 1.91374 0 1.22676C0 0.539773 0.539773 0 1.22676 0Z' fill='rgba(255,255,255,0.07)'/%3E%3C/svg%3E")}.dark\:bg-gradient-to-bl{background-image:linear-gradient(to bottom left, var(--tw-gradient-stops))}.dark\:stroke-gray-600{stroke:#4b5563}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175 / var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.dark\:ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.dark\:ring-inset{--tw-ring-inset:inset}.dark\:ring-white\/5{--tw-ring-color:rgb(255 255 255 / 0.05)}.dark\:hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity))}.group:hover .dark\:group-hover\:stroke-gray-400{stroke:#9ca3af}}@media (min-width: 640px){.sm\:fixed{position:fixed}.sm\:top-0{top:0px}.sm\:right-0{right:0px}.sm\:ml-0{margin-left:0px}.sm\:flex{display:flex}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media (min-width: 1024px){.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}
        </style>

        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
    </head>
    <body class="antialiased">
        <div class="chat-container row justify-content-center pt-4">
            <div class="chat-area">
                <div class="card">
                    <div class="card-header">Comment</div>
                    <div class="card-body chat-card">
                        <div class="media">
                            <div class="media-body comment-body">
                                <span class="comment-body-content">ダミーの内容</span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <form method="" action="">
            <div class="comment-container row justify-content-center">
                <div class="input-group comment-area">
                    <textarea class="form-control" placeholder="input massage" aria-label="With textarea" name="comment"></textarea>
                    <button type="submit" class="btn btn-outline-primary comment-btn">Submit</button>
                </div>
            </div>
        </form>
    </body>
</html>

<style>
.chat-container {
  width: 100%;
  height: 100%;
}
.chat-card {
  height: 67vh;
  overflow: auto;
}
.chat-area {
  width: 70%;
}
.comment-container {
  position: fixed;
  bottom: 20px;
  text-align: center;
  width: 100%;
}
.comment-area {
  width: 70%;
}
.comment-btn {
  margin: 0px 10px;
}
.comment-body {
  padding: 5px 30px 20px 30px;
}
.comment-body:hover {
  background-color: #dfdfdf;
}
.comment-body-user {
  font-weight: bold;
  font-size: 20px;
}
.comment-body-time {
  font-size: 10px;
  margin-top: 10px;
  margin-left: 5px;
  color: #a0a0a0;
}
</style>

これで下記の表示になります。

20250120_221823_chat1.jpg

チャットを動作させる前にチャットの内容を保存するテーブルを作成します。

テーブル名はchatsとします。

それとコントローラーも作成します。

コントローラー名はChatコントローラーとします。


chatsテーブルとChatコントローラーの作成

下記のコマンドを叩きます。

php artisan make:model Chat -m

マイグレーションファイルを下記にします。

public function up(): void
{
    Schema::create('chats', function (Blueprint $table) {
        $table->id();
        $table->string('body');      //この行を追加
        $table->timestamps();
    });
}

.envに下記の記述をします。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=chats
DB_USERNAME=root
DB_PASSWORD=root

DB名はchatsにしました。

下記のコマンドを叩いてchatsテーブルを作成します。

php artisan migrate

コントローラーを作成します。

下記のコマンドを叩きます。

php artisan make:controller ChatController

それでは実装していきます。


実装

welcome.blade.phpを表示するコントローラーを変更します。

web.phpに下記の記述をします。

<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\ChatController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/

Route::controller(ChatController::class)->group(function() {
    Route::get('/', 'index')->name('welcome');
});

ChatController.phpに下記の記述をします。

public function index() {
    return view('welcome');
}

チャットをする為の記述をします。

web.phpに下記の記述をします。

Route::controller(ChatController::class)->group(function() {
    Route::get('/', 'index')->name('welcome');

    Route::post('/store', 'store')->name('chat.store');          //この行を追加
});

ChatController.phpに下記の記述をします。

public function index() {


    //ここから修正
    $chats = Chat::all();

    return view('welcome', compact('chats'));
    //ここまで修正


}


//ここから追加
public function store(Chat $chat, Request $request) {
    $new_chat = $chat->chatStore($request);

    return response()->json($new_chat);
}
//ここまで追加

Chat.phpに下記の記述をします。

protected $fillable = [
    'body'
];

public function chatStore($request) {
    return Chat::create([
        'body' => $request->input('body')
    ]);
}

コントローラーのstoreアクションとモデルのchatStoreアクションはあとで説明します。

テンプレートのコードを下記に変更します。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">

        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

        <meta name="csrf-token" content="{{ csrf_token() }}">



    </head>
    <body class="antialiased">
        <div class="chat-container row justify-content-center pt-4">
            <div class="chat-area">
                <div class="card">
                    <div class="card-header">Comment</div>
                    <div class="card-body chat-card" id="chat-wrap">
                    @forelse($chats as $chat)
                        <div class="media">
                            <div class="media-body comment-body">
                                <div class="row">
                                    <span class="comment-body-user">{{ $chat->body }}</span>
                                </div>
                            </div>
                        </div>
                    @empty
                        <p id="no-chat">チャットはまだありません。</p>
                    @endforelse
                    </div>
                </div>
            </div>
        </div>

        <form method="post" action="{{ route('chat.store') }}">
            @csrf
            <div class="comment-container row justify-content-center">
                <div class="input-group comment-area">
                    <textarea class="form-control" placeholder="input massage" aria-label="With textarea" name="body"></textarea>
                    <button type="submit" class="btn btn-outline-primary comment-btn" id="submit-button">Submit</button>
                </div>
            </div>
        </form>
    </body>
</html>


<script>
    $(document).ready(function () {
        $('#submit-button').on('click', function (e) {
            e.preventDefault();

            const user_chat = $('textarea[name="body"]').val();
            const csrf_token = $('meta[name="csrf-token"]').attr('content');

            if (!user_chat) {
                alert('コメントを入力して下さい');

                return;
            }

            $.ajax({
                url: '/store',
                type: 'POST',
                headers: {
                    'X-CSRF-TOKEN': csrf_token
                },
                data: {
                    body: user_chat
                },
                success: function (response) {
                    const new_comment = `
                        <div class="media">
                            <div class="media-body comment-body">
                                <span class="comment-body-content">${response.body}</span>
                            </div>
                        </div>
                    `;

                    $('#chat-wrap').append(new_comment);
                    $('textarea[name="body"]').val('');

                    if ($('#no-chat').length == 1) {
                        $('#no-chat').remove();
                    }
                }
            })
        });
    })
</script>

<style>
.chat-container {
  width: 100%;
  height: 100%;
}
.chat-card {
  height: 67vh;
  overflow: auto;
}
.chat-area {
  width: 70%;
}
.comment-container {
  position: fixed;
  bottom: 20px;
  text-align: center;
  width: 100%;
}
.comment-area {
  width: 70%;
}
.comment-btn {
  margin: 0px 10px;
}
.comment-body {
  padding: 5px 30px 20px 30px;
}
.comment-body:hover {
  background-color: #dfdfdf;
}
.comment-body-time {
  font-size: 10px;
  margin-top: 10px;
  margin-left: 5px;
  color: #a0a0a0;
}
</style>

jQueryのコードの解説をします。

フォームの送信ボタンを押すと画面の読み込みをするHTMLの仕様になっていますがそれを防ぐのが下記のコードです。

e.preventDefault();

下記のコードはフォームに値を入力せずに送信ボタンを押した時のバリデーション用です。

if (!user_chat) {
    alert('コメントを入力して下さい');

    return;
}

「user_chat」はフォームに入力した内容です。

ajaxはjQuery側からコントローラーにデータを送る処理とコントローラーから送られたデータを受け取る処理の2つに分かれています。

$.ajax({


    //jQueryからコントローラーにデータを送る
    url: '/store',
    type: 'POST',
    headers: {
        'X-CSRF-TOKEN': csrf_token
    },
    data: {
        body: user_chat
    },
    //jQueryからコントローラーにデータを送る


    //コントローラーから送られたデータを受け取る
    success: function (response) {
        const new_comment = `
            <div class="media">
                <div class="media-body comment-body">
                    <span class="comment-body-content">${response.body}</span>
                </div>
            </div>
        `;

        $('#chat-wrap').append(new_comment);
        $('textarea[name="body"]').val('');

        if ($('#no-chat').length == 1) {
            $('#no-chat').remove();
        }
    }
    //コントローラーから送られたデータを受け取る

    
})

「url」の項目はajaxの処理をしたいweb.phpのURLを記述します。

今回は保存の処理にajaxを使うので下記を使います。

Route::post('/store', 'store')->name('chat.store');

「type」の項目はweb.phpの「Route::〜」の「〜」を記述します。

「headers」の項目はそのまま書いていいですが「csrf_token」はheadタグの中のmetaタグを使っています。(下記参照)

<meta name="csrf-token" content="{{ csrf_token() }}">

「data」の項目の「body: user_chat」は「key: value」の関係になっていてコントローラーにデータを送ります。

ここからコントローラーに処理が移ります。

テーブルを経由する処理をする時はモデルで行ってそれをコントローラーで使うのでまずモデルからコード見ます。

$request->input('body')」のbodyはajaxの「body: user_chat」のbodyのことです。

コントローラーに戻ってコントローラーでモデルのメソッドを使う時の記述は下記になります。

モデル変数->モデルのメソッド;

具体的に説明します。

public function store(Chat $chat, Request $request) {
    $new_chat = $chat->chatStore($request);

    return response()->json($new_chat);
}

storeメソッドの引数に「Chat $chat」がありますが$chatがモデル変数になります。

モデルのメソッドに当たるのがchatStoreメソッドです。

chatStoreメソッドの引数に$requestが入っていますがこれはモデルで使うので記述しています。

「$new_chat」はテーブルに保存したデータですがjQueryに送信する為に「returnresponse()->json($new_chat);」の記述をしています。

「json( )」の引数に使う値をjQueryに送信します。

ここからjQueryの処理を見ます。

「success: function (response)」のresponseにコントローラーからのデータが入っています。

下記の記述をしてデータの中身を確認します。

success: function (response) {
    console.log(response);
    
    const new_comment = `
        <div class="media">
            <div class="media-body comment-body">
                <span class="comment-body-content">${response.body}</span>
            </div>
        </div>
    `;

下記の表示になります。

{
    body: "こんにちは"
    created_at: "2025-01-25T02:34:50.000000Z"
    id: 5
    updated_at: "2025-01-25T02:34:50.000000Z"
}

オブジェクトの形式になります。

これはコントローラーの「returnresponse()->json($new_chat);」の「$new_chat」の内容です。

下記の記述はチャットを送った時にチャット一覧にチャットを追加する為にあります。

const new_comment = `
    <div class="media">
        <div class="media-body comment-body">
            <span class="comment-body-content">${response.body}</span>
        </div>
    </div>
`;

$('#chat-wrap').append(new_comment);

Ajaxでテーブルに値を保存してそれを画面に表示する時ですがテーブルに保存された最新の値は画面を読み込まないと表示されません。

だからAjaxを使った時はjQueryで表示する必要があります。

画面の読み込みをするとjQueryで表示したチャットは消えるのでjQueryで表示したチャットとテーブルに保存されているチャットが両方表示される心配はないです。

下記の記述はチャットを表示した時にフォームに書いた内容を消す為にあります。

$('textarea[name="body"]').val('');

下記の記述はチャットを始めて表示する時に「チャットはまだありません」の表示を消す為にあります。

if ($('#no-chat').length == 1) {
    $('#no-chat').remove();
}

これでチャットアプリが完成です。








戻る