JavaScript에서 DOM 데이터 바인딩을 구현하는 방법


244

이 질문을 엄격하게 교육적인 것으로 취급하십시오. 나는 아직도 이것을 구현하기위한 새로운 답변과 아이디어를 듣는 데 관심이 있습니다.

tl; dr

JavaScript로 양방향 데이터 바인딩을 어떻게 구현합니까?

DOM에 데이터 바인딩

DOM에 데이터를 바인딩한다는 a것은 속성 이있는 JavaScript 객체 를 갖는 것을 의미 합니다 b. 그런 다음 <input>DOM 요소 (예 :)가 있으면 DOM 요소가 변경되고 a변경되거나 그 반대가됩니다 (즉, 양방향 데이터 바인딩을 의미합니다).

AngularJS의 다이어그램은 다음과 같습니다.

양방향 데이터 바인딩

그래서 기본적으로 JavaScript와 비슷한 것이 있습니다 :

var a = {b:3};

그런 다음 입력 (또는 다른 양식) 요소는 다음과 같습니다.

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

입력 값을 a.b(예를 들어) 값으로하고 싶습니다. 입력 텍스트가 변경되면 변경하고 싶습니다 a.b. 때 a.b자바 스크립트의 변화, 입력이 변경됩니다.

질문

평범한 JavaScript에서 이것을 달성하는 몇 가지 기본 기술은 무엇입니까?

특히, 나는 다음을 언급하는 좋은 대답을 원합니다.

  • 객체에 대한 바인딩은 어떻게 작동합니까?
  • 양식의 변화를 듣는 것이 어떻게 효과가 있을까요?
  • 템플릿 수준에서 HTML 만 수정하는 것이 간단한 방법으로 가능합니까? HTML 문서 자체의 바인딩을 추적하지 않고 JavaScript (DOM 이벤트 및 JavaScript가 사용되는 DOM 요소 참조)에서만 바인딩을 추적하고 싶습니다.

내가 무엇을 시도 했습니까?

나는 Mustache의 열렬한 팬이므로 템플릿 사용에 사용해 보았습니다. 그러나 Mustache가 HTML을 문자열로 처리하기 때문에 데이터 바인딩 자체를 수행하려고 할 때 문제가 발생하여 결과를 얻은 후 뷰 모델의 객체 위치에 대한 참조가 없습니다. 내가 생각할 수있는 유일한 해결 방법은 HTML 문자열 (또는 생성 된 DOM 트리) 자체를 속성으로 수정하는 것입니다. 다른 템플릿 엔진을 사용하는 것은 마음에 들지 않습니다.

기본적으로 나는 당면한 문제를 복잡하게 만들고 있다는 간단한 느낌을 받았으며 간단한 해결책이 있습니다.

참고 : 외부 라이브러리, 특히 수천 줄의 코드를 사용하는 답변은 제공하지 마십시오. AngularJS와 KnockoutJS를 사용했습니다. 'use framework x'형식의 답변을 정말로 원하지 않습니다. 최적의 방법으로, 양방향 데이터 바인딩 자체를 구현하는 방법을 파악하기 위해 많은 프레임 워크를 사용하는 방법을 모르는 미래의 독자가 필요합니다. 나는 완전한 대답을 기대하지는 않지만 아이디어를 얻습니다.


2
나는 CrazyGlue 를 Benjamin Gruenbaum의 디자인에 기반을 두었 습니다. 또한 SELECT, 확인란 및 라디오 태그를 지원합니다. jQuery는 종속성입니다.
JohnSz

12
이 질문은 정말 대단합니다. 주제를 벗어난 또는 다른 어리석은 말이되지 않아 문을 닫는다면 나는 진지하게 떨릴 것입니다.
OCDev December

CrazyGlue 프로젝트를 언급 해 주셔서 감사합니다. 간단한 양방향 데이터 바인더를 오랫동안 찾고 있습니다. Object.observe를 사용하지 않는 것처럼 보이므로 브라우저 지원이 우수해야합니다. 그리고 당신은 콧수염 템플릿을 사용하지 않으므로 완벽합니다.
Gavin

@ Benjamin 무엇을하셨습니까?
johnny

