Xây dựng ứng dụng CRUD sử dụng Vue.js trong Laravel

CRUD viết tắt của Create, Read, Update, Delete là một thuật ngữ lập trình nói đến 4 phương thức quen thuộc khi làm việc với kho dữ liệu. Ứng dụng CRUD là ứng dụng cung cấp đủ các hoạt động trên cùng với phân trang và tìm kiếm. Ứng dụng CRUD là rất thuận tiện trong việc sử dụng đem lại trải nghiệm tốt cho người dùng. Trong bài viết này, chúng ta sẽ tìm hiểu cách xây dựng một ứng dụng CRUD để quản lý các sản phẩm, ứng dụng sẽ sử dụng framework Vue.js một framework javascript được Laravel khuyến cáo sử dụng.

Trong bài viết Xây dựng truy vấn cùng Laravel query builder, chúng ta cũng đã xây dựng một ứng dụng CRUD cho việc quản lý sản phẩm. Tuy nhiên, bài viết này hướng đến việc sử dụng Vue.js, bạn sẽ thấy những trải nghiệm khác biệt.

Ứng dụng CRUD sử dụng Vue.js trong Laravel

Bước 1: Xây dựng database

Trong các viết trước chúng ta đã xây dựng project laravel-test để thực hành cho các bài viết (xem khóa học Laravel 7 ngày để biết thêm chi tiết). Nếu bạn sử dụng laravel-test thì đã có sẵn bảng product nên ta có thể bỏ qua bước này. Sử dụng Laravel Migration để tạo bảng.

c:\xampp\htdocs\laravel-test>php artisan make:migration create_products_table --create=products
Created Migration: 2017_04_24_024728_create_products_table

Thêm code tạo bảng product vào file 2017_04_24_024728_create_products_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 255);
            $table->integer('price');
            $table->text('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('products');
    }
}

Chạy lệnh php artisan migrate để tạo ra bảng product trong CSDL.

c:\xampp\htdocs\laravel-test>php artisan migrate
Migrated: 2017_04_24_024728_create_products_table

Bước 2: Tạo VueJSProductController xử lý các hoạt động trên sản phẩm

Chúng ta sẽ tạo ra một RESTful Controller có sẵn các phương thức cho từng hoạt động trên sản phẩm (Xem Laravel Controller để hiểu hơn về RESTful Controller). Cũng vẫn sử dụng câu lệnh artisan make:controller nhưng thêm tham số –resource để tạo RESTful Controller:

php artisan make:controller VueJSProductController --resource

Tôi sử dụng tên VueJSProductController để tránh bị trùng với ProductController đã có sẵn trong laravel-test.

Tiếp theo, đưa code vào VueJSProductController:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Product;

class VueJSProductController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        $products = Product::latest()->paginate(5);

        $response = [
            'pagination' => [
                'total'        => $products->total(),
                'per_page'     => $products->perPage(),
                'current_page' => $products->currentPage(),
                'last_page'    => $products->lastPage(),
                'from'         => $products->firstItem(),
                'to'           => $products->lastItem()
            ],
            'data' => $products
        ];

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

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $this->validate($request, [
            'name'    => 'required',
            'price'   => 'required',
            'content' => 'required',
        ]);

        $create = Product::create($request->all());

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

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $this->validate($request, [
            'name'    => 'required',
            'price'   => 'required',
            'content' => 'required',
        ]);

        $edit = Product::find($id)->update($request->all());

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

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        Product::find($id)->delete();
        return response()->json(['done']);
    }

    public function showVueProduct(){
        return view('fontend.vue-product');
    }
}

Bước 3: Tạo route và view

Tạo view vue-product.blade.php trong resources/views/fontend, chú ý chèn vào các thư viện vue.js và toastr (sử dụng để hiển thị thông báo trên trình duyệt).

@extends('layouts.default')

@section('title', 'Referral system - Allaravel.com')
@section('link-header')
    <link href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css" rel="stylesheet">
    <meta id="token" name="token" value="{{ csrf_token() }}">
