Cách triển khai liên kết dữ liệu DOM trong JavaScript


244

Xin vui lòng coi câu hỏi này là giáo dục nghiêm ngặt. Tôi vẫn thích nghe câu trả lời và ý tưởng mới để thực hiện điều này

tl; dr

Làm cách nào để triển khai liên kết dữ liệu hai chiều với JavaScript?

Liên kết dữ liệu với DOM

Ví dụ, bằng cách liên kết dữ liệu với DOM, có một đối tượng JavaScript acó thuộc tính b. Sau đó, có một <input>phần tử DOM (ví dụ), khi phần tử DOM thay đổi, athay đổi và ngược lại (đó là, tôi có nghĩa là ràng buộc dữ liệu hai chiều).

Đây là một sơ đồ từ AngularJS về cái này trông như thế nào:

ràng buộc dữ liệu hai chiều

Về cơ bản, tôi có JavaScript tương tự như:

var a = {b:3};

Sau đó, một yếu tố đầu vào (hoặc hình thức khác) như:

<input type='text' value=''>

Tôi muốn giá trị của đầu vào là giá a.btrị (ví dụ) và khi văn bản đầu vào thay đổi, tôi cũng muốn a.bthay đổi. Khi a.bthay đổi trong JavaScript, đầu vào thay đổi.

Câu hỏi

Một số kỹ thuật cơ bản để thực hiện điều này trong JavaScript đơn giản là gì?

Cụ thể, tôi muốn có một câu trả lời hay để tham khảo:

  • Làm thế nào sẽ ràng buộc làm việc cho các đối tượng?
  • Làm thế nào để lắng nghe để thay đổi trong hình thức có thể làm việc?
  • Có thể theo một cách đơn giản là chỉ sửa đổi HTML ở cấp độ mẫu? Tôi không muốn theo dõi ràng buộc trong chính tài liệu HTML mà chỉ trong JavaScript (với các sự kiện DOM và JavaScript giữ tham chiếu đến các thành phần DOM được sử dụng).

Tôi đã thử những gì?

Tôi là một fan hâm mộ lớn của bộ ria mép nên tôi đã thử sử dụng nó để tạo khuôn mẫu. Tuy nhiên, tôi gặp vấn đề khi cố gắng tự thực hiện ràng buộc dữ liệu do Mustache xử lý HTML dưới dạng chuỗi nên sau khi tôi nhận được kết quả, tôi không có tham chiếu đến vị trí của các đối tượng trong chế độ xem của mình. Cách giải quyết duy nhất tôi có thể nghĩ cho việc này là sửa đổi chuỗi HTML (hoặc cây DOM được tạo) bằng các thuộc tính. Tôi không phiền khi sử dụng một công cụ tạo khuôn mẫu khác.

Về cơ bản, tôi có một cảm giác mạnh mẽ rằng tôi đang làm phức tạp vấn đề trong tay và có một giải pháp đơn giản.

Lưu ý: Vui lòng không cung cấp câu trả lời sử dụng các thư viện bên ngoài, đặc biệt là các thư viện có hàng ngàn dòng mã. Tôi đã sử dụng (và thích!) AngularJS và KnockoutJS. Tôi thực sự không muốn câu trả lời ở dạng 'sử dụng khung x'. Tối ưu, tôi muốn một người đọc trong tương lai không biết cách sử dụng nhiều khung để nắm bắt cách tự thực hiện ràng buộc dữ liệu hai chiều. Tôi không mong đợi một câu trả lời hoàn chỉnh , nhưng một câu trả lời có ý tưởng.


2
Tôi dựa trên CrazyGlue dựa trên thiết kế của Benjamin Gruenbaum. Nó cũng hỗ trợ CHỌN, hộp kiểm và thẻ radio. jQuery là một phụ thuộc.
JohnSz

12
Câu hỏi này là hoàn toàn tuyệt vời. Nếu nó bị đóng cửa vì lạc đề hoặc một số điều vô nghĩa ngớ ngẩn khác, tôi sẽ bị đánh dấu nghiêm trọng.
OCDev

@JohnSz cảm ơn vì đã đề cập đến dự án CrazyGlue của bạn. Tôi đã tìm kiếm một chất kết dính dữ liệu 2 chiều đơn giản trong một thời gian dài. Có vẻ như bạn không sử dụng Object.observe nên hỗ trợ trình duyệt của bạn sẽ rất tuyệt. Và bạn không sử dụng khuôn ria mép để nó hoàn hảo.
Gavin

@Benjamin Rốt cuộc bạn đã làm gì?
johnny

@johnny theo tôi cách tiếp cận đúng là tạo DOM trong JS (như React) chứ không phải ngược lại. Tôi nghĩ rằng cuối cùng đó là những gì chúng ta sẽ làm.
Benjamin Gruenbaum

Câu trả lời:


106
  • Làm thế nào sẽ ràng buộc làm việc cho các đối tượng?
  • Làm thế nào để lắng nghe để thay đổi trong hình thức có thể làm việc?

Một bản tóm tắt cập nhật cả hai đối tượng

Tôi cho rằng có các kỹ thuật khác, nhưng cuối cùng tôi cũng có một đối tượng tham chiếu đến một phần tử DOM có liên quan và cung cấp một giao diện điều phối các cập nhật cho dữ liệu của chính nó và phần tử liên quan của nó.

Các .addEventListener()cung cấp một giao diện rất đẹp cho việc này. Bạn có thể cung cấp cho nó một đối tượng thực hiện eventListenergiao diện và nó sẽ gọi các trình xử lý của nó với đối tượng đó làm thisgiá trị.

Điều này cho phép bạn tự động truy cập vào cả phần tử và dữ liệu liên quan.

Xác định đối tượng của bạn

Kế thừa nguyên mẫu là một cách tốt đẹp để thực hiện điều này, mặc dù tất nhiên không bắt buộc. Trước tiên, bạn tạo một hàm tạo nhận phần tử của bạn và một số dữ liệu ban đầu.

function MyCtor(element, data) {
    this.data = data;
    this.element = element;
    element.value = data;
    element.addEventListener("change", this, false);
}

Vì vậy, ở đây hàm tạo lưu trữ phần tử và dữ liệu trên các thuộc tính của đối tượng mới. Nó cũng liên kết một changesự kiện cho trước element. Điều thú vị là nó truyền đối tượng mới thay vì hàm như đối số thứ hai. Nhưng điều này một mình sẽ không làm việc.

