[VueJS] Giao tiếp giữa các component

Component trong VueJs là gì?

Component là một trong những tính năng quan trọng nhất trong Vue mà mình cần phải biết. Nó giúp cho việc kế thừa các thành phần HTML cơ bản, dễ dàng đóng gói và tái sử dụng code. Ở mức cao hơn thì component như là custom elements mà trình biên dịch của Vue sẽ gắn các xử lý vào đó. Trong một vài trường hợp nó cũng có thể xuất hiện như một phần tử HTML được mở rộng (native html element extended) bởi thuộc tính đặc biệt là is

Nếu ứng dụng mình xây dựng sử dụng component thì có thể tưởng tượng các thành phần bên trong sẽ như sau Và nó đúng với việc tái sử dụng lại các component giống như việc mình sử dụng các thẻ HTML (input, p ...) nhiều lần vậy.

Việc tổ chức các component cũng giống như một Dom tree, cũng có phân cấp thành phần.

Giao tiếp giữa các Component

Khởi tạo ứng dụng

Để tạo template cho ứng dụng mình sử dụng vue-cli. CLI (Command-line interface) đơn giản cho việc khởi tạo bộ khung của ứng dụng Vue

$ vue init webpack-simple component-communication

Đi tới đường dẫn thư mục project cài đặt các package cần thiết

$ npm install

Cài đặt babel-preset-stage-3

$ npm install --save-dev babel-preset-stage-3

Start ứng dụng

$ npm run dev

Mở trình duyệt đi đến đường dẫn localhost:8080 (hoặc một port khác trên màn hình console echo ra) để xem kết quả

Cấu trúc code demo

Trong folder project, mình sẽ tạo folder components để chứa code các thành phần component. Trong folder components tạo 3 file User.vue, UserDetail.vue, UserEdit.vue

Root component (Parent component) User.vue

<template>
    <div class="component">
        <p>Ten toi la {{ name }}</p>
        <p>Toi nam nay {{ age }} tuoi</p>
        <hr>
        <div class="row">
            <div class="col-xs-12 col-sm-6 col-6">
                <app-user-detail></app-user-detail>
            </div>
            <div class="col-xs-12 col-sm-6 col-6">
                <app-user-edit></app-user-edit>
            </div>
        </div>
    </div>
</template>

<script>
    import UserDetail from './UserDetail.vue';
    import UserEdit from './UserEdit.vue';

    export default {
        data: function () {
            return {
                name: 'Duy',
                age: 23
            };
        },

        components: {
            appUserDetail: UserDetail,
            appUserEdit: UserEdit
        }
    }
</script>

UserDetail.vue

<template>
    <div class="component">
        <h3>User Details</h3>
        <p>Tuoi toi:</p>
    </div>
</template>

<script>
</script>

UserEdit.vue

<template>
    <div class="component">
        <h3>Edit User</h3>
        <p>Tuoi cua toi: </p>
        <button>Thay doi</button>
    </div>
</template>

<script>
</script>

Thay đổi nội dung file App.vue như sau

<template>
    <div class="container">
        <div class="row">
            <div class="col-xs-12">
                <app-user></app-user>
            </div>
        </div>
    </div>
</template>

<script>
    import User from './components/User.vue';

    export default {
        components: {
            appUser: User
        }
    }
</script>

Kết quả mình nhận được

Sử dụng Prop cho parent->child communication

Mỗi component instance đều có một scope riêng của nó, nghĩa là mình không thể và cũng không nên trực tiếp gọi tới parent data trong child component template. Data có thể gửi xuống từ compoent cha thông qua một custom attribute là props. Trở lại với dí dụ demo mình sẽ truyền data nameage từ component cha User xuống UserDetails. Sử dụng props trong child component (UserDetail.vue)

<template>
    <p>Tuoi toi: {{ age }}</p>
</template>
<script>
	export default {
		props: ['age']
	}
</script>

Để có thể pass được data từ component cha xuống component con, mình sẽ sử dụng v-bind property trong component cha như sau:

# User.vue
<app-user-detail v-bind:age="age"></app-user-detail>

Bây giờ mình có thể dụng age gần giống như data trong child component

Sử dụng custom event cho child->parent communication

Trong UserDetail.vue mình sẽ tạo một button mà khi click vào mình muốn tuổi của mình sẽ thay đổi thành 24

//UserDetail.vue

<button v-on:click="changeAge">Doi tuoi</button>

// function changeAge
methods: {
    changeAge () {
        this.age = 24
    }
}

Kết qủa mình nhận được đó là chỉ có thay đổi ở trong component con là UserDetail

Vấn đề ở đây là mình muốn thay đổi từ bên trong thằng con cũng sẽ tác động đến thằng cha. Component kế thừa từ Vue instance do đó nó cũng là một instance của vuejs nên mình sẽ sử dụng một build in method của vuejs đó là $emit . Sử dụng emit để mình có thể gửi đi một sự kiện ra bên ngoài component, thông báo với cha của nó rằng hey tao vừa cập nhật tuổi đấy, giờ là 24 nhé từ đó component cha sẽ cập nhật tuổi tương ứng. Hàm $emit mình sẽ truyền vào 2 tham số, tham số thứ nhất là định danh của event (để biết là m đổi tuổi chứ không đổi tên) và tham số thứ dữ liệu muốn truyền đi tới hàm nhận xử lý