@endsection
@section('content')
    <div class="container" id="manage-vue">

        <div class="row">
            <div class="col-lg-12 margin-tb">
                <div class="pull-left">
                    <h2>Laravel + VueJS Product CRUD</h2>
                </div>
                <div class="pull-right">
                    <button type="button" class="btn btn-success" data-toggle="modal" data-target="#create-item">
                        Tạo sản phẩm mới
                    </button>
                </div>
            </div>
        </div>

        <!-- Item Listing -->
        <div class="row">
            <div class="col-md-12">
                <table class="table table-bordered">
                    <tr>
                        <th class="col-md-2">Name</th>
                        <th class="col-md-1">Price</th>
                        <th class="col-md-7">Content</th>
                        <th class="col-md-2">Action</th>
                    </tr>
                    <tr v-for="item in items">
                        <td>@{{ item.name }}</td>
                        <td>@{{ item.price }}</td>
                        <td>@{{ item.content }}</td>
                        <td>    
                            <button class="btn btn-primary" @click.prevent="editItem(item)">Edit</button>
                            <button class="btn btn-danger" @click.prevent="deleteItem(item)">Delete</button>
                        </td>
                    </tr>
                </table>
                <!-- Pagination -->
                <nav>
                    <ul class="pagination">
                        <li v-if="pagination.current_page > 1">
                            <a href="#" aria-label="Previous"
                               @click.prevent="changePage(pagination.current_page - 1)">
                                <span aria-hidden="true">«</span>
                            </a>
                        </li>
                        <li v-for="page in pagesNumber"
                            v-bind:class="[ page == isActived ? 'active' : '']">
                            <a href="#"
                               @click.prevent="changePage(page)">@{{ page }}</a>
                        </li>
                        <li v-if="pagination.current_page < pagination.last_page">
                            <a href="#" aria-label="Next"
                               @click.prevent="changePage(pagination.current_page + 1)">
                                <span aria-hidden="true">»</span>
                            </a>
                        </li>
                    </ul>
                </nav>
            </div>
        </div>

        <!-- Create Item Modal -->
        <div class="modal fade" id="create-item" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
                        <h4 class="modal-title" id="myModalLabel">Tạo sản phẩm mới</h4>
                    </div>
                    <div class="modal-body">
                        <form method="POST" enctype="multipart/form-data" v-on:submit.prevent="createItem">
                            <div class="form-group">
                                <label for="name">Name:</label>
                                <input type="text" name="name" class="form-control" v-model="newItem.name" />
                                <span v-if="formErrors['name']" class="error text-danger">@{{ formErrors['name'] }}</span>
                            </div>

                            <div class="form-group">
                                <label for="price">Price:</label>
                                <input type="text" name="price" class="form-control" v-model="newItem.price" />
                                <span v-if="formErrors['price']" class="error text-danger">@{{ formErrors['price'] }}</span>
                            </div>

                            <div class="form-group">
                                <label for="content">Content:</label>
                                <textarea name="content" class="form-control" v-model="newItem.content"></textarea>
                                <span v-if="formErrors['content']" class="error text-danger">@{{ formErrors['content'] }}</span>
                            </div>

                            <div class="form-group">
                                <button type="submit" class="btn btn-success">Tạo sản phẩm</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>

        <!-- Edit Item Modal -->
        <div class="modal fade" id="edit-item" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
                        <h4 class="modal-title" id="myModalLabel">Chỉnh sửa sản phẩm</h4>
                    </div>
                    <div class="modal-body">
                        <form method="POST" enctype="multipart/form-data" v-on:submit.prevent="updateItem(fillItem.id)">
                            <div class="form-group">
                                <label for="name">Name:</label>
                                <input type="text" name="name" class="form-control" v-model="fillItem.name" />
                                <span v-if="formErrorsUpdate['name']" class="error text-danger">@{{ formErrorsUpdate['name'] }}</span>
                            </div>

                            <div class="form-group">
                                <label for="price">Price:</label>
                                <input type="text" name="price" class="form-control" v-model="fillItem.price" />
                                <span v-if="formErrorsUpdate['price']" class="error text-danger">@{{ formErrorsUpdate['price'] }}</span>
                            </div>

                            <div class="form-group">
                                <label for="content">Content:</label>
                                <textarea name="content" class="form-control" v-model="fillItem.content"></textarea>
                                <span v-if="formErrorsUpdate['content']" class="error text-danger">@{{ formErrorsUpdate['content'] }}</span>
                            </div>

                            <div class="form-group">
                                <button type="submit" class="btn btn-success">Cập nhật sản phẩm</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>

    </div>