Thực hiện eventListenergiao diện

Để thực hiện công việc này, đối tượng của bạn cần thực hiện eventListenergiao diện. Tất cả những gì cần thiết để thực hiện điều này là cung cấp cho đối tượng một handleEvent()phương thức.

Đó là nơi mà sự kế thừa đến.

MyCtor.prototype.handleEvent = function(event) {
    switch (event.type) {
        case "change": this.change(this.element.value);
    }
};

MyCtor.prototype.change = function(value) {
    this.data = value;
    this.element.value = value;
};

Có nhiều cách khác nhau để có thể cấu trúc nó, nhưng với ví dụ của bạn về việc phối hợp các bản cập nhật, tôi quyết định làm cho change()phương thức chỉ chấp nhận một giá trị và có handleEventgiá trị đó thay vì đối tượng sự kiện. Bằng cách này, change()có thể được gọi mà không có sự kiện là tốt.

Vì vậy, bây giờ, khi changesự kiện xảy ra, nó sẽ cập nhật cả phần tử và thuộc .datatính. Và điều tương tự sẽ xảy ra khi bạn gọi .change()trong chương trình JavaScript của mình.

Sử dụng mã

Bây giờ bạn chỉ cần tạo đối tượng mới và để nó thực hiện cập nhật. Các cập nhật trong mã JS sẽ xuất hiện trên đầu vào và các sự kiện thay đổi trên đầu vào sẽ hiển thị với mã JS.

var obj = new MyCtor(document.getElementById("foo"), "20");

// simulate some JS based changes.
var i = 0;
setInterval(function() {
    obj.change(parseInt(obj.element.value) + ++i);
}, 3000);

DEMO: http://jsfiddle.net/RkTMD/


5
+1 Cách tiếp cận rất sạch sẽ, rất đơn giản và đủ đơn giản để mọi người học hỏi, sạch sẽ hơn rất nhiều so với những gì tôi có. Một trường hợp sử dụng phổ biến là sử dụng các mẫu trong mã để thể hiện các khung nhìn của các đối tượng. Tôi đã tự hỏi làm thế nào điều này có thể làm việc ở đây? Trong các công cụ như Mustache tôi làm một cái gì đó Mustache.render(template,object), giả sử tôi muốn giữ một đối tượng được đồng bộ hóa với mẫu (không cụ thể cho Mustache), tôi sẽ tiếp tục như thế nào?
Benjamin Gruenbaum

3
@BenjaminGruenbaum: Tôi chưa sử dụng các mẫu phía máy khách, nhưng tôi sẽ tưởng tượng rằng Mustache có một số cú pháp để xác định các điểm chèn và cú pháp đó bao gồm nhãn. Vì vậy, tôi nghĩ rằng các phần "tĩnh" của mẫu sẽ được hiển thị thành các đoạn HTML được lưu trữ trong một mảng và các phần động sẽ nằm giữa các khối đó. Sau đó, các nhãn trên các điểm chèn sẽ được sử dụng làm thuộc tính đối tượng. Sau đó, nếu một số inputlà cập nhật một trong những điểm đó, sẽ có một ánh xạ từ đầu vào đến điểm đó. Tôi sẽ xem liệu tôi có thể đưa ra một ví dụ nhanh không.

1
@BenjaminGruenbaum: Hmmm ... Tôi chưa nghĩ đến cách phối hợp sạch sẽ hai yếu tố khác nhau. Điều này có liên quan nhiều hơn một chút so với tôi nghĩ lúc đầu. Mặc dù tôi tò mò, vì vậy tôi có thể cần phải làm việc này một lát sau. :)

2
Bạn sẽ thấy rằng có một hàm tạo chính Templatethực hiện phân tích cú pháp, giữ các MyCtorđối tượng khác nhau và cung cấp một giao diện để cập nhật từng đối tượng bằng mã định danh của nó. Hãy cho tôi biết nếu bạn có thắc mắc. :) EDIT: ... sử dụng liên kết này thay vào đó ... Tôi đã quên rằng tôi đã tăng theo cấp số nhân giá trị đầu vào cứ sau 10 giây để thể hiện các cập nhật JS. Điều này giới hạn nó.

2
... phiên bản nhận xét đầy đủ cộng với những cải tiến nhỏ.

36

Vì vậy, tôi quyết định ném giải pháp của riêng tôi vào nồi. Đây là một fiddle làm việc . Lưu ý điều này chỉ chạy trên các trình duyệt rất hiện đại.

Những gì nó sử dụng

Việc triển khai này rất hiện đại - nó đòi hỏi một trình duyệt (rất) hiện đại và người dùng hai công nghệ mới:

  • MutationObservers để phát hiện những thay đổi trong dom (người nghe sự kiện cũng được sử dụng)
  • Object.observeđể phát hiện những thay đổi trong đối tượng và thông báo cho dom. Nguy hiểm, vì câu trả lời này đã được viết Oo đã được thảo luận và quyết định chống lại ECMAScript TC, hãy xem xét một polyfill .

Làm thế nào nó hoạt động

  • Trên phần tử, đặt domAttribute:objAttributeánh xạ - ví dụ:bind='textContent:name'
  • Đọc rằng trong hàm dataBind. Quan sát các thay đổi cho cả phần tử và đối tượng.
  • Khi một sự thay đổi xảy ra - cập nhật các yếu tố liên quan.

Giải pháp

Đây là dataBindchức năng, lưu ý rằng nó chỉ có 20 dòng mã và có thể ngắn hơn:

function dataBind(domElement, obj) {    
    var bind = domElement.getAttribute("bind").split(":");
    var domAttr = bind[0].trim(); // the attribute on the DOM element
    var itemAttr = bind[1].trim(); // the attribute the object

    // when the object changes - update the DOM
    Object.observe(obj, function (change) {
        domElement[domAttr] = obj[itemAttr]; 
    });
    // when the dom changes - update the object
    new MutationObserver(updateObj).observe(domElement, { 
        attributes: true,
        childList: true,
        characterData: true
    });
    domElement.addEventListener("keyup", updateObj);
    domElement.addEventListener("click",updateObj);
    function updateObj(){
        obj[itemAttr] = domElement[domAttr];   
    }
    // start the cycle by taking the attribute from the object and updating it.
    domElement[domAttr] = obj[itemAttr]; 
}