내 의견으로는 @johnny 올바른 접근법은 JS에서 DOM (React와 같은)을 만드는 것이지 그 반대는 아닙니다. 나는 그것이 결국 우리가 할 것이라고 생각합니다.
Benjamin Gruenbaum

답변:


106
  • 객체에 대한 바인딩은 어떻게 작동합니까?
  • 양식의 변화를 듣는 것이 어떻게 효과가 있을까요?

두 객체를 모두 업데이트하는 추상화

다른 기술이 있다고 가정하지만 궁극적으로 관련 DOM 요소에 대한 참조를 보유하고 자체 데이터 및 관련 요소에 대한 업데이트를 조정하는 인터페이스를 제공하는 객체가 있습니다.

.addEventListener()이것에 대한 아주 좋은 인터페이스를 제공합니다. eventListener인터페이스 를 구현하는 객체를 제공 할 수 있으며 해당 객체를 this값 으로 사용하여 처리기를 호출 합니다.

그러면 요소와 관련 데이터 모두에 자동으로 액세스 할 수 있습니다.

객체 정의

물론 프로토 타입 상속은 이것을 구현하는 좋은 방법이지만 물론 필요하지는 않습니다. 먼저 요소와 일부 초기 데이터를받는 생성자를 만듭니다.

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

따라서 생성자는 새 객체의 속성에 요소와 데이터를 저장합니다. 또한 change이벤트를 주어진 이벤트에 바인딩합니다 element. 흥미로운 점은 함수 대신 새 객체를 두 번째 인수로 전달한다는 것입니다. 그러나 이것만으로는 작동하지 않습니다.

eventListener인터페이스 구현

이 작업을 수행하려면 객체가 eventListener인터페이스 를 구현해야합니다 . 이를 위해 필요한 것은 객체에 handleEvent()메소드 를 제공하는 것입니다.

그것이 상속이 들어오는 곳입니다.

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;
};

이것을 구성하는 방법에는 여러 가지가 있지만 업데이트 조정의 예에서는 change()메서드가 값만 허용 handleEvent하고 이벤트 객체 대신 해당 값을 전달 하기로 결정했습니다 . 이렇게 change()하면 이벤트 없이도 호출 할 수 있습니다.

이제 change이벤트가 발생하면 요소와 .data속성이 모두 업데이트 됩니다. .change()JavaScript 프로그램 을 호출 할 때도 마찬가지입니다 .

코드 사용

이제 새 객체를 만들고 업데이트를 수행하도록하겠습니다. JS 코드의 업데이트가 입력에 나타나고 입력의 변경 이벤트가 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);

데모 : http://jsfiddle.net/RkTMD/


5
+1 매우 깨끗한 접근 방식, 사람들이 배울 수있을 정도로 간단하고 간단하며 내가 가진 것보다 훨씬 깨끗합니다. 일반적인 사용 사례는 코드에서 템플릿을 사용하여 객체의 뷰를 나타내는 것입니다. 이것이 어떻게 작동하는지 궁금합니다. Mustache와 같은 엔진 Mustache.render(template,object)에서 객체를 템플릿과 동기화 된 상태로 유지한다고 가정하고 (Mustache에만 해당되지 않음) 어떻게하면됩니까?
Benjamin Gruenbaum

3
@ BenjaminGruenbaum : 클라이언트 측 템플릿을 사용하지는 않았지만 Mustache에 삽입 지점을 식별하는 구문이 있고 해당 구문에 레이블이 있다고 생각합니다. 따라서 템플릿의 "정적"부분은 Array에 저장된 HTML 덩어리로 렌더링되고 동적 부분은 해당 덩어리 사이에 있다고 생각합니다. 그런 다음 삽입 점의 레이블이 객체 속성으로 사용됩니다. 그런 다음 일부 input가 해당 지점 중 하나를 업데이트하는 경우 입력에서 해당 지점으로의 맵핑이 있습니다. 간단한 예를 생각해 보겠습니다.

1
@ BenjaminGruenbaum : 흠 ... 나는 두 가지 요소를 깨끗하게 조정하는 방법에 대해 생각하지 못했습니다. 이것은 처음에 생각했던 것보다 조금 더 복잡합니다. 그래도 궁금해서 조금 나중에 작업해야 할 수도 있습니다. :)