@endsection
@section('js')
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>

    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/vue.resource/0.9.3/vue-resource.min.js"></script>

    <script type="text/javascript" src="{{ Asset('js/product.js') }}"></script>
@endsection

Tạo route trong routes/web.php

Route::get('/vue-product', 'VueJSProductController@showVueProduct');
Route::resource('vueproduct','VueJSProductController');

Bước 4: Tạo file product.js chứa code Vue.js

Tham khảo các bài viết về Vue.js Framework trước khi thực hiện code product.js, những hướng dẫn cơ bản giúp bạn hiểu nhanh chóng ví dụ CRUD.

Vue.http.headers.common['X-CSRF-TOKEN'] = $("#token").attr("value");

new Vue({

    el: '#manage-vue',

    data: {
        items: [],
        pagination: {
            total: 0, 
            per_page: 2,
            from: 1, 
            to: 0,
            current_page: 1
        },
        offset: 4,
        formErrors:{},
        formErrorsUpdate:{},
        newItem : {'name':'','price':'','content':''},
        fillItem : {'name':'','price':'','content':'','id':''}
    },

    computed: {
        isActived: function () {
            return this.pagination.current_page;
        },
        pagesNumber: function () {
            if (!this.pagination.to) {
                return [];
            }
            var from = this.pagination.current_page - this.offset;
            if (from < 1) {
                from = 1;
            }
            var to = from + (this.offset * 2);
            if (to >= this.pagination.last_page) {
                to = this.pagination.last_page;
            }
            var pagesArray = [];
            while (from <= to) {
                pagesArray.push(from);
                from++;
            }
            return pagesArray;
        }
    },

    ready : function(){
    	this.getvueproduct(this.pagination.current_page);
    },

    methods : {

        getvueproduct: function(page){
            this.$http.get('/vueproduct?page='+page).then((response) => {
                this.$set('items', response.data.data.data);
                this.$set('pagination', response.data.pagination);
            });
        },

        createItem: function(){
            var input = this.newItem;
            this.$http.post('/vueproduct',input).then((response) => {
                this.changePage(this.pagination.current_page);
                this.newItem = {'name':'','price':'','content':''};
                $("#create-item").modal('hide');
                toastr.success('Item Created Successfully.', 'Success Alert', {timeOut: 5000});
            }, (response) => {
                this.formErrors = response.data;
            });
        },

        deleteItem: function(item){
            this.$http.delete('/vueproduct/'+item.id).then((response) => {
                this.changePage(this.pagination.current_page);
                toastr.success('Item Deleted Successfully.', 'Success Alert', {timeOut: 5000});
            });
        },

        editItem: function(item){
            this.fillItem.name = item.name;
            this.fillItem.id = item.id;
            this.fillItem.price = item.price;
            this.fillItem.content = item.content;
            $("#edit-item").modal('show');
        },

        updateItem: function(id){
            var input = this.fillItem;
            this.$http.put('/vueproduct/'+id,input).then((response) => {
                this.changePage(this.pagination.current_page);
                this.fillItem = {'name':'','price':'','content':'','id':''};
                $("#edit-item").modal('hide');
                toastr.success('Item Updated Successfully.', 'Success Alert', {timeOut: 5000});
            }, (response) => {
                this.formErrorsUpdate = response.data;
            });
        },

        changePage: function (page) {
            this.pagination.current_page = page;
            this.getvueproduct(page);
        }
    }
});