Đây là một số cách sử dụng:

HTML:

<div id='projection' bind='textContent:name'></div>
<input type='text' id='textView' bind='value:name' />

JavaScript:

var obj = {
    name: "Benjamin"
};
var el = document.getElementById("textView");
dataBind(el, obj);
var field = document.getElementById("projection");
dataBind(field,obj);

Đây là một fiddle làm việc . Lưu ý rằng giải pháp này là khá chung chung. Object.observe và đột biến quan sát mờ có sẵn.


1
Tôi chỉ tình cờ viết cái này (es5) cho vui, nếu có ai thấy nó hữu ích - hãy tự đánh gục jsfiddle.net/P9rMm
Benjamin Gruenbaum

1
Hãy nhớ rằng khi obj.namecó setter thì không thể quan sát được bên ngoài, nhưng phải phát ra rằng nó đã thay đổi từ bên trong setter - html5rocks.com/en/tutorials/es7/observe/#toc-notifying - loại ném cờ lê trong tác phẩm cho Oo () nếu bạn muốn hành vi phụ thuộc lẫn nhau phức tạp hơn bằng cách sử dụng setters. Hơn nữa, khi obj.namekhông thể định cấu hình, xác định lại trình thiết lập của nó (với nhiều thủ thuật khác nhau để thêm thông báo) cũng không được phép - vì vậy, tổng quát với Oo () hoàn toàn bị loại bỏ trong trường hợp cụ thể đó.
Nolo

8
Object.observe bị xóa khỏi tất cả các trình duyệt: caniuse.com/#feat=object-observe
JvdBerg

1
Một Proxy có thể được sử dụng thay cho Object.observe, hoặc github.com/anywhichway/proxy-observe hoặc gist.github.com/ebidel/1b553d571f924da2da06 hoặc polyfills cũ, cũng trên github @JvdBerg
jimmont

29

Tôi muốn thêm vào phần chuẩn bị của tôi. Tôi đề nghị một cách tiếp cận hơi khác sẽ cho phép bạn chỉ cần gán một giá trị mới cho đối tượng của mình mà không cần sử dụng phương thức. Cần phải lưu ý rằng điều này không được hỗ trợ bởi các trình duyệt đặc biệt cũ và IE9 vẫn yêu cầu sử dụng giao diện khác.

Đáng chú ý nhất là cách tiếp cận của tôi không sử dụng các sự kiện.

Getters và Setters

Đề xuất của tôi sử dụng tính năng tương đối trẻ của getters và setters , đặc biệt là chỉ setters. Nói chung, bộ biến đổi cho phép chúng ta "tùy chỉnh" hành vi về cách các thuộc tính nhất định được gán một giá trị và được truy xuất.

Một triển khai tôi sẽ sử dụng ở đây là phương thức Object.defineProperty . Nó hoạt động trong FireFox, GoogleChrom và - tôi nghĩ - IE9. Không thử nghiệm các trình duyệt khác, nhưng vì đây chỉ là lý thuyết ...

Dù sao, nó chấp nhận ba tham số. Tham số đầu tiên là đối tượng mà bạn muốn xác định thuộc tính mới, chuỗi thứ hai giống với tên của thuộc tính mới và cuối cùng là "đối tượng mô tả" cung cấp thông tin về hành vi của thuộc tính mới.

Hai mô tả đặc biệt thú vị là getset. Một ví dụ sẽ trông giống như sau. Lưu ý rằng việc sử dụng hai điều này cấm sử dụng 4 mô tả khác.

function MyCtor( bindTo ) {
    // I'll omit parameter validation here.

    Object.defineProperty(this, 'value', {
        enumerable: true,
        get : function ( ) {
            return bindTo.value;
        },
        set : function ( val ) {
            bindTo.value = val;
        }
    });
}

Bây giờ sử dụng điều này trở nên hơi khác nhau:

var obj = new MyCtor(document.getElementById('foo')),
    i = 0;
setInterval(function() {
    obj.value += ++i;
}, 3000);

Tôi muốn nhấn mạnh rằng điều này chỉ hoạt động cho các trình duyệt hiện đại.

Fiddle làm việc: http://jsfiddle.net/Derija93/RkTMD/1/


2
Nếu chỉ có chúng tôi có Proxycác đối tượng Harmony :) Setters có vẻ như là một ý tưởng hay, nhưng điều đó có yêu cầu chúng tôi sửa đổi các đối tượng thực tế không? Ngoài ra, trên một lưu ý phụ - Object.createcó thể được sử dụng ở đây (một lần nữa, giả sử trình duyệt hiện đại cho phép tham số thứ hai). Ngoài ra, setter / getter có thể được sử dụng để 'chiếu' một giá trị khác cho đối tượng và thành phần DOM :). Tôi tự hỏi nếu bạn có bất kỳ hiểu biết nào về templating quá, đó có vẻ như là một thách thức thực sự ở đây, đặc biệt là để cấu trúc độc đáo :)
Benjamin Gruenbaum

Giống như người quản lý của tôi, tôi cũng không làm việc nhiều với các công cụ tạo khuôn mẫu phía máy khách, xin lỗi. :( Nhưng ý của bạn là gì khi sửa đổi các đối tượng thực tế ? Và tôi muốn hiểu suy nghĩ của bạn về cách bạn hiểu rằng setter / getter có thể được sử dụng để .... Các getters / setters ở đây không được sử dụng nhưng chuyển hướng tất cả đầu vào và truy xuất từ ​​đối tượng sang thành phần DOM, về cơ bản giống như Proxy, như bạn đã nói.) Tôi hiểu thách thức là giữ cho hai thuộc tính riêng biệt được đồng bộ hóa. Phương pháp của tôi loại bỏ một trong cả hai.
Kiruse

A Proxysẽ loại bỏ nhu cầu sử dụng getters / setters, bạn có thể liên kết các phần tử mà không cần biết chúng có thuộc tính gì. Ý tôi là, các getters có thể thay đổi nhiều hơn bindTo.value chúng có thể chứa logic (và thậm chí có thể là một mẫu). Câu hỏi là làm thế nào để duy trì loại ràng buộc hai chiều này với một mẫu trong tâm trí? Hãy nói rằng tôi đang ánh xạ đối tượng của mình thành một biểu mẫu, tôi muốn duy trì cả phần tử và biểu mẫu được đồng bộ hóa và tôi tự hỏi làm thế nào tôi tiếp tục về loại điều đó. Bạn có thể kiểm tra cách nó hoạt động trên knockout learn.knockoutjs.com/#/?tutorial=intro chẳng hạn
Benjamin Gruenbaum

