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


Answers

그래서, 나는 냄비에 내 자신의 솔루션을 던지기로 결정했습니다. 여기에 일하는 바이올린이 있습니다. 매우 현대적인 브라우저에서만 실행됩니다.

사용 용도

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

  • dom의 변경을 탐지하는 MutationObservers (이벤트 리스너도 사용됨)
  • Object.observe 를 사용하여 객체의 변경 사항을 감지하고 DOM에 알립니다. 위험,이 답변은 Oo가 ECMAScript TC에 의해 논의되고 결정되었으므로 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 observer shimming을 사용할 수 있습니다.

Question

이 질문을 엄격한 교육으로 취급하십시오. 나는 아직도 새로운 답변과 아이디어를 구현하는 것에 관심이있다.

tl; dr

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

DOM에 대한 데이터 바인딩

DOM에 바인딩 된 데이터는 예를 들어, 속성이있는 JavaScript 객체 a 를 가짐을 의미합니다. b . 그런 다음 DOM 요소가 변경되면 <input> DOM 요소 (예 :)를 가지며 그 반대도 마찬가지입니다 (즉, 양방향 데이터 바인딩을 의미 함).

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

그래서 기본적으로 비슷한 JavaScript가 있습니다.

var a = {b:3};

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

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

입력 값이 ab 값 (예 ab 되고 입력 텍스트가 변경되면 ab 도 변경하고 싶습니다. JavaScript에서 ab 변경되면 입력이 변경됩니다.

질문

일반 자바 스크립트에서이를 수행하기위한 몇 가지 기본 기술은 무엇입니까?

구체적으로 말하면 좋은 대답을 원합니다.

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

나는 무엇을 시도 했는가?

저는 콧수염을 좋아해서 템플릿으로 사용하려고했습니다. 그러나 Mustache는 문자열을 HTML로 처리하므로 데이터 바인딩 자체를 수행하려고 할 때 문제가 발생했습니다. 결과를 얻은 후에는 내 viewmodel의 객체가 어디에 있는지에 대한 참조가 없습니다. 이 문제를 해결할 수있는 유일한 방법은 HTML 문자열 (또는 DOM 트리 생성) 자체를 특성으로 수정하는 것입니다. 다른 템플릿 엔진을 사용해도 상관 없습니다.

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

참고 : 외부 라이브러리, 특히 수천 줄의 코드를 사용하는 응답은 제공하지 마십시오. 저는 AngularJS와 KnockoutJS를 사용했습니다 (그리고 좋아합니다!). 정말 '프레임 워크 x 사용'형식의 응답을 원하지 않습니다. 최적으로, 저는 양방향 데이터 바인딩 자신을 구현하는 방법을 이해하기 위해 많은 프레임 워크를 사용하는 방법을 모르는 미래의 독자를 원합니다. 나는 완벽한 대답을 기대하지는 않지만 그 생각을 가로막는 대답을 기대한다.




요소의 값을 변경하면 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;
  });
}

Here 은 DOM 요소가 서로 또는 JavaScript 객체로 바인딩되는 방법을 보여주는 코드 및 데모입니다.




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



어제 나는 데이터를 바인딩하는 내 자신의 방법을 쓰기 시작했다.

그걸 가지고 노는 것은 매우 재미 있습니다.

나는 그것이 아름답고 매우 유용하다고 생각한다. 적어도 파이어 폭스와 크롬을 사용하는 테스트에서 Edge도 작동해야합니다. 다른 사람들에 대해서는 잘 모르겠지만 Proxy를 지원한다면 제대로 작동 할 것입니다.

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 값 bind를 추가하기 만하면됩니다.

당신이 그것을 향상시키는 방법을 안다면 그것을 알게 될 것입니다.




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

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

이렇게하면 querySelectorAll 를 통해 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);

내가 도움이되기를 바랍니다.




A simple way of binding a variable to an input (two-way binding) is to just directly access the input element in the getter and setter:

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

In HTML:

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

And to use:

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

A fancier way of doing the above without 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.



Related