Two-ways data binding với Object.observe

Các framework Javascript thường nhấn mạnh vào tính năng data binding như một ưu thế nổi bật của chúng. Nhưng thực ra bạn có thể thêm data binding vào ứng dụng Javascript của bạn một cách nhanh chóng và không có gì khó khăn cả.

Data binding về bản chất chỉ là việc đồng bộ thông tin một cách tự động giữa phần thể hiện ra ngoài trang web (DOM element, thường liên quan đến View) và phần dữ liệu được kiểm soát bên trong code Javascript (properties, thường liên quan đến Model). Vì thế, trong mô hình MVC, người ta thường hiểu data binding như là cơ chế tự cập nhật View khi Model thay đổi, và/hoặc tự cập nhật Model khi View thay đổi. Nếu sự đồng bộ diễn ra theo cả 2 chiều thì đó là two-ways binding.

Nguyên lý của data binding là theo dõi sự thay đổi của DOM và đối tượng Javascript liên quan, một khi có thay đổi ở phía bên này thì phát tín hiệu để cập nhật ở đầu kia của mối quan hệ.

Ảnh minh họa data binding, codeproject.com

Một trường hợp cụ thể

Giả sử chúng ta có 1 object:

var people = {
  firstName: "Dong",
  lastName: "Nguyen"
}

và chúng ta output thông tin này ra ngoài giao diện bằng đoạn HTML sau:

<input type="text" id="txtFirstName" value="">
<input type="text" id="txtLastName" value="">

Để hiển thị, chúng ta có script sau:

var people = {
  firstName: "Dong",
  lastName: "Nguyen"
}

var element = function(id){
  return document.getElementById(id);   
}

var $firstName = element('txtFirstName');
var $lastName = element('txtLastName');

var render = function(){
  $firstName.value = people.firstName;
  $lastName.value = people.lastName;
}

render();

Nhìn nó như thế này:

https://jsfiddle.net/ndaidong/76eadzth/1/

Ở đây nói về data binding tức là chúng ta muốn :
  • Khi user thay đổi giá trị của các input thì thuộc tính của people cũng thay đổi theo. Đây là chiều từ DOM tới Object.
  • Khi kịch bản làm cho thuộc tính của people thay đổi thì giá trị của các input cũng được thay đổi. Đây là chiều từ Object tới DOM.

Chiều thứ nhất: từ DOM tới Object

Để giải quyết việc đồng bộ từ DOM tới Object thì rất dễ vì có thể gắn event listener vào bất kỳ HTML element nào để lắng nghe. Chúng ta xử lý đoạn này trước bằng hàm domToObject như sau:

function domToObject(source, target, attribute){
  source.onchange = function(){
    var v = String(source.value);
    target[attribute] = v;
  }
}

Hàm này nhận vào 3 tham số, phần tử DOM đóng vai trò nguồn phát tín hiệu, đối tượng Javascript đóng vai trò tiếp nhận tín hiệu, và thuộc tính mà chúng ta muốn theo dõi. Bây giờ hãy thử cho kịch bản tự cập nhật các giá trị của đối tượng people khi bên ngoài input thay đổi:

 domToObject($firstName, people, 'firstName');
 domToObject($lastName, people, 'lastName');

Kết quả như thế này:

https://jsfiddle.net/ndaidong/76eadzth/15/

Các bạn thử thay đổi các giá trị trong inputs và nhấn button bên dưới để kiểm tra xem các thuộc tính của people đã được đồng bộ ra sao.

Chiều thứ 2: từ Object tới DOM

Bây giờ, chúng ta xét theo chiều ngược lại, từ Object tới DOM. Thời “xa xưa”, mọi thứ rất phức tạp. Hãy xem bài viết của Luca Ongaro cách đây 2 năm:

Easy Two-Way Data Binding in JavaScript

Nhưng bây giờ, với Object.observe, câu chuyện trở nên đơn giản hơn nhiều.

Đây là cách tôi xử lý, với một hàm objectToDom như sau:

function objectToDom(source, target, property){
  Object.observe(source, function(changes){
    changes.forEach(function(change){
      if(change.name === property){
        target.value = change.object[property];
      }
    });
  });
}