@BenjaminGruenbaum Gotcha. Tôi sẽ cho nó một cái nhìn.
Kiruse

@BenjaminGruenbaum Tôi thấy những gì bạn đang cố gắng hiểu. Thiết lập tất cả điều này với các mẫu trong tâm trí hóa ra khó khăn hơn một chút. Tôi sẽ làm việc với kịch bản này trong một thời gian (và liên tục khởi động lại nó). Nhưng hiện tại, tôi đang nghỉ ngơi. Tôi thực sự không có thời gian cho việc này.
Virus

7

Tôi nghĩ rằng câu trả lời của tôi sẽ mang tính kỹ thuật hơn, nhưng không khác biệt khi những người khác trình bày cùng một điều bằng cách sử dụng các kỹ thuật khác nhau.
Vì vậy, điều đầu tiên, giải pháp cho vấn đề này là sử dụng một mẫu thiết kế được gọi là "người quan sát", nó cho phép bạn tách dữ liệu khỏi bản trình bày của mình, làm cho sự thay đổi trong một điều được phát ra cho người nghe của họ, nhưng trong trường hợp này nó được thực hiện hai chiều.

Đối với cách DOM đến JS

Để liên kết dữ liệu từ DOM với đối tượng js, bạn có thể thêm đánh dấu ở dạng datathuộc tính (hoặc các lớp nếu bạn cần tương thích), như sau:

<input type="text" data-object="a" data-property="b" id="b" class="bind" value=""/>
<input type="text" data-object="a" data-property="c" id="c" class="bind" value=""/>
<input type="text" data-object="d" data-property="e" id="e" class="bind" value=""/>

Bằng cách này, nó có thể được truy cập thông qua js bằng cách sử dụng querySelectorAll(hoặc người bạn cũ getElementsByClassNameđể tương thích).

Bây giờ bạn có thể liên kết sự kiện lắng nghe các thay đổi theo các cách: một người nghe cho mỗi đối tượng hoặc một người nghe lớn vào vùng chứa / tài liệu. Liên kết với tài liệu / bộ chứa sẽ kích hoạt sự kiện cho mọi thay đổi được thực hiện trong đó hoặc nó là con, nó sẽ có dấu chân bộ nhớ nhỏ hơn nhưng sẽ sinh ra các cuộc gọi sự kiện.
Mã sẽ trông giống như thế này:

//Bind to each element
var elements = document.querySelectorAll('input[data-property]');

function toJS(){
    //Assuming `a` is in scope of the document
    var obj = document[this.data.object];
    obj[this.data.property] = this.value;
}

