Sự kiện cầu nối trong giao tiếp giữa các Vue.js component

Bài viết được sự cho phép của tác giả Kien Dang Chung

Trong bài Truyền dữ liệu vào Vue.js Component chúng ta đang dang dở ở ví dụ xây dựng greeting-component từ hai component là form-component và hello-component. Dữ liệu đã được truyền đi với chiều thuận từ component cha (component bao ngoài) là greeting-component vào cả hai component con. Chiều ngược lại là rất cần thiết, giống như một function có thể có tham số và cũng có thể trả về một kết quả.

  10 kinh nghiệm khi làm việc với các dự án lớn viết bằng Vue.js
  3 phút làm quen với Vue.js

Mô hình component bên trong component

Chiều ngược lại từ form-component (component con) ra greeting-component (component cha) – chiều số 2 được xử lý bằng các sự kiện trên component con, vì dữ liệu sẽ chỉ thay đổi khi có các sự kiện xảy ra.

Giao tiếp giữa component cha và component con

1. Truyền dữ liệu thông qua sự kiện với $emit

Chúng ta cùng quay lại hoàn thiện ví dụ từ đây bạn sẽ hiểu hơn cách thức truyền dữ liệu theo chiều ngược lại:

<!DOCTYPE html>
<html>
<head>
    <title>Component trong component - Example 4 - Allaravel.com</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
    <template id="hello-template">
        <div class="row">
            <div class="col-md-12">
                <h1>{{ message }} {{ uperCaseUser }}</h1>
            </div>
        </div>
    </template>
    <template id="form-template">
        <div class="row">
            <div class="col-md-3">
                <label for="name">Tên bạn là gì?</label>
            </div>
            <div class="col-md-9">
                <input class="form-control" :value="userName" type="text" @input="onInput">
            </div>
        </div>
    </template>
    <template id="greeting-template">
        <div>
            <form-component :user-name="userName" @emit-input="formInput"></form-component>
            <hello-component :user-name="userName"></hello-component>
        </div>
    </template>

    <div class="container" id="app">
        <greeting-component></greeting-component>
    </div>
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
    <script type="text/javascript">
        Vue.component('hello-component',{
            data: function (){
                return {
                    message: 'Xin chào'
                }
            },
            computed: {
                uperCaseUser: function (){
                    return this.userName.trim().toUpperCase()
                }
            },
            props: ['userName'],
            template: '#hello-template'
        })
        Vue.component('form-component', {
            template: '#form-template',
            props: ['userName'],
            methods: {
                onInput: function (event){
                    this.$emit('emit-input', event.target.value)
                }
            }
        });
        Vue.component('greeting-component', {
            template: '#greeting-template',
            data: function () {
                return {
                    userName: 'Allaravel'
                }
            },
            methods: {
                formInput: function (val){
                    this.userName = val
                }
            }
        });
        new Vue({
            el: '#app'
        });
    </script>
</body>
</html>

Trong ví dụ này có một số thay đổi và chúng ta sẽ cùng nhau mổ xẻ những thay đổi này:

Thay đổi thứ nhất trong form-component:

<template id="form-template">
    <div class="row">
        <div class="col-md-3">
            <label for="name">Tên bạn là gì?</label>
        </div>
        <div class="col-md-9">
            <input class="form-control" :value="userName" type="text" @input="onInput">
        </div>
    </div>
</template>

Thay vì sử dụng v-model để gắn dữ liệu hai chiều giữa thẻ <input> và biến name, chúng ta sử dụng v-bind:value=”userName” sử dụng trực tiếp props userName vì v-bind chỉ gán một chiều từ props userName vào View nên ta bỏ qua biến name (Trong bài Truyền dữ liệu vào Vue.js component chúng ta sử dụng biến name để tránh thay đổi giá trị được truyền vào một cách trực tiếp, giờ sử dụng v-bind không cần đến). Chúng ta cũng quản lý sự kiện input trên ô nhập liệu này vì nó phát sinh dữ liệu thay đổi và mục đích cuối là cập nhật dữ liệu thay đổi ngược lại greeting-component.