Hàm này cũng nhận vào 3 tham số, nhưng bắt đầu với source là một đối tượng Javascript, kế đó - target - là phần tử DOM móc nối với nó, và cuối cùng là property - tên thuộc tính mà chúng ta muốn nắm bắt sự thay đổi.

Kết quả như sau:

https://jsfiddle.net/ndaidong/76eadzth/16/

Trong demo, tôi để people tự thay đổi giá trị các thuộc tính firstNamelastName thành “Altonio” và “Vivaldi” sau vài giây. Các bạn có thể nhấn button để thay đổi chúng một cách ngẫu nhiên thành tên các tổng thống Mỹ ;)

Và đây là vấn đề chính: trong hàm objectToDom, tôi sử dụng Object.observe để theo dõi các biến đổi trên đối tượng Javascript.

Syntax của Object.observe như sau:

Object.observe(ob, callback);

Khi 1 hay một số thuộc tính của ob thay đổi, hàm callback sẽ được gọi cùng với tham số là một mảng chứa các thuộc tính đã thay đổi. Trên website của Mozilla có giải thích rất rõ về Object.observe.

Các ví dụ trong bài dùng để minh họa cách áp dụng data binding theo từng chiều ở mức độ cơ bản. Khi đưa vào thực tế, các bạn có thể cần bổ sung thêm các phương thức khác để testing, validating…

Now, hãy thử ghép chúng lại…

Đồng bộ 2 chiều

Dưới đây tôi sẽ xử lý cả 2 chiều cùng lúc bằng cách định nghĩa 1 đối tượng DataBinder như sau:

var DataBinder = (function(){

  var domToObject = function(source, target, attribute){
    source.onchange = function(){
      var v = String(source.value);
      target[attribute] = v;
    }
  }

  var objectToDom = function(source, target, property){
    Object.observe(source, function(changes){
      changes.forEach(function(change){
        if(change.name === property){
          target.value = change.object[property];
        }
      });
    });
  }

  var isElement = function(v){
    if(!!v && typeof v === 'object'){
      var ots = Object.prototype.toString;
      if(ots.call(v).indexOf('HTML') !== -1 && ots.call(v).indexOf('Element') !== -1){
        return true;
      }
    }
    return false;
  }     

  var start = function(source, target, attr){
    if(isElement(source)){
      domToObject(source, target, attr);
      objectToDom(target, source, attr);
    }
    else{
      objectToDom(source, target, attr);
      domToObject(target, source, attr);
    }
  }

  return {
    domToObject: domToObject,
    objectToDom: objectToDom,
    start: start
  }
})();

Như các bạn thấy, DataBinder có 3 public methods: domToObject, objectToDomstart dùng để tự động bind theo 2 chiều. Nhờ đó tôi chỉ việc gọi:

DataBinder.start(people, $firstName, 'firstName');
DataBinder.start(people, $lastName, 'lastName');

Hoặc có thể đảo thứ tự 2 tham số đầu tiên:

DataBinder.start($firstName, people, 'firstName');
DataBinder.start($lastName, people, 'lastName');

Dù thế nào, kết quả thu được vẫn như sau:

https://jsfiddle.net/ndaidong/76eadzth/17/

Cần nhắc lại, DataBinder như trên vẫn thiếu một vài thứ như exception handling, validation… Trong bài này, tôi chỉ muốn share concept của Object.observe và việc áp dụng nó trong two-ways data binding mà thôi.

Lời kết

Object.observe được mô tả trong ES7, hiện nay mới chỉ có trình duyệt Chrome hỗ trợ (33+). Nhưng polyfills của Jeremy Darling hoặc MaxArt2501 viết sẽ giúp bạn đem nó vào các trình duyệt khác. Chỉ việc load script từ CDN của Polyfills.io như cách tôi load vào JSFiddle.

Mong rằng qua những gì tôi đề cập trên đây, các fan mới của Javascript có thể hiểu được bản chất vấn đề data binding và có khả năng ứng dụng Object.observe vào thực tiễn.

Cảm ơn các bạn đã theo dõi bài viết này.