Bước 5: Kiểm tra kết quả ứng dụng CRUD quản lý sản phẩm

Vào đường dẫn http://laravel.dev/vue-product chúng ta thấy các sản phẩm được liệt kê từ CSDL đã được phân trang.

Ứng dụng quản lý sản phẩm CRUD sử dụng Vuejs

Khi click vào Tạo sản phẩm mới, một Modal hiện ra

Tạo sản phẩm mới trong ứng dụng Vuejs CRUD

Sử dụng ứng dụng CRUD kết hợp với Vue.js cho thấy một số ưu điểm như sau:

Trải nghiệm người dùng

  • Các thao tác với sản phẩm như create, edit, delete, phân trang… rất thuận tiện và tốc độ xử lý nhanh, không phải tải qua lại nhiều trang.
  • Các thao tác trực quan hơn, giao diện kết hợp Modal rất hiện đại.

Lập trình viên

  • Viết code ngắn gọn hơn, thay vì trước đây phải viết thiết kế nhiều view thì giờ chỉ cần 1 view cho toàn bộ một CRUD của một đối tượng.
  • Tạo ít request hơn đến máy chủ.

Laravel hiện đang là PHP framework số một hiện nay, giờ kết hợp cùng với Vue.js và Bootstrap, OMG không còn gì để nói, bạn có thể xây dựng những ứng dụng web tuyệt vời, cực nhanh, mạnh và hơn hết là mang lại một trải nghiệm tốt cho người dùng.

 

13 thoughts on “Xây dựng ứng dụng CRUD sử dụng Vue.js trong Laravel

  1. Hình như phương thức ready trên phải là mounted mới đúng phải không bạn …
    Cơ mà mình lấy mẫu theo, khi chạy nó gọi api nhiều quá không biết làm gì sai không … Nên phải dùng debounce

  2. hi bạn, cám ơn bạn đã chia sẽ 1 tut rất hay, mình đã làm theo và chạy được trên firefox nhưng mình lại đang vấp phải 1 vấn đề là nó không load được data ra ở chrome, không biết bạn đã có từng gặp phải vấn đề này chưa, rất mong bạn giúp đỡ, mình cám ơn

      1. Hi bạn, mình không có tắt, các ứng dụng khác mình đang làm javascript vẫn chạy bình thường, mình cũng không hiểu sao nó lại như thế, nhưng không sao dù gì vấn đề ở tut này là hiểu được cách làm việc của nó. Cám ơn bạn rất nhiều

  3. hi bạn, bạn có thể hướng dẫn cho mình làm thêm crud nhiều table trong 1 trang được không, hoặc bạn có thể giới thiệu cho mình 1 tut trên internet được không,mình tìm mãi về multi crud với vuejs trong 1 trang không có, Cám ơn bạn

      1. cám ơn bạn đã reply comment của mình, mình mô tả có hơi ngoằng nghèo xíu :D, mình xin mô tả lại như bạn có thể hướng dẫn lại là làm nhiều crud trong 1 trang của laravel. ( ví dụ như mình thấy trong blog có bài viết multi panigation laravel), bạn có thể làm 1 tut hướng dẫn làm nhiều crud trong laravel với vuejs để mọi người có thể tham khảo thêm, hoặc bạn có biết 1 tut nào trên internet có thể gửi cho mình để mình học hỏi, mình tìm mãi mà không ra

        1. Ý tưởng của bạn có phải kiểu như một CRUD tạo danh mục sản phẩm (category) và một CRUD tạo sản phẩm (product) trên cùng một trang? Khi tạo product mà product này chưa có category thì tạo luôn category sau đó tạo product. Nếu đúng như vậy thì kiến thức trong bài viết này hoàn toàn có thể áp dụng được.

  4. Chạy trên firefox thì được. Không chạy được trên Chrome. Và hiện giờ Vue dùng axios rồi không dùng Vue resource nữa

Add Comment