Vue.component('form-component', {
    template: '#form-template',
    props: ['userName'],
    methods: {
        onInput: function (event){
            this.$emit('emit-input', event.target.value)
        }
    }
});

khi sự kiện onInput xảy ra nó sẽ phát sinh ra một sự kiện ’emit-input’ để greeting-component (component cha) quản lý bằng hàm this.$emit với hai tham số:

  • Tham số đầu là tên sự kiện muốn phát đến component cha.
  • Dữ liệu muốn gửi kèm đến sự kiện này.

Ở đây event.target.value chứa giá trị text hiện tại của ô nhập liệu tên.

Thay đổi thứ hai là ở greeting-component:

<template id="greeting-template">
    <div>
        <form-component :user-name="userName" @emit-input="formInput"></form-component>
        <hello-component :user-name="userName"></hello-component>
    </div>
</template>

Trong form-component chúng ta có thêm quản lý sự kiện v-on:emit-input (viết tắt @emit-input, xem thêm Quản lý sự kiện trong Vue.js). Khi sự kiện này phát sinh từ component con gửi đến component cha, chúng ta sẽ cập nhật giá trị được truyền qua sự kiện vào biến trên component cha:

Vue.component('greeting-component', {
    template: '#greeting-template',
    data: function () {
        return {
            userName: 'Allaravel'
        }
    },
    methods: {
        formInput: function (val){
            this.userName = val
        }
    }
});

như vậy formInput() sẽ nhận được event.target.value từ component con truyền lên, đây cũng là cách thức truyền dữ liệu theo chiều ngược từ component con lên component cha.

2. Rút gọn code component với v-model

Từ phiên bản Vue.js 2.2 trở đi, Vue.js cho phép sử dụng v-model với một component, đây là câu lệnh cho phép gán dữ liệu hai chiều trong một thẻ HTML. Vue.js component là một thẻ HTML tổng hợp hơn, làm được nhiều việc hơn, tại sao lại không dùng được v-model? v-model sử dụng trong component với hai nhiệm vụ:

  • Sử dụng thuộc tính value của component như là props
  • Sử dụng sự kiện input của component là sự kiện mặc định

Chúng ta cùng sử dụng v-model cho ví dụ ở trên, thay vì viết phức tạp:

<form-component :user-name="userName" @emit-input="formInput"></form-component>

methods: {
    formInput: function (val){
        this.userName = val
    }
}

Chúng ta chỉ cần sử dụng v-model như sau:

<form-component v-model="userName"></form-component>

Chạy thử code bạn thấy không hoạt động, tại sao vậy? v-model sử dụng sự kiện input mặc định mà chúng ta lại emit sự kiện emit-input thì hoạt động sao được:

this.$emit('emit-input', event.target.value)

cần sửa lại thành

this.$emit('input', event.target.value)

Ok, ví dụ hoạt động tốt rồi đấy, với cách dùng v-model với component chúng ta đã rút gọn được kha khá code. Với một số thẻ HTML không có có sự kiện input, khi đó các component được xây dựng trên thẻ này không thể sử dụng sự kiện mặc định input, vậy có thể thiết lập sự kiện khác thay thế không? Vue.js cho phép tùy chọn sự kiện khác thay thế cho sự kiện input mặc định. Tương tự như vậy với thuộc tính value, bạn hoàn toàn có thể thiết lập thuộc tính khác mặc định cho việc sử dụng v-model:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: '
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  '
})

Khi đó bạn hoàn toàn sử dụng v-model như bình thường:

<base-checkbox v-model="lovingVue"></base-checkbox>

Giá trị của lovingVue sẽ được truyền vào prop checked. Thuộc tính lovingVue cũng được cập nhật khi component <base-checkbox>phát ra sự kiện change với một giá trị.