2
Template구문 분석을 수행하고 다른 MyCtor객체를 보유 하며 식별자로 각 객체를 업데이트하는 인터페이스를 제공하는 기본 생성자가 있음을 알 수 있습니다. 궁금한 점이 있으면 알려주세요. :) 편집 : ... 이 링크를 대신 사용 하십시오 ... JS 업데이트를 보여주기 위해 10 초마다 입력 값이 기하 급수적으로 증가한다는 것을 잊었습니다. 이것은 그것을 제한합니다.

2
... 완전히 논평 된 버전 과 약간의 개선.

36

그래서 나는 냄비에 내 자신의 솔루션을 던지기로 결정했습니다. 여기에 작동하는 바이올린이 있습니다. 이것은 최신 브라우저에서만 실행됩니다.

그것이 사용하는 것

이 구현은 매우 현대적입니다. (매우) 최신 브라우저가 필요하며 사용자에게는 두 가지 새로운 기술이 필요합니다.

  • MutationObserver이야 는 DOM의 변화 (이벤트 리스너가 아니라 사용되는) 검출
  • Object.observe객체의 변화를 감지하고 돔에 알리는 것입니다. 위험은이 답변이 작성되었으므로 ECMAScript TC가 Oo에 대해 논의하고 결정 했으므로 polyfill을 고려하십시오 .

작동 원리

  • 요소에 domAttribute:objAttribute매핑을 넣으십시오 -예를 들어bind='textContent:name'
  • dataBind 함수에서 읽어보십시오. 요소와 객체 모두의 변경 사항을 관찰하십시오.
  • 변경이 발생하면 관련 요소를 업데이트하십시오.

해결책

dataBind함수 는 다음과 같습니다. 코드는 20 줄에 불과하며 짧을 수 있습니다.

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]; 
}

사용법은 다음과 같습니다.

HTML :

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

자바 스크립트 :

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

여기에 작동하는 바이올린이 있습니다. 이 솔루션은 매우 일반적입니다. Object.observe 및 mutation 관찰자 shimming을 사용할 수 있습니다.


1
누군가가 유용하다고 생각하면 jsfiddle.net/P9rMm을
Benjamin Gruenbaum

1
때 명심 obj.name- 그것은 세터 내에서 변경하는 것이 외부에서 관찰 할 수없는 세터해야하지만 방송이 html5rocks.com/en/tutorials/es7/observe/#toc-notifications을 - 좀 작품에 렌치를 던졌습니다 setter를 사용하여 더 복잡하고 상호 의존적 인 동작을 원하는 경우 Oo () 또한 obj.name구성 할 수없는 경우 알림을 추가하는 다양한 트릭으로 세터를 재정의하는 것도 허용되지 않으므로 Oo ()를 사용하는 제네릭은 완전히 삭제됩니다.
Nolo

8
Object.observe가 모든 브라우저에서 제거되었습니다 : caniuse.com/#feat=object-observe
JvdBerg

1
프록시는 Object.observe 또는 github.com/anywhichway/proxy-observe 또는 gist.github.com/ebidel/1b553d571f924da2da06 또는 이전 polyfill 대신 github @JvdBerg
jimmont

29

프리 포스터에 추가하고 싶습니다. 메소드를 사용하지 않고 단순히 객체에 새로운 값을 할당 할 수있는 약간 다른 접근법을 제안합니다. 이것은 특히 오래된 브라우저에서 지원되지 않으며 IE9는 여전히 다른 인터페이스를 사용해야합니다.

가장 주목할만한 것은 내 접근 방식이 이벤트를 사용하지 않는다는 것입니다.

게터와 세터

내 제안은 게터와 세터 , 특히 세터 의 비교적 젊은 기능을 사용 합니다. 일반적으로 뮤 테이터를 사용하면 특정 속성에 값이 할당되고 검색되는 방식의 동작을 "사용자 정의"할 수 있습니다.