elements.forEach(function(el){
    el.addEventListener('change', toJS, false);
}

//Bind to document
function toJS2(){
    if (this.data && this.data.object) {
        //Again, assuming `a` is in document's scope
        var obj = document[this.data.object];
        obj[this.data.property] = this.value;
    }
}

document.addEventListener('change', toJS2, false);

Đối với JS, hãy thực hiện DOM

Bạn sẽ cần hai thứ: một siêu đối tượng sẽ chứa các tham chiếu của phần tử DOM phù thủy được liên kết với từng đối tượng / thuộc tính js và cách lắng nghe các thay đổi trong các đối tượng. Về cơ bản nó cũng giống như vậy: bạn phải có cách lắng nghe những thay đổi trong đối tượng và sau đó liên kết nó với nút DOM, vì đối tượng của bạn "không thể có" siêu dữ liệu, bạn sẽ cần một đối tượng khác giữ siêu dữ liệu theo cách rằng tên thuộc tính ánh xạ tới các thuộc tính của đối tượng siêu dữ liệu. Mã sẽ giống như thế này:

var a = {
        b: 'foo',
        c: 'bar'
    },
    d = {
        e: 'baz'
    },
    metadata = {
        b: 'b',
        c: 'c',
        e: 'e'
    };
function toDOM(changes){
    //changes is an array of objects changed and what happened
    //for now i'd recommend a polyfill as this syntax is still a proposal
    changes.forEach(function(change){
        var element = document.getElementById(metadata[change.name]);
        element.value = change.object[change.name];
    });
}
//Side note: you can also use currying to fix the second argument of the function (the toDOM method)
Object.observe(a, toDOM);
Object.observe(d, toDOM);

Tôi hy vọng rằng tôi đã được giúp đỡ.


không có vấn đề so sánh với việc sử dụng .observer?
Mohsen Shakiba

bây giờ nó cần một shim hoặc polyfill để Object.observehỗ trợ chỉ hiện diện trong chrome. caniuse.com/#feat=object-observe
madcampos

9
Object.observe đã chết. Chỉ cần nghĩ rằng tôi lưu ý rằng ở đây.
Benjamin Gruenbaum

@BenjaminGruenbaum Điều gì là chính xác để sử dụng bây giờ, vì điều này đã chết?
johnny

1
@johnny nếu tôi không sai thì đó sẽ là bẫy proxy vì chúng cho phép kiểm soát chi tiết hơn những gì tôi có thể làm với một đối tượng, nhưng tôi phải điều tra điều đó.
madcampos

7

Hôm qua, tôi bắt đầu viết theo cách riêng của mình để ràng buộc dữ liệu.

Chơi với nó rất buồn cười.

Tôi nghĩ nó đẹp và rất hữu ích. Ít nhất trong các thử nghiệm của tôi sử dụng firefox và chrome, Edge cũng phải hoạt động. Không chắc chắn về người khác, nhưng nếu họ hỗ trợ Proxy, tôi nghĩ nó sẽ hoạt động.

https://jsfiddle.net/2ozoovne/1/

<H1>Bind Context 1</H1>
<input id='a' data-bind='data.test' placeholder='Button Text' />
<input id='b' data-bind='data.test' placeholder='Button Text' />
<input type=button id='c' data-bind='data.test' />
<H1>Bind Context 2</H1>
<input id='d' data-bind='data.otherTest' placeholder='input bind' />
<input id='e' data-bind='data.otherTest' placeholder='input bind' />
<input id='f' data-bind='data.test' placeholder='button 2 text - same var name, other context' />
<input type=button id='g' data-bind='data.test' value='click here!' />
<H1>No bind data</H1>
<input id='h' placeholder='not bound' />
<input id='i' placeholder='not bound'/>
<input type=button id='j' />

Đây là mã:

(function(){
    if ( ! ( 'SmartBind' in window ) ) { // never run more than once
        // This hack sets a "proxy" property for HTMLInputElement.value set property
        var nativeHTMLInputElementValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
        var newDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
        newDescriptor.set=function( value ){
            if ( 'settingDomBind' in this )
                return;
            var hasDataBind=this.hasAttribute('data-bind');
            if ( hasDataBind ) {
                this.settingDomBind=true;
                var dataBind=this.getAttribute('data-bind');
                if ( ! this.hasAttribute('data-bind-context-id') ) {
                    console.error("Impossible to recover data-bind-context-id attribute", this, dataBind );
                } else {
                    var bindContextId=this.getAttribute('data-bind-context-id');
                    if ( bindContextId in SmartBind.contexts ) {
                        var bindContext=SmartBind.contexts[bindContextId];
                        var dataTarget=SmartBind.getDataTarget(bindContext, dataBind);
                        SmartBind.setDataValue( dataTarget, value);
                    } else {
                        console.error( "Invalid data-bind-context-id attribute", this, dataBind, bindContextId );
                    }
                }
                delete this.settingDomBind;
            }
            nativeHTMLInputElementValue.set.bind(this)( value );
        }
        Object.defineProperty(HTMLInputElement.prototype, 'value', newDescriptor);

    var uid= function(){
           return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
               var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
               return v.toString(16);
          });
   }

        // SmartBind Functions
        window.SmartBind={};
        SmartBind.BindContext=function(){
            var _data={};
            var ctx = {
                "id" : uid()    /* Data Bind Context Id */
                , "_data": _data        /* Real data object */
                , "mapDom": {}          /* DOM Mapped objects */
                , "mapDataTarget": {}       /* Data Mapped objects */
            }
            SmartBind.contexts[ctx.id]=ctx;
            ctx.data=new Proxy( _data, SmartBind.getProxyHandler(ctx, "data"))  /* Proxy object to _data */
            return ctx;
        }

        SmartBind.getDataTarget=function(bindContext, bindPath){
            var bindedObject=
                { bindContext: bindContext
                , bindPath: bindPath 
                };
            var dataObj=bindContext;
            var dataObjLevels=bindPath.split('.');
            for( var i=0; i<dataObjLevels.length; i++ ) {
                if ( i == dataObjLevels.length-1 ) { // last level, set value
                    bindedObject={ target: dataObj
                    , item: dataObjLevels[i]
                    }
                } else {    // digg in
                    if ( ! ( dataObjLevels[i] in dataObj ) ) {
                        console.warn("Impossible to get data target object to map bind.", bindPath, bindContext);
                        break;
                    }
                    dataObj=dataObj[dataObjLevels[i]];
                }
            }
            return bindedObject ;
        }

        SmartBind.contexts={};
        SmartBind.add=function(bindContext, domObj){
            if ( typeof domObj == "undefined" ){
                console.error("No DOM Object argument given ", bindContext);
                return;
            }
            if ( ! domObj.hasAttribute('data-bind') ) {
                console.warn("Object has no data-bind attribute", domObj);
                return;
            }
            domObj.setAttribute("data-bind-context-id", bindContext.id);
            var bindPath=domObj.getAttribute('data-bind');
            if ( bindPath in bindContext.mapDom ) {
                bindContext.mapDom[bindPath][bindContext.mapDom[bindPath].length]=domObj;
            } else {
                bindContext.mapDom[bindPath]=[domObj];
            }
            var bindTarget=SmartBind.getDataTarget(bindContext, bindPath);
            bindContext.mapDataTarget[bindPath]=bindTarget;
            domObj.addEventListener('input', function(){ SmartBind.setDataValue(bindTarget,this.value); } );
            domObj.addEventListener('change', function(){ SmartBind.setDataValue(bindTarget, this.value); } );
        }

        SmartBind.setDataValue=function(bindTarget,value){
            if ( ! ( 'target' in bindTarget ) ) {
                var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
                if ( 'target' in lBindTarget ) {
                    bindTarget.target=lBindTarget.target;
                    bindTarget.item=lBindTarget.item;
                } else {
                    console.warn("Still can't recover the object to bind", bindTarget.bindPath );
                }
            }
            if ( ( 'target' in bindTarget ) ) {
                bindTarget.target[bindTarget.item]=value;
            }
        }
        SmartBind.getDataValue=function(bindTarget){
            if ( ! ( 'target' in bindTarget ) ) {
                var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
                if ( 'target' in lBindTarget ) {
                    bindTarget.target=lBindTarget.target;
                    bindTarget.item=lBindTarget.item;
                } else {
                    console.warn("Still can't recover the object to bind", bindTarget.bindPath );
                }
            }
            if ( ( 'target' in bindTarget ) ) {
                return bindTarget.target[bindTarget.item];
            }
        }
        SmartBind.getProxyHandler=function(bindContext, bindPath){
            return  {
                get: function(target, name){
                    if ( name == '__isProxy' )
                        return true;
                    // just get the value
                    // console.debug("proxy get", bindPath, name, target[name]);
                    return target[name];
                }
                ,
                set: function(target, name, value){
                    target[name]=value;
                    bindContext.mapDataTarget[bindPath+"."+name]=value;
                    SmartBind.processBindToDom(bindContext, bindPath+"."+name);
                    // console.debug("proxy set", bindPath, name, target[name], value );
                    // and set all related objects with this target.name
                    if ( value instanceof Object) {
                        if ( !( name in target) || ! ( target[name].__isProxy ) ){
                            target[name]=new Proxy(value, SmartBind.getProxyHandler(bindContext, bindPath+'.'+name));
                        }
                        // run all tree to set proxies when necessary
                        var objKeys=Object.keys(value);
                        // console.debug("...objkeys",objKeys);
                        for ( var i=0; i<objKeys.length; i++ ) {
                            bindContext.mapDataTarget[bindPath+"."+name+"."+objKeys[i]]=target[name][objKeys[i]];
                            if ( typeof value[objKeys[i]] == 'undefined' || value[objKeys[i]] == null || ! ( value[objKeys[i]] instanceof Object ) || value[objKeys[i]].__isProxy )
                                continue;
                            target[name][objKeys[i]]=new Proxy( value[objKeys[i]], SmartBind.getProxyHandler(bindContext, bindPath+'.'+name+"."+objKeys[i]));
                        }
                        // TODO it can be faster than run all items
                        var bindKeys=Object.keys(bindContext.mapDom);
                        for ( var i=0; i<bindKeys.length; i++ ) {
                            // console.log("test...", bindKeys[i], " for ", bindPath+"."+name);
                            if ( bindKeys[i].startsWith(bindPath+"."+name) ) {
                                // console.log("its ok, lets update dom...", bindKeys[i]);
                                SmartBind.processBindToDom( bindContext, bindKeys[i] );
                            }
                        }
                    }
                    return true;
                }
            };
        }
        SmartBind.processBindToDom=function(bindContext, bindPath) {
            var domList=bindContext.mapDom[bindPath];
            if ( typeof domList != 'undefined' ) {
                try {
                    for ( var i=0; i < domList.length ; i++){
                        var dataTarget=SmartBind.getDataTarget(bindContext, bindPath);
                        if ( 'target' in dataTarget )
                            domList[i].value=dataTarget.target[dataTarget.item];
                        else
                            console.warn("Could not get data target", bindContext, bindPath);
                    }
                } catch (e){
                    console.warn("bind fail", bindPath, bindContext, e);
                }
            }
        }
    }
})();