// UserDetail.vue
changeAge () {
    this.age = 24
    this.$emit('ageWasUpdated', this.age)
},

Bây giờ tại component cha mình cần bắt lấy cái sự kiên mà child component phát ra

// User.vue
<button @click="resetAge">Reset tuoi</button>
<app-user-detail :age="age" @ageWasUpdated="age = $event"></app-user-detail>
// Thêm method resetAge trong script
methods: {
    resetAge () {
        this.age = 23
    }
}

$event sẽ refer tới tham chiếu tới data mà mình đã truyền lên thông qua hàm $emit Kết quả ok khi mà việc cập nhật dữ liệu ở con hay ở cha đều được đồng bộ sang bên kia.

Giao tiếp giữa các child component

Hai compnent có chung một parent trực tiếp

Trong nhiều trường hợp mình chỉ muốn tương tác trực tiếp giữa các thành phần child component với nhau, tuy nhiên điều này là không thể. Để có thể giao tiếp với child component khác đều phải thông qua component cha. Luồng xử lý này được gọi là Undirection data flow

Thực hành luôn, giờ mình muốn thay đổi tuổi từ compoent UserEdit và muốn giá trị đó sẽ được cập nhật tương ứng trong UserDetail

Parent pass data xuống child component thông qua props

// User.vue
    // Truyền props age xuống UserEdit
    <app-user-edit :user-age="age"></app-user-edit>

Child 1 báo với parent về thay đổi thông qua emit event Trong component Userdetail.vue addListener cho button "Thay doi" khi có sự kiện click vào sẽ emit event báo với cha nó rằng "ê thay đổi tuổi của user đi nhé"

// UserEdit.vue
    // template
    <button @click="editAge">Thay doi</button>
    // script
    export default {
        ...
        props: ['userAge']
        methods: {
            editAge () {
                this.userAge += 1
                this.$emit('ageWasEdited', this.userAge)
            }
        }
    }

Parent nhận event và xử lý

// User.vue
    <app-user-edit
        :user-age="age"

        // chỗ này lười viết func thay đổi age để inline luôn nhé :))
        // $event chính là thằng data mình gửi lên trong hàm emit \từ UserEdit
        @ageWasEdited="age = $event"
    ></app-user-edit>

age mới được parent gửi lại xuống 2 thằng child

Hai component không cùng một parent trực tiếp

Tưởng tượng nhánh cây component của bạn phát triển sâu thêm, khi đó để giao tiếp giữa 2 thằng component thôi mình có thể phải làm childx -> childy -> childz -> parent -> childe Khi đó việc giao tiếp giữa các component là cồng kềnh và khó quản lý, để giải quyết vấn đề đó mình sử dụng tới event bus/publish-subscribe pattern - empty vue instance giống như một người vận chuyển. Khởi tạo vue instance này tại root component

// main.js
// Chú ý để thằng này lên đầu trước khi khỏi tạo main vue \instance
export const eventBus = new Vue()
// UserEdit.vue
    import { eventBus } from '../main'
    ...
    // script
        // thay vì emit sự kiện từ this instance, giờ mình emit nó từ eventBus
        //" this.$emit('ageWasEdited', this.userAge)
        eventBus.$emit('ageWasEdited', this.userAge)
// UserDetail.vue
    import { eventBus } from '../main'
    ...
    created () {
        eventBus.$on('ageWasEdited', (age) => {
            this.age = age
        })
    }    

Kết quả

Bạn có thể thấy là hai child component đã giao tiếp được với nhau mà không cần thông qua parent như trước.

Ở đây bạn thấy kết quả hiển thị vẫn như mong muốn nhưng khi bật development tool (console) lên thì sẽ thấy vue có cảnh báo là tránh thay đổi giá trị của property một cách trực tiếp. Ví dụ cùng một data X từ component cha, truyền xuống các component con thông qua props chẳng hạn, mà nếu để thằng con nào cũng cập nhật prop một cách trực tiếp như thế sẽ dẫn tới khó quản lý không biết thằng nào thay đổi và thay đổi khi nào. Prop được thay đổi từ component gốc sẽ được cập nhật xuống component con còn chiều ngược lại thì lại không xảy ra. Giải pháp ở đây bạn có khai báo một biến local data nghĩa là một biết data ở chính component mà bạn đang sử dụng giá trị biến prop và muốn thay đổi nó.

Kết luận

  • Qua bài viết mình đã chia sẻ những cách cơ bản nhất có thể sử dụng để giao tiếp, chia sẻ trạng thái giữa các component với nhau mà mình đã tìm hiểu được trong quá trình tìm hiểu về Vuejs
  • Nên kết hợp sử các cách thức này cùng với nhau để phù hợp với ứng dụng của mình
  • Vuex bạn nên tìm hiều về khái niệm này và sử dụng nó! Hãy để ý eventBus giống như cái tên của nó vậy giờ cao điểm "xe bus" quá tải bạn sẽ làm như thế nào? Rất nhiều sự kiện và event cần quản lý?

Cảm ơn bạn đã dành thời gian đọc hết bài chia sẻ của mình!