여기서 사용할 하나의 구현은 Object.defineProperty 메소드입니다. FireFox, GoogleChrome 및 IE9에서 작동합니다. 다른 브라우저를 테스트하지는 않았지만 이론이기 때문에 ...

어쨌든 세 가지 매개 변수를 사용합니다. 첫 번째 매개 변수는 새 특성을 정의하려는 오브젝트이고, 두 번째 매개 변수는 새 특성의 이름과 유사한 문자열이고 마지막 특성은 새 특성의 동작에 대한 정보를 제공하는 "설명자 오브젝트"입니다.

특히 흥미로운 두 가지 설명자는 getset입니다. 예를 들면 다음과 같습니다. 이 두 가지를 사용하면 다른 4 개의 설명자를 사용할 수 없습니다.

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;
        }
    });
}

이제 이것을 사용하는 것이 약간 다릅니다.

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

나는 이것이 최신 브라우저에서만 작동한다는 것을 강조하고 싶습니다.

일 바이올린 : http://jsfiddle.net/Derija93/RkTMD/1/


2
우리가 Harmony Proxy객체 만 가지고 있다면 :) Setter는 좋은 아이디어처럼 보이지만 실제 객체를 수정하지 않아도됩니까? 또한, 참고로- Object.create여기에서 사용할 수 있습니다 (두 번째 매개 변수를 허용하는 최신 브라우저를 가정). 또한 setter / getter를 사용하여 객체와 DOM 요소에 다른 값을 '투사'할 수 있습니다. :). 템플릿에 대한 통찰력이 있는지 궁금합니다. 특히
훌륭한

저의 프리 포스터처럼 클라이언트 측 템플릿 엔진도 많이 사용하지 않습니다. 죄송합니다. :( 그러나 실제 객체수정 한다는 것은 무엇을 의미 합니까? 그리고 나는 setter / getter가 ...에 사용될 수 있다는 것을 이해하는 방법에 대한 당신의 생각을 이해하고 싶습니다 . 그러나 기본적으로 Proxy당신이 말한 것처럼 모든 입력을 객체로 가져오고 DOM 요소로 검색합니다 .;) 나는 두 개의 다른 속성을 동기화 된 상태로 유지해야한다는 과제를 이해했습니다. 내 방법은 두 가지 중 하나를 제거합니다.
Kiruse

A Proxy는 getter / setter를 사용할 필요가 없으므로 속성이 무엇인지 모른 채 요소를 바인딩 할 수 있습니다. 내가 의미하는 바는 게터가 bindTo.value보다 더 많은 것을 변경할 수 있다는 것입니다. 그것들은 로직 (그리고 템플릿)을 포함 할 수 있습니다. 문제는 템플릿을 염두에두고 이러한 종류의 양방향 바인딩을 유지하는 방법입니다. 객체를 양식에 매핑한다고 가정하고 요소와 양식을 동기화 된 상태로 유지하고 싶습니다. 그런 종류의 일에 대해 어떻게 진행하는지 궁금합니다. 당신은 체크 아웃 할 수있는 방법 녹아웃에 그 작품 learn.knockoutjs.com/#/?tutorial=intro 예를 들어
벤자민 Gruenbaum

@BenjaminGruenbaum 잡았다. 한번 살펴 볼게요.
Kiruse

@ BenjaminGruenbaum 나는 당신이 이해하려고하는 것을 봅니다. 템플릿을 염두에두고이 모든 것을 설정하는 것은 조금 더 어려운 것으로 판명되었습니다. 나는 스크립트를 잠시 동안 작업 하고 지속적으로 리베이스 할 것입니다. 하지만 지금은 휴식을 취하고 있습니다. 나는 실제로 이것에 대한 시간이 없다.
Kiruse

7

내 대답은 더 기술적이지만 다른 사람들은 다른 기술을 사용하여 동일한 것을 제시하므로 다르지 않습니다.
먼저,이 문제에 대한 해결책은 "관찰자"라는 디자인 패턴을 사용하는 것입니다. 프리젠 테이션에서 데이터를 분리하여 한 가지 변경 사항을 청취자에게 브로드 캐스트 할 수 있습니다. 양방향으로 만들어졌습니다.

DOM에서 JS로