Sau đó, để thiết lập, chỉ cần:

var bindContext=SmartBind.BindContext();
SmartBind.add(bindContext, document.getElementById('a'));
SmartBind.add(bindContext, document.getElementById('b'));
SmartBind.add(bindContext, document.getElementById('c'));

var bindContext2=SmartBind.BindContext();
SmartBind.add(bindContext2, document.getElementById('d'));
SmartBind.add(bindContext2, document.getElementById('e'));
SmartBind.add(bindContext2, document.getElementById('f'));
SmartBind.add(bindContext2, document.getElementById('g'));

setTimeout( function() {
    document.getElementById('b').value='Via Script works too!'
}, 2000);

document.getElementById('g').addEventListener('click',function(){
bindContext2.data.test='Set by js value'
})

Hiện tại, tôi vừa thêm liên kết giá trị HTMLInputE bổ sung.

Hãy cho tôi biết nếu bạn biết cách cải thiện nó.


6

Có một triển khai barebones rất đơn giản về liên kết dữ liệu 2 chiều trong liên kết này " dữ liệu hai chiều dễ dàng trong JavaScript"

Liên kết trước đó cùng với các ý tưởng từ knockoutjs, backbone.js và agility.js, đã dẫn đến khung MVVM nhẹ và nhanh này, ModelView.js dựa trên jQuery chơi độc đáo với jQuery và trong đó tôi là tác giả khiêm tốn (hoặc có thể không khiêm tốn).

Sao chép mã mẫu bên dưới (từ liên kết bài đăng trên blog ):

Mã mẫu cho DataBinder

function DataBinder( object_id ) {
  // Use a jQuery object as simple PubSub
  var pubSub = jQuery({});

  // We expect a `data` element specifying the binding
  // in the form: data-bind-<object_id>="<property_name>"
  var data_attr = "bind-" + object_id,
      message = object_id + ":change";

  // Listen to change events on elements with the data-binding attribute and proxy
  // them to the PubSub, so that the change is "broadcasted" to all connected objects
  jQuery( document ).on( "change", "[data-" + data_attr + "]", function( evt ) {
    var $input = jQuery( this );

    pubSub.trigger( message, [ $input.data( data_attr ), $input.val() ] );
  });

  // PubSub propagates changes to all bound elements, setting value of
  // input tags or HTML content of other tags
  pubSub.on( message, function( evt, prop_name, new_val ) {
    jQuery( "[data-" + data_attr + "=" + prop_name + "]" ).each( function() {
      var $bound = jQuery( this );

      if ( $bound.is("input, textarea, select") ) {
        $bound.val( new_val );
      } else {
        $bound.html( new_val );
      }
    });
  });

  return pubSub;
}

Đối với những gì liên quan đến đối tượng JavaScript, việc triển khai tối thiểu mô hình Người dùng vì lợi ích của thử nghiệm này có thể là như sau:

function User( uid ) {
  var binder = new DataBinder( uid ),

      user = {
        attributes: {},

        // The attribute setter publish changes using the DataBinder PubSub
        set: function( attr_name, val ) {
          this.attributes[ attr_name ] = val;
          binder.trigger( uid + ":change", [ attr_name, val, this ] );
        },

        get: function( attr_name ) {
          return this.attributes[ attr_name ];
        },

        _binder: binder
      };

  // Subscribe to the PubSub
  binder.on( uid + ":change", function( evt, attr_name, new_val, initiator ) {
    if ( initiator !== user ) {
      user.set( attr_name, new_val );
    }
  });

  return user;
}

Bây giờ, bất cứ khi nào chúng ta muốn liên kết thuộc tính của một mô hình với một phần UI, chúng ta chỉ cần đặt một thuộc tính dữ liệu phù hợp trên thành phần HTML tương ứng:

// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );

<!-- html -->
<input type="number" data-bind-123="name" />

Mặc dù liên kết này có thể trả lời câu hỏi, tốt hơn là bao gồm các phần thiết yếu của câu trả lời ở đây và cung cấp liên kết để tham khảo. Câu trả lời chỉ liên kết có thể trở nên không hợp lệ nếu trang được liên kết thay đổi.
Sam Hanley

@sphanley, lưu ý, tôi có thể sẽ cập nhật khi tôi có nhiều thời gian hơn, vì nó là một mã khá dài cho một bài trả lời
Nikos M.

@sphanley, sao chép mã mẫu theo câu trả lời từ liên kết được tham chiếu (mặc dù tôi làm điều này tạo ra nội dung trùng lặp hầu hết thời gian, dù sao đi nữa)
Nikos M.

1
Nó chắc chắn tạo ra nội dung trùng lặp, nhưng đó là điểm chính - các liên kết blog thường có thể bị phá vỡ theo thời gian và bằng cách sao chép nội dung có liên quan ở đây, nó đảm bảo rằng nó sẽ có sẵn và hữu ích cho người đọc trong tương lai. Câu trả lời có vẻ tuyệt vời bây giờ!
Sam Hanley