3. Giao tiếp giữa các component không có quan hệ

Từ đầu đến giờ chúng ta chủ yếu bàn luận về cách giao tiếp giữa các component có quan hệ cha con, tức là component cha sử dụng lại component con. Với mối quan hệ này, bạn chỉ cần ghi nhớ một câu “prop down emit up”.

Giao tiếp giữa component cha và component con

Trong ứng dụng không phải lúc nào quan hệ giữa các component cũng là quan hệ cha con, có khi hai component ngang hàng nhau cũng cần có sự giao tiếp với nhau. Khi các component không có quan hệ với nhau có thể sử dụng pattern event bus. Đầu tiên, chúng ta cần tạo ra event bus và export nó để các module khác hoặc component khác có thể sử dụng lại (nếu component dạng Single file component bạn cần đọc qua để hiểu phần tiếp theo). Tạo một file event-bus.js với nội dung:

import Vue from 'vue';
export const EventBus = new Vue();

Chúng ta vừa tạo ra một module với Javascript ES6 (ECMAScript 6), nó import Vue và export một Vue instance. Mọi việc tiếp theo là import thư viện này và export instance (EventBus) để sử dụng trong các component. Mỗi component sẽ có thể gửi đi một sự kiện và component khác có thể lắng nghe sự kiện thông qua event bus. Tất cả những gì cần làm chỉ gói gọn trong câu trên, tiếp theo chúng ta sẽ đi vào một ví dụ cụ thể để thấy rõ hơn.

3.1 Gửi đi một sự kiện

Tạo component-a theo kiểu Single file component (tạo file component-a.js):

<template>
  <div @click="emitMethod">Component A</div>
</template>
<script>
// import EventBus tạo ở trên
import { EventBus } from './event-bus.js';

export default {
  data() {
    return {
      clickCount: 0
    }
  },
  methods: {
    emitMethod() {
      console.log('Component A gửi đi một sự kiện vào event bus chung');
      // Sử dụng phương thức $emit để gửi sự kiện vào bus chung
      EventBus.$emit('component-a-clicked', this.clickCount);
    }
  }
}
</script>

3.2 Nhận một sự kiện

Tiếp theo, component-b còn lại sẽ lắng nghe sự kiện component-a phát ra bằng cách sử dụng phương thức ontrênEventBusinstance:EventBus.ontrênEventBusinstance:EventBus.on(channel: string, callback(payload1,…)). Tạo file component-b.js với nội dung:

// Import EventBus.
import { EventBus } from './event-bus.js';

// Lắng nghe component-a-clicked từ component-a
EventBus.$on('component-a-clicked', clickCount => {
  console.log('Nhận được ${clickCount} click từ component A')
});

Như vậy, khi sử dụng các component-a, component-b trong một ứng dụng (tạo file app.js), khi đó hai component-a và component-b đã giao tiếp với nhau:

import Vue from 'vue';
import ComponentA form './component-a'
import ComponentB form './component-b';

new Vue({
  el: "#app",
  components: {
    ComponentA,
    ComponentB
  }
});

3.3 Dừng lắng nghe sự kiện

Đôi khi chúng ta không muốn tiếp tục lắng nghe sự kiện để giao tiếp, có thể sử dụng phương thức $off trên EventBus instance:

EventBus.$off('component-a-clicked', clickHandler);

4. Lời kết

Component là khái niệm quan trọng trong Vue.js nó giúp mô đun hóa các ứng dụng lớn. Các component có thể giao tiếp với nhau với cả mô hình component cha con và mô hình component ngang hàng. Trong những bài tiếp theo chúng ta sẽ tiếp tục tìm hiểu về Vue component. Mọi thắc mắc bạn hãy để lại comment ở dưới bài nhé.

Bài viết gốc được đăng tải tại allaravel.com

Có thể bạn quan tâm:

Xem thêm Việc làm Developer hấp dẫn trên TopDev