DOM의 데이터를 js 객체에 바인딩하려면 다음과 같이 data속성 (또는 호환성이 필요한 경우 클래스) 형식으로 마크 업을 추가 할 수 있습니다 .

<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=""/>

이런 식으로 js를 통해 querySelectorAll(또는 getElementsByClassName호환성을 위해 오래된 친구) 를 통해 액세스 할 수 있습니다 .

이제 변경 사항을 수신하는 이벤트를 객체 당 하나의 리스너 또는 컨테이너 / 문서에 대한 하나의 큰 리스너로 바인딩 할 수 있습니다. 문서 / 컨테이너에 바인딩하면 문서 또는 하위 항목에서 변경 될 때마다 이벤트가 트리거되고 메모리 사용량이 적지 만 이벤트 호출이 생성됩니다.
코드는 다음과 같습니다.

//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);

JS의 경우 DOM 방식

두 가지가 필요합니다. 마녀 DOM 요소의 참조를 보유하는 하나의 메타 객체는 각 js 객체 / 속성에 바인딩되며 객체의 변경 사항을 듣는 방법입니다. 기본적으로 같은 방식입니다. 객체의 변경 사항을 듣고 DOM 노드에 바인딩 할 수있는 방법이 있어야합니다. 객체는 메타 데이터를 가질 수 없으므로 메타 데이터를 보유하는 다른 객체가 필요합니다. 속성 이름이 메타 데이터 개체의 속성에 매핑됩니다. 코드는 다음과 같습니다.

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);

도움이 되었기를 바랍니다.


.observer를 사용할 때 비교 가능한 문제가 없습니까?
Mohsen Shakiba

현재로서는 Object.observe지지대가 크롬에만 존재하기 때문에 심 또는 폴리 필이 필요합니다 . caniuse.com/#feat=object-observe
madcampos

9
Object.observe가 종료되었습니다. 내가 여기서 주목할 것이라고 생각했습니다.
Benjamin Gruenbaum

@BenjaminGruenbaum 이것이 죽었 기 때문에 지금 사용해야 할 올바른 것은 무엇입니까?
johnny

1
@johnny 내가 틀리지 않으면 객체로 할 수있는 일을보다 세밀하게 제어 할 수 있기 때문에 프록시 트랩이 될 것이지만 조사해야합니다.
madcampos

7

어제 데이터 바인딩 방법을 직접 작성하기 시작했습니다.

그것을 가지고 노는 것은 매우 재미있다.

나는 그것이 아름답고 매우 유용하다고 생각합니다. 적어도 파이어 폭스와 크롬을 사용한 테스트에서 Edge도 작동해야합니다. 다른 사람들에 대해서는 확실하지 않지만 그들이 프록시를 지원한다면 그것이 효과가 있다고 생각합니다.

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' />

코드는 다음과 같습니다.

(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);
                }
            }
        }
    }
})();

그런 다음 설정하려면 다음을 수행하십시오.

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'
})

지금은 방금 HTMLInputElement 값 바인딩을 추가했습니다.

개선 방법을 알고 있다면 알려주십시오.


6

이 링크 "자바 스크립트에서 쉬운 양방향 데이터 바인딩" 에 2-way 데이터 바인딩의 매우 간단한 베어 본 구현이 있습니다.

knockoutjs, backbone.js 및 agility.js의 아이디어와 함께 이전 링크는 이 가볍고 빠른 MVVM 프레임 워크 인 ModelView.js로 연결되었습니다. jQuery를 기반으로 jQuery와 잘 어울리고 겸손하거나 겸손하지 않은 저자입니다.

블로그 게시물 링크 에서 아래의 샘플 코드를 재현합니다 .

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;
}

JavaScript 객체와 관련하여이 실험을 위해 사용자 모델의 최소 구현은 다음과 같습니다.

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;
}

이제 모델의 속성을 UI에 바인딩 할 때마다 해당 HTML 요소에 적절한 데이터 속성을 설정해야합니다.

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

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

이 링크가 질문에 대한 답변을 제공 할 수 있지만 여기에 답변의 필수 부분을 포함시키고 참조 용 링크를 제공하는 것이 좋습니다. 링크 된 페이지가 변경되면 링크 전용 답변이 유효하지 않을 수 있습니다.
Sam Hanley