3

Thay đổi giá trị của một phần tử có thể kích hoạt sự kiện DOM . Trình nghe phản hồi các sự kiện có thể được sử dụng để thực hiện liên kết dữ liệu trong JavaScript.

Ví dụ:

function bindValues(id1, id2) {
  const e1 = document.getElementById(id1);
  const e2 = document.getElementById(id2);
  e1.addEventListener('input', function(event) {
    e2.value = event.target.value;
  });
  e2.addEventListener('input', function(event) {
    e1.value = event.target.value;
  });
}

Đây là mã và bản demo cho thấy các phần tử DOM có thể được liên kết với nhau hoặc với một đối tượng JavaScript.


3

Ràng buộc bất kỳ đầu vào html nào

<input id="element-to-bind" type="text">

định nghĩa hai hàm:

function bindValue(objectToBind) {
var elemToBind = document.getElementById(objectToBind.id)    
elemToBind.addEventListener("change", function() {
    objectToBind.value = this.value;
})
}

function proxify(id) { 
var handler = {
    set: function(target, key, value, receiver) {
        target[key] = value;
        document.getElementById(target.id).value = value;
        return Reflect.set(target, key, value);
    },
}
return new Proxy({id: id}, handler);
}

sử dụng các chức năng:

var myObject = proxify('element-to-bind')
bindValue(myObject);

3

Đây là một ý tưởng sử dụng Object.definePropertytrực tiếp sửa đổi cách truy cập một thuộc tính.

Mã số:

function bind(base, el, varname) {
    Object.defineProperty(base, varname, {
        get: () => {
            return el.value;
        },
        set: (value) => {
            el.value = value;
        }
    })
}

Sử dụng:

var p = new some_class();
bind(p,document.getElementById("someID"),'variable');

p.variable="yes"

fiddle: đây


2

Tôi đã trải qua một số ví dụ javascript cơ bản bằng cách sử dụng trình xử lý sự kiện onkeypress và onchange để tạo chế độ xem ràng buộc với js và js của chúng tôi để xem

Ở đây ví dụ plunker http://plnkr.co/edit/7hSOIFRTvqLAvdZT4Bcc?p=preview

<!DOCTYPE html>
<html>
<body>

    <p>Two way binding data.</p>

    <p>Binding data from  view to JS</p>

    <input type="text" onkeypress="myFunction()" id="myinput">
    <p id="myid"></p>
    <p>Binding data from  js to view</p>
    <input type="text" id="myid2" onkeypress="myFunction1()" oninput="myFunction1()">
    <p id="myid3" onkeypress="myFunction1()" id="myinput" oninput="myFunction1()"></p>

    <script>

        document.getElementById('myid2').value="myvalue from script";
        document.getElementById('myid3').innerHTML="myvalue from script";
        function myFunction() {
            document.getElementById('myid').innerHTML=document.getElementById('myinput').value;
        }
        document.getElementById("myinput").onchange=function(){

            myFunction();

        }
        document.getElementById("myinput").oninput=function(){

            myFunction();

        }

        function myFunction1() {

            document.getElementById('myid3').innerHTML=document.getElementById('myid2').value;
        }
    </script>

</body>
</html>

2
<!DOCTYPE html>
<html>
<head>
    <title>Test</title>
</head>
<body>

<input type="text" id="demo" name="">
<p id="view"></p>
<script type="text/javascript">
    var id = document.getElementById('demo');
    var view = document.getElementById('view');
    id.addEventListener('input', function(evt){
        view.innerHTML = this.value;
    });

</script>
</body>
</html>

2

Một cách đơn giản để liên kết một biến với đầu vào (liên kết hai chiều) là chỉ cần truy cập trực tiếp vào phần tử đầu vào trong getter và setter:

var variable = function(element){                    
                   return {
                       get : function () { return element.value;},
                       set : function (value) { element.value = value;} 
                   }
               };

Trong HTML:

<input id="an-input" />
<input id="another-input" />

Và sử dụng:

var myVar = new variable(document.getElementById("an-input"));
myVar.set(10);

// and another example:
var myVar2 = new variable(document.getElementById("another-input"));
myVar.set(myVar2.get());


Một cách dễ dàng hơn để làm điều trên mà không có getter / setter:

var variable = function(element){

                return function () {
                    if(arguments.length > 0)                        
                        element.value = arguments[0];                                           

                    else return element.value;                                                  
                }

        }

Để sử dụng:

var v1 = new variable(document.getElementById("an-input"));
v1(10); // sets value to 20.
console.log(v1()); // reads value.

1

Nó rất đơn giản ràng buộc dữ liệu hai chiều trong vanilla javascript ....

<input type="text" id="inp" onkeyup="document.getElementById('name').innerHTML=document.getElementById('inp').value;">

<div id="name">

</div>


2
chắc chắn điều này sẽ chỉ làm việc với sự kiện onkeyup? tức là nếu bạn đã thực hiện một yêu cầu ajax và sau đó thay đổi InternalHTML thông qua JavaScript thì điều này sẽ không hoạt động
Zach Smith

1

Đến bữa tiệc muộn, đặc biệt là vì tôi đã viết 2 lib liên quan đến tháng / năm trước, tôi sẽ đề cập đến chúng sau, nhưng vẫn có vẻ phù hợp với tôi. Để làm cho nó thực sự ngắn spoiler, các công nghệ mà tôi chọn là:

  • Proxy để quan sát mô hình
  • MutationObserver cho các thay đổi theo dõi của DOM (vì lý do ràng buộc, không phải thay đổi giá trị)
  • thay đổi giá trị (xem luồng mô hình) được xử lý thông qua addEventListener trình xử lý

IMHO, ngoài OP, điều quan trọng là việc triển khai ràng buộc dữ liệu sẽ:

  • xử lý các trường hợp vòng đời ứng dụng khác nhau (trước tiên là HTML, sau đó là JS, trước tiên là HTML, thay đổi thuộc tính động, v.v.)
  • cho phép liên kết sâu của mô hình, để người ta có thể liên kết user.address.block
  • mảng như một mô hình nên được hỗ trợ chính xác ( shift,splice và như nhau)
  • xử lý ShadowDOM
  • cố gắng dễ dàng thay thế công nghệ nhất có thể, do đó, bất kỳ ngôn ngữ phụ tạo khuôn mẫu nào cũng là một cách tiếp cận thân thiện với những thay đổi không tương lai vì nó được kết hợp quá nhiều với khung

Theo tất cả những điều đó, theo ý kiến ​​của tôi làm cho không thể chỉ ném vài chục dòng JS. Tôi đã cố gắng làm điều đó như một mô hình chứ không phải lib - không hiệu quả với tôi.

Tiếp theo, có Object.observe xóa bỏ và được đưa ra rằng việc quan sát mô hình là một phần rất quan trọng - toàn bộ phần này PHẢI được phân tách mối quan tâm với một lib khác. Bây giờ đến thời điểm hiệu trưởng về cách tôi xử lý vấn đề này - chính xác như OP đã hỏi:

Mô hình (phần JS)

Tôi quan sát mô hình là Proxy , đó là cách duy nhất để làm cho nó hoạt động, IMHO. Đầy đủ tính năng observerxứng đáng là thư viện riêng của nó, vì vậy tôi đã phát triểnobject-observer thư viện cho mục đích duy nhất đó.

Các mô hình nên được đăng ký thông qua một số API chuyên dụng, đó là điểm mà POJO biến thành Observable s, không thể thấy bất kỳ lối tắt nào ở đây. Các phần tử DOM được coi là một khung nhìn ràng buộc (xem bên dưới), được cập nhật với các giá trị của mô hình / s sau đó sau mỗi lần thay đổi dữ liệu.

Lượt xem (phần HTML)

IMHO, cách sạch nhất để thể hiện ràng buộc, là thông qua các thuộc tính. Nhiều người đã làm điều này trước đây và nhiều người sẽ làm sau đó, vì vậy không có tin tức nào ở đây, đây chỉ là một cách đúng đắn để làm điều đó. Trong trường hợp của tôi, tôi đã sử dụng cú pháp sau: <span data-tie="modelKey:path.to.data => targerProperty"></span>nhưng điều này ít quan trọng hơn. Có gì quan trọng với tôi, không có cú pháp kịch bản phức tạp trong HTML - điều này là sai, một lần nữa, IMHO.

Tất cả các yếu tố được chỉ định là một quan điểm ràng buộc sẽ được thu thập lúc đầu. Tôi không thể tránh khỏi một khía cạnh hiệu suất để quản lý một số ánh xạ nội bộ giữa các mô hình và các khung nhìn, có vẻ như là một trường hợp đúng trong đó bộ nhớ + một số quản lý nên được hy sinh để lưu các bản cập nhật và cập nhật thời gian chạy.

Các khung nhìn được cập nhật lúc đầu từ mô hình, nếu có sẵn và sau đó thay đổi mô hình, như chúng tôi đã nói. Hơn nữa, toàn bộ DOM phải được quan sát bằng các phương tiện MutationObserverđể phản ứng (liên kết / hủy liên kết) trên các phần tử được thêm / xóa / thay đổi động. Hơn nữa, tất cả điều này, nên được sao chép vào ShadowDOM (tất nhiên là mở một) để không để lại các lỗ đen không liên kết.

Danh sách các chi tiết cụ thể có thể thực sự đi xa hơn, nhưng theo tôi, các nguyên tắc chính sẽ khiến việc ràng buộc dữ liệu được thực hiện với sự cân bằng tốt về tính đầy đủ của tính năng từ một bên và sự đơn giản lành mạnh từ phía bên kia.

Và do đó, ngoài các object-observerđề cập ở trên, tôi thực sự cũng đã viết data-tierthư viện, thực hiện ràng buộc dữ liệu dọc theo các khái niệm đã đề cập ở trên.


0

Mọi thứ đã thay đổi rất nhiều trong 7 năm qua, chúng tôi có các thành phần web gốc trong hầu hết các trình duyệt hiện nay. IMO cốt lõi của vấn đề là chia sẻ trạng thái giữa các yếu tố, một khi bạn có tầm thường để cập nhật ui khi trạng thái thay đổi và ngược lại.

Để chia sẻ dữ liệu giữa các thành phần, bạn có thể tạo lớp StateObserver và mở rộng các thành phần web của mình từ đó. Một triển khai tối thiểu trông giống như thế này:

// create a base class to handle state
class StateObserver extends HTMLElement {
	constructor () {
  	super()
    StateObserver.instances.push(this)
  }
	stateUpdate (update) {
  	StateObserver.lastState = StateObserver.state
    StateObserver.state = update
    StateObserver.instances.forEach((i) => {
    	if (!i.onStateUpdate) return
    	i.onStateUpdate(update, StateObserver.lastState)
    })
  }
}

StateObserver.instances = []
StateObserver.state = {}
StateObserver.lastState = {}

// create a web component which will react to state changes
class CustomReactive extends StateObserver {
	onStateUpdate (state, lastState) {
  	if (state.someProp === lastState.someProp) return
    this.innerHTML = `input is: ${state.someProp}`
  }
}
customElements.define('custom-reactive', CustomReactive)

class CustomObserved extends StateObserver {
	connectedCallback () {
  	this.querySelector('input').addEventListener('input', (e) => {
    	this.stateUpdate({ someProp: e.target.value })
    })
  }
}
customElements.define('custom-observed', CustomObserved)
<custom-observed>
  <input>
</custom-observed>
<br />
<custom-reactive></custom-reactive>

câu đố ở đây

Tôi thích cách tiếp cận này vì:

  • không có dom traversal để tìm data-tài sản
  • không có Object.observe (không dùng nữa)
  • không có Proxy (cung cấp một hook nhưng không có cơ chế giao tiếp nào)
  • không phụ thuộc, (ngoài polyfill tùy thuộc vào trình duyệt mục tiêu của bạn)
  • đó là trạng thái tập trung và mô đun hợp lý ... mô tả trạng thái trong html và việc người nghe ở khắp mọi nơi sẽ trở nên lộn xộn rất nhanh.
  • nó có thể mở rộng. Việc triển khai cơ bản này là 20 dòng mã, nhưng bạn có thể dễ dàng xây dựng một số tiện ích, bất biến và ma thuật hình dạng trạng thái để làm cho nó dễ dàng hơn để làm việc với.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.