내가 그것을로 응답 게시물에 대한 다소 긴 코드, 더 많은 시간이있을 때 @sphanley, 지적, 난 아마 업데이 트됩니다
니 코스 M.

@sphanley, 참조 링크에서 응답으로 샘플 코드를 재현했습니다 (어쨌든 thinbk는 대부분 중복 콘텐츠를 생성하지만)
Nikos M.

1
그것은 분명히 중복 된 내용을 만들지 만 요점은 블로그 링크가 종종 시간이 지남에 따라 중단 될 수 있으며 여기에 관련 내용 을 복제하여 향후 독자에게 유용하고 유용 할 수 있도록합니다. 대답은 지금 멋져 보인다!
Sam Hanley

3

요소의 값을 변경하면 DOM 이벤트 가 트리거 될 수 있습니다 . 이벤트에 응답하는 리스너는 JavaScript에서 데이터 바인딩을 구현하는 데 사용될 수 있습니다.

예를 들면 다음과 같습니다.

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;
  });
}

다음 은 DOM 요소를 서로 또는 JavaScript 객체와 바인딩하는 방법을 보여주는 코드와 데모입니다.


3

모든 HTML 입력 바인드

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

두 가지 기능을 정의하십시오.

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);
}

기능을 사용하십시오 :

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

3

다음 Object.defineProperty은 속성에 액세스하는 방식을 직접 수정 하는 아이디어 입니다.

암호:

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

용법:

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

p.variable="yes"

바이올린 : 여기


2

js와 js에 바인딩 뷰를 만들기 위해 onkeypress 및 onchange 이벤트 핸들러를 사용하는 기본 자바 스크립트 예제를 살펴 보았습니다.

다음은 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

변수를 입력에 바인딩하는 간단한 방법 (양방향 바인딩)은 getter 및 setter의 입력 요소에 직접 액세스하는 것입니다.

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

HTML에서 :

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

그리고 사용하려면 :

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());


getter / setter없이 위의 작업을 수행하는 더 멋진 방법 :

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

쓰다:

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

1

바닐라 자바 ​​스크립트에서 매우 간단한 양방향 데이터 바인딩입니다.

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

<div id="name">

</div>


2
확실히 이것은 onkeyup 이벤트에서만 작동합니까? 즉, 당신은 아약스 요청을 한 다음 다음 자바 스크립트를 통해 본 것없는 일을 innerHTML을 변경하는 경우
자크 스미스

1

파티에 늦게, 특히 몇 달 전에 몇 년 전에 2 개의 라이브러리를 작성 했으므로 나중에 언급 할 것이지만 여전히 나와 관련이 있습니다. 실제로 스포일러를 짧게 만들기 위해 내가 선택한 기술은 다음과 같습니다.

  • Proxy 모형 관찰 용
  • MutationObserver DOM의 변경 사항 추적 (값 변경이 아닌 바인딩 이유)
  • 값 변경 (모델 흐름보기)은 일반 addEventListener핸들러 를 통해 처리됩니다.

IMHO는 OP 외에도 데이터 바인딩 구현이 다음을 수행하는 것이 중요합니다.

  • 다양한 앱 수명주기 사례 처리 (HTML, JS, JS, HTML, 동적 속성 변경 등)
  • 모델의 깊은 바인딩을 허용하여 모델이 바인딩되도록 user.address.block
  • 모델로 배열 제대로 지원 (해야한다 shift, splice그리고 모두)
  • ShadowDOM 처리
  • 가능한 한 기술을 쉽게 교체 할 수 있도록하기 위해 모든 템플릿 하위 언어는 프레임 워크와 너무 밀접하게 결합되어 있기 때문에 미래 지향적이지 않은 접근 방식입니다.

모든 사람들을 고려하면 수십 개의 JS 라인을 던질 수 없습니다. 나는 그것을 lib 대신 패턴 으로하려고 노력했다 -나에게 효과가 없었다.

다음으로 가지고있는 것을 Object.observe제거하고 모델을 관찰하는 것이 중요하다는 점을 감안하면이 부분은 반드시 다른 라이브러리와 분리되어야합니다. 이제 OP가 요청 한대로이 문제를 어떻게 처리했는지에 대한 교장의 시점까지 :

모델 (JS 부분)

모델 관찰에 대한 나의 취향은 프록시 이며, 그것이 작동하게하는 유일한 방법 인 IMHO입니다. 모든 기능을 갖춘 observer라이브러리는 자체 라이브러리가 필요하므로 object-observer그 목적을 위해 라이브러리를 개발했습니다 .

모델은 POJO가 Observables 로 바뀌는 지점 인 일부 전용 API를 통해 등록해야합니다 . 여기서 바로 가기를 볼 수 없습니다. 바운드 뷰 (아래 참조)로 간주되는 DOM 요소는 처음에 모델 값으로 업데이트 된 다음 각 데이터가 변경 될 때마다 업데이트됩니다.

뷰 (HTML 부분)

바인딩을 표현하는 가장 깨끗한 방법 인 IMHO는 속성을 사용하는 것입니다. 많은 사람들이 전에 이것을했고 많은 사람들이 그렇게 할 것이므로 여기서는 아무런 소식이 없습니다. 이것은 올바른 방법입니다. 필자의 경우 다음 구문을 사용 <span data-tie="modelKey:path.to.data => targerProperty"></span>했지만 중요하지 않습니다. 무엇 이며 나에게 중요한 것은, HTML에서 더 복잡한 스크립트 구문 -이 다시, 이럴 잘못입니다.

바운드 뷰로 지정된 모든 요소는 처음에 수집됩니다. 모델과 뷰 사이의 일부 내부 매핑을 관리하는 것은 성능 측면에서 필연적으로 보이지만 런타임 조회 및 업데이트를 저장하기 위해 메모리 + 일부 관리를 희생 해야하는 올바른 경우 인 것 같습니다.

가능한 경우 뷰는 모델에서 처음에 업데이트되고 이후에 모델이 변경되면 업데이트됩니다. 더욱 MutationObserver동적으로 추가 / 제거 / 변경된 요소에 반응 (바인드 / 언 바인드)하기 위해서는 전체 DOM을 관찰해야합니다 . 또한,이 모든 것은 바운드 블랙홀을 남기지 않기 위해 ShadowDOM으로 복제되어야합니다 (물론 하나를 열어야합니다).

세부 사항 목록은 실제로 더 나아갈 수 있지만, 제 생각에는 데이터 바인딩을 하나의 기능 완성도와 다른 측면의 깔끔한 단순성 사이에서 균형있게 구현하는 주요 원칙입니다.

따라서 object-observer위에서 언급 한 것 외에도 data-tier위에서 언급 한 개념을 따라 데이터 바인딩을 구현하는 라이브러리 도 실제로 작성했습니다 .


0

지난 7 년 동안 많은 변화가 있었으며 현재 대부분의 브라우저에 기본 웹 구성 요소가 있습니다. IMO 문제의 핵심은 상태가 변경 될 때 UI를 업데이트하고 그 반대의 경우에도 UI를 업데이트하기 만하면 요소간에 상태를 공유하는 것입니다.

요소간에 데이터를 공유하기 위해 StateObserver 클래스를 작성하고 웹 컴포넌트를 확장 할 수 있습니다. 최소 구현은 다음과 같습니다.

// 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>

여기에 바이올린

나는이 접근법을 좋아한다.

  • data-속성 을 찾기위한 돔 통과 없음
  • Object.observe 없음 (더 이상 사용되지 않음)
  • 프록시 없음 (어쨌든 후크를 제공하지만 통신 메커니즘은 없음)
  • 종속성 없음 (대상 브라우저에 따라 폴리 필 제외)
  • 합리적으로 중앙 집중화되고 모듈화되어 있습니다. html로 상태를 설명하고 모든 곳에서 청취자가 매우 어려워집니다.
  • 확장 가능합니다. 이 기본 구현은 20 줄의 코드이지만 작업하기 쉽도록 일부 편의성, 불변성 및 상태 모양 마법을 쉽게 구축 할 수 있습니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.