Jak zaimplementować wiązanie danych DOM w JavaScript

244

Potraktuj to pytanie jako ściśle edukacyjne. Nadal jestem zainteresowany nowymi odpowiedziami i pomysłami na wdrożenie tego

tl; dr

Jak zaimplementować dwukierunkowe wiązanie danych za pomocą JavaScript?

Powiązanie danych z DOM

Przez powiązanie danych z DOM rozumiem na przykład posiadanie obiektu JavaScript az właściwością b. Następnie mając <input>element DOM (na przykład), gdy element DOM się zmienia, azmienia się i odwrotnie (to znaczy dwukierunkowe wiązanie danych).

Oto schemat od AngularJS na temat tego, jak to wygląda:

dwukierunkowe wiązanie danych

Więc w zasadzie mam JavaScript podobny do:

var a = {b:3};

Następnie element wejściowy (lub inny formularz), taki jak:

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

Chciałbym, aby wartość wejściowa była wartością a.b(na przykład), a gdy tekst wejściowy zmienia się, również chciałbym a.bzmienić. Kiedya.b zmiany w JavaScript, dane wejściowe ulegają zmianie.

Pytanie

Jakie są podstawowe techniki, aby to osiągnąć w zwykłym JavaScript?

W szczególności chciałbym, aby dobra odpowiedź dotyczyła:

  • Jak działałoby wiązanie dla obiektów?
  • Jak może działać słuchanie zmian w formularzu?
  • Czy w prosty sposób można modyfikować HTML tylko na poziomie szablonu? Chciałbym nie śledzić powiązania w samym dokumencie HTML, ale tylko w JavaScript (ze zdarzeniami DOM i JavaScript z odniesieniem do użytych elementów DOM).

Co próbowałem?

Jestem wielkim fanem Wąsów, więc spróbowałem użyć go do szablonów. Jednak natrafiłem na problemy podczas próby wykonania samego wiązania danych, ponieważ wąsy przetwarza HTML jako ciąg znaków, więc po otrzymaniu jego wyniku nie mam odniesienia do tego, gdzie znajdują się obiekty w moim viewmodelu. Jedynym obejściem, jakie mogłem wymyślić, było zmodyfikowanie samego łańcucha HTML (lub utworzonego drzewa DOM) za pomocą atrybutów. Nie mam nic przeciwko użyciu innego silnika szablonów.

Zasadniczo miałem wrażenie, że komplikowałem ten problem i istnieje proste rozwiązanie.

Uwaga: Proszę nie podawać odpowiedzi, które korzystają z bibliotek zewnętrznych, zwłaszcza tych, które zawierają tysiące linii kodu. Użyłem (i lubię!) AngularJS i KnockoutJS. Naprawdę nie chcę odpowiedzi w formie „use framework x”. Optymalnie chciałbym, aby przyszły czytelnik nie wiedział, jak korzystać z wielu frameworków, aby samodzielnie zrozumieć, w jaki sposób samodzielnie wdrożyć dwukierunkowe wiązanie danych. Nie oczekuję pełnej odpowiedzi, ale takiej, która przenosi ten pomysł.

Benjamin Gruenbaum
źródło
2
Oparłem CrazyGlue na projektowaniu Benjamin Gruenbaum użytkownika. Obsługuje również tagi SELECT, checkbox i radio. jQuery jest zależnością.
JohnSz
12
To pytanie jest całkowicie niesamowite. Jeśli kiedykolwiek zostanie zamknięty za bycie poza tematem lub jakiś inny głupi nonsens, będę poważnie odznaczony.
OCDev
@JohnSz dzięki za wzmiankę o projekcie CrazyGlue. Długo szukałem prostego, dwukierunkowego segregatora danych. Wygląda na to, że nie używasz Object.observe, więc obsługa przeglądarki powinna być świetna. I nie używasz szablonów wąsów, więc jest idealny.
Gavin
@Benjamin Czym się zajmujesz?
Johnny
@ johnny moim zdaniem poprawnym podejściem jest utworzenie DOM w JS (jak React), a nie odwrotnie. Myślę, że w końcu to zrobimy.
Benjamin Gruenbaum

Odpowiedzi:

106
  • Jak działałoby wiązanie dla obiektów?
  • Jak może działać słuchanie zmian w formularzu?

Abstrakcja aktualizująca oba obiekty

Przypuszczam, że istnieją inne techniki, ale ostatecznie miałbym obiekt, który zawiera odniesienie do pokrewnego elementu DOM i zapewnia interfejs, który koordynuje aktualizacje własnych danych i powiązanego elementu.

.addEventListener()Zapewnia bardzo przyjemny interfejs do tego. Możesz nadać mu obiekt, który implementuje eventListenerinterfejs, i będzie wywoływał swoje procedury obsługi z tym obiektem jako thiswartością.

Zapewnia to automatyczny dostęp zarówno do elementu, jak i powiązanych z nim danych.

Definiowanie swojego obiektu

Dziedziczenie prototypowe to dobry sposób na wdrożenie tego, choć oczywiście nie jest to wymagane. Najpierw utwórz konstruktor, który odbierze Twój element i niektóre dane początkowe.

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

Tak więc tutaj konstruktor przechowuje element i dane dotyczące właściwości nowego obiektu. Wiąże również changezdarzenie z danym element. Interesujące jest to, że przekazuje nowy obiekt zamiast funkcji jako drugi argument. Ale to samo nie zadziała.

Implementacja eventListenerinterfejsu

Aby to zadziałało, twój obiekt musi zaimplementować eventListenerinterfejs. Wszystko, co jest potrzebne do tego celu, to dać obiektowi handleEvent()metodę.

Tam właśnie przychodzi dziedzictwo.

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

Istnieje wiele różnych sposobów, w jakie można to ustrukturyzować, ale dla twojego przykładu koordynowania aktualizacji postanowiłem, że change()metoda będzie akceptować tylko wartość i handleEventprzekaże tę wartość zamiast obiektu zdarzenia. W ten sposób change()można wywoływać również bez zdarzenia.

Teraz, gdy changezdarzenie się wydarzy, zaktualizuje zarówno element, jak i .datawłaściwość. To samo stanie się, gdy zadzwonisz .change()w swoim programie JavaScript.

Korzystanie z kodu

Teraz wystarczy utworzyć nowy obiekt i pozwolić mu wykonywać aktualizacje. Aktualizacje kodu JS pojawią się na wejściu, a zmiany zdarzeń na wejściu będą widoczne dla kodu 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/

1106925
źródło
5
+1 Bardzo czyste podejście, bardzo prosto i na tyle proste, że ludzie mogą się uczyć, dużo czystsze niż to, co miałem. Typowym przypadkiem użycia jest użycie szablonów w kodzie do reprezentowania widoków obiektów. Zastanawiałem się, jak to może tutaj działać? W silnikach takich jak Mustache robię coś Mustache.render(template,object), zakładając, że chcę zsynchronizować obiekt z szablonem (niespecyficznym dla Mustache), jak mam to zrobić?
Benjamin Gruenbaum,
3
@BenjaminGruenbaum: Nie korzystałem z szablonów po stronie klienta, ale wyobrażam sobie, że wąsy mają pewną składnię do identyfikowania punktów wstawiania i że ta składnia zawiera etykietę. Sądzę więc, że „statyczne” części szablonu zostałyby renderowane na fragmenty HTML przechowywane w tablicy, a części dynamiczne przechodziłyby między tymi fragmentami. Następnie etykiety w punktach wstawiania byłyby używane jako właściwości obiektu. Następnie, jeśli niektóre inputmają zaktualizować jeden z tych punktów, nastąpi mapowanie od danych wejściowych do tego punktu. Zobaczę, czy mogę wymyślić szybki przykład.
1
@BenjaminGruenbaum: Hmmm ... Nie zastanawiałem się nad tym, jak dokładnie koordynować dwa różne elementy. Jest to trochę bardziej zaangażowane, niż początkowo myślałem. Jestem ciekawy, więc może będę musiał popracować nad tym trochę później. :)
2
Zobaczysz, że istnieje główny Templatekonstruktor, który wykonuje parsowanie, przechowuje różne MyCtorobiekty i zapewnia interfejs do aktualizacji każdego z nich za pomocą swojego identyfikatora. Daj mi znać, jeśli masz pytania. :) EDYCJA: ... zamiast tego użyj tego linku ... Zapomniałem, że co 10 sekund mam wykładniczy wzrost wartości wejściowej, aby zademonstrować aktualizacje JS. To ogranicza.
2
... w pełni skomentowana wersja plus drobne poprawki.
36

Postanowiłem więc wrzucić własne rozwiązanie do puli. Oto działające skrzypce . Uwaga: działa to tylko w bardzo nowoczesnych przeglądarkach.

Czego używa

Ta implementacja jest bardzo nowoczesna - wymaga (bardzo) nowoczesnej przeglądarki, a użytkownicy dwóch nowych technologii:

  • MutationObservers do wykrywania zmian w dom (używane są również detektory zdarzeń)
  • Object.observew celu wykrycia zmian w obiekcie i powiadomienia dom. Niebezpieczeństwo, odkąd ta odpowiedź została napisana Oo zostało omówione i odrzucone przez ECMAScript TC, rozważ polifill .

Jak to działa

  • Na elemencie umieść domAttribute:objAttributemapowanie - na przykładbind='textContent:name'
  • Przeczytaj to w funkcji dataBind. Obserwuj zmiany zarówno elementu, jak i obiektu.
  • Kiedy nastąpi zmiana - zaktualizuj odpowiedni element.

Rozwiązanie

Oto dataBindfunkcja, zauważ, że to tylko 20 linii kodu i może być krótsza:

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

Oto niektóre zastosowania:

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

Oto działające skrzypce . Zauważ, że to rozwiązanie jest dość ogólne. Dostępny jest podgląd obiektu i obserwacja mutacji.

Benjamin Gruenbaum
źródło
1
Właśnie napisałem to (es5) dla zabawy, jeśli ktoś uzna to za przydatne - znokautuj jsfiddle.net/P9rMm
Benjamin
1
Należy pamiętać, że gdy obj.namema Settera, nie można go zaobserwować zewnętrznie, ale musi nadawać, że zmienił się z Settera - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - trochę wrzuca klucz w prace dla Oo (), jeśli chcesz bardziej złożone, współzależne zachowanie za pomocą ustawiaczy. Ponadto, gdy obj.namenie jest konfigurowalne, redefiniowanie jego ustawiacza (z różnymi sztuczkami, aby dodać powiadomienie) jest również niedozwolone - więc generyczne z Oo () są całkowicie złomowane w tym konkretnym przypadku.
Nolo,
8
Object.observe jest usuwany ze wszystkich przeglądarek: caniuse.com/#feat=object-observe
JvdBerg
1
Zamiast Object.observe można użyć serwera proxy lub github.com/anywhichway/proxy-observe lub gist.github.com/ebidel/1b553d571f924da2da06 lub starszych polifillów, również na github @JvdBerg
jimmont
29

Chciałbym dodać do mojego prepostera. Sugeruję nieco inne podejście, które pozwoli ci po prostu przypisać nową wartość do twojego obiektu bez użycia metody. Należy jednak zauważyć, że nie jest to obsługiwane przez szczególnie starsze przeglądarki, a IE9 nadal wymaga użycia innego interfejsu.

Najważniejsze jest to, że moje podejście nie wykorzystuje wydarzeń.

Getters and Setters

Moja propozycja korzysta ze stosunkowo młodej funkcji pobierających i ustawiających , szczególnie tylko ustawiających. Ogólnie rzecz biorąc, mutatory pozwalają nam „dostosować” sposób, w jaki pewne właściwości są przypisywane wartości i pobierane.

Jedną implementacją, której będę tutaj używać, jest metoda Object.defineProperty . Działa w FireFox, GoogleChrome i - tak myślę - IE9. Nie testowałem innych przeglądarek, ale ponieważ jest to tylko teoria ...

W każdym razie akceptuje trzy parametry. Pierwszy parametr to obiekt, dla którego chcesz zdefiniować nową właściwość, drugi to ciąg znaków przypominający nazwę nowej właściwości, a ostatni „obiekt deskryptora” dostarczający informacji o zachowaniu nowej właściwości.

Dwa szczególnie interesujące deskryptory to geti set. Przykład mógłby wyglądać podobnie do następującego. Zauważ, że użycie tych dwóch zabrania używania pozostałych 4 deskryptorów.

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

Teraz korzystanie z tego staje się nieco inne:

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

Chcę podkreślić, że działa to tylko w przypadku nowoczesnych przeglądarek.

Działające skrzypce: http://jsfiddle.net/Derija93/RkTMD/1/

Kiruse
źródło
2
Gdybyśmy tylko mieli Proxyobiekty Harmony :) Setery wydają się fajnym pomysłem, ale czy nie wymagałoby to modyfikacji rzeczywistych obiektów? Na marginesie - Object.createmożna go tutaj użyć (ponownie, zakładając, że nowoczesna przeglądarka pozwoliła na drugi parametr). Ponadto setter / getter może być użyty do „rzutowania” innej wartości na obiekt i element DOM :). Zastanawiam się, czy masz jakieś spostrzeżenia na temat tworzenia szablonów, co wydaje się tutaj prawdziwym wyzwaniem, szczególnie jeśli chodzi o ładną strukturę :)
Benjamin Gruenbaum
Podobnie jak mój preposter, ja również nie pracuję dużo z silnikami szablonów po stronie klienta, przepraszam. :( Ale co rozumiesz przez modyfikację rzeczywistych obiektów ? Chciałbym zrozumieć twoje myśli na temat tego, w jaki sposób zrozumiałeś, że setter / getter może być użyty do ... Getters / setters tutaj są używane do niczego ale przekierowanie wszystkich danych wejściowych i pobierania z obiektu do elementu DOM, w zasadzie jak Proxy, jak powiedziałeś;) Zrozumiałem wyzwanie polegające na zsynchronizowaniu dwóch różnych właściwości. Moja metoda eliminuje jedno z obu.
Kiruse
ProxyWyeliminuje konieczność używania pobierające / ustawiające, można powiązać elementy nie wiedząc, jakie właściwości mają. Miałem na myśli to, że pobierający mogą zmienić więcej niż bindTo.value, mogą zawierać logikę (a może nawet szablon). Pytanie brzmi: jak zachować tego rodzaju dwukierunkowe wiązanie z myślą o szablonie? Powiedzmy, że mapuję mój obiekt na formularz, chciałbym zachować zarówno element, jak i formularz zsynchronizowany i zastanawiam się, jak sobie poradzę w takich sprawach. Na przykład możesz sprawdzić, jak to działa w przypadku nokautu. Learn.knockoutjs.com/#/?tutorial=intro
Benjamin Gruenbaum
@BenjaminGruenbaum Gotcha. Spojrzę na to.
Kiruse,
@BenjaminGruenbaum Widzę, co próbujesz zrozumieć. Konfiguracja tego wszystkiego z myślą o szablonach okazuje się nieco trudniejsza. Będę pracował nad tym skryptem przez pewien czas (i ciągle go bazuję). Ale na razie robię sobie przerwę. Właściwie nie mam na to czasu.
Kiruse,
7

Myślę, że moja odpowiedź będzie bardziej techniczna, ale nie różna, ponieważ inni prezentują to samo przy użyciu różnych technik.
Po pierwsze, rozwiązaniem tego problemu jest użycie wzorca projektowego znanego jako „obserwator”, pozwala on oddzielić dane od prezentacji, dzięki czemu zmiana jednej rzeczy jest transmitowana do ich słuchaczy, ale w tym przypadku jest dwukierunkowy.

Dla sposobu DOM-JS

Aby powiązać dane z DOM z obiektem js, możesz dodać znaczniki w postaci dataatrybutów (lub klas, jeśli potrzebujesz kompatybilności):

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

W ten sposób można uzyskać do niego dostęp za pomocą js używając querySelectorAll(lub starego znajomego getElementsByClassNamedla kompatybilności).

Teraz możesz powiązać zdarzenie nasłuchując zmian na różne sposoby: jeden detektor na obiekt lub jeden duży detektor do kontenera / dokumentu. Powiązanie z dokumentem / kontenerem wywoła zdarzenie dla każdej wprowadzonej w nim zmiany lub jego potomka, będzie miało mniejszy ślad pamięci, ale odrodzi wywołania zdarzeń.
Kod będzie wyglądał mniej więcej tak:

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

Dla JS do DOM sposób

Będziesz potrzebował dwóch rzeczy: jeden metaobiektyw, który będzie zawierał odniesienia do elementu DOM, który jest powiązany z każdym obiektem / atrybutem js oraz sposób nasłuchiwania zmian w obiektach. Jest to w zasadzie taki sam sposób: musisz mieć sposób na słuchanie zmian w obiekcie, a następnie powiązanie go z węzłem DOM, ponieważ twój obiekt „nie może mieć” metadanych, będziesz potrzebować innego obiektu, który przechowuje metadane w pewien sposób że nazwa właściwości odwzorowuje właściwości obiektu metadanych. Kod będzie mniej więcej taki:

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

Mam nadzieję, że pomogłem.

madcampos
źródło
czy nie ma problemu z porównywalnością przy użyciu .observer?
Mohsen Shakiba,
na razie potrzebuje podkładki dystansowej lub wypełniacza, Object.observeponieważ wsparcie jest na razie prezentowane tylko w chromie. caniuse.com/#feat=object-observe
madcampos
9
Object.observe nie żyje. Pomyślałem, że zwrócę na to uwagę.
Benjamin Gruenbaum
@BenjaminGruenbaum Jaka jest teraz właściwa metoda, ponieważ jest martwa?
Johnny
1
@ Johnny, jeśli się nie mylę, byłyby to pułapki proxy, ponieważ pozwalają one na bardziej szczegółową kontrolę tego, co mogę zrobić z obiektem, ale muszę to zbadać.
madcampos
7

Wczoraj zacząłem pisać własny sposób wiązania danych.

Bardzo zabawnie się z tym bawić.

Myślę, że to piękne i bardzo przydatne. Przynajmniej na moich testach z firefoxem i chromem, Edge też musi działać. Nie jestem pewien co do innych, ale jeśli obsługują proxy, myślę, że to zadziała.

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

Oto kod:

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

Następnie, aby ustawić, wystarczy:

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

Na razie dodałem właśnie powiązanie wartości HTMLInputElement.

Daj mi znać, jeśli wiesz, jak to poprawić.

tona
źródło
6

W tym linku „Łatwe dwukierunkowe wiązanie danych w JavaScript” dostępna jest bardzo prosta implementacja dwukierunkowego wiązania danych.

Poprzedni link wraz z pomysłami knockoutjs, backbone.js i agility.js doprowadził do tego lekkiego i szybkiego frameworka MVVM, ModelView.js oparty na jQuery który dobrze gra z jQuery i którego jestem skromnym (a może nie tak skromnym) autorem.

Ponowne odtwarzanie przykładowego kodu poniżej (z linku do postu na blogu ):

Przykładowy kod dla 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;
}

Jeśli chodzi o obiekt JavaScript, minimalna implementacja modelu użytkownika na potrzeby tego eksperymentu może wyglądać następująco:

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

Teraz, ilekroć chcemy powiązać właściwość modelu z fragmentem interfejsu użytkownika, musimy po prostu ustawić odpowiedni atrybut danych w odpowiednim elemencie HTML:

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

<!-- html -->
<input type="number" data-bind-123="name" />
Nikos M.
źródło
Chociaż ten link może odpowiedzieć na pytanie, lepiej jest dołączyć tutaj istotne części odpowiedzi i podać link w celach informacyjnych. Odpowiedzi zawierające tylko łącze mogą stać się nieprawidłowe, jeśli połączona strona ulegnie zmianie.
Sam Hanley,
@sphanley, zauważono, prawdopodobnie zaktualizuję, gdy będę miał więcej czasu, ponieważ jest to dość długi kod dla postu z odpowiedzią
Nikos M.
@sphanley, odtworzono przykładowy kod na odpowiedź z odnośnika (chociaż i tak myślę, że przez większość czasu tworzy to duplikat)
Nikos M.
1
Zdecydowanie tworzy zduplikowane treści, ale o to chodzi - linki do blogów często mogą zepsuć się z czasem, a powielenie odpowiednich treści tutaj zapewnia, że ​​będą one dostępne i przydatne dla przyszłych czytelników. Odpowiedź wygląda teraz świetnie!
Sam Hanley
3

Zmiana wartości elementu może wywołać zdarzenie DOM . Detektorów reagujących na zdarzenia można użyć do implementacji powiązania danych w JavaScript.

Na przykład:

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

Oto kod i wersja demonstracyjna pokazująca, w jaki sposób elementy DOM można powiązać ze sobą lub z obiektem JavaScript.

zabawny twórca
źródło
3

Powiąż dowolne wejście HTML

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

zdefiniuj dwie funkcje:

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

użyj funkcji:

var myObject = proxify('element-to-bind')
bindValue(myObject);
Ollie Williams
źródło
3

Oto pomysł, Object.definePropertyktóry bezpośrednio modyfikuje sposób dostępu do właściwości.

Kod:

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

Stosowanie:

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

p.variable="yes"

skrzypce: tutaj

Thornkey
źródło
2

Przeszedłem przez podstawowy przykład javascript przy użyciu programów obsługi zdarzeń onkeypress i onchange, aby utworzyć widok wiązania do naszych plików js i js do wyświetlenia

Oto przykład 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>
macha devendher
źródło
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>
Anthony Newlineinfo
źródło
2

Prostym sposobem powiązania zmiennej z wejściem (wiązanie dwukierunkowe) jest po prostu bezpośredni dostęp do elementu wejściowego w module pobierającym i ustawiającym:

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

W HTML:

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

I użyć:

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


Bardziej wymyślny sposób na wykonanie powyższej czynności bez gettera / setera:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Używać:

var v1 = new variable(document.getElementById("an-input"));
v1(10); // sets value to 20.
console.log(v1()); // reads value.
A-Sharabiani
źródło
1

Jest to bardzo proste dwukierunkowe wiązanie danych w javascript waniliowym ....

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

<div id="name">

</div>

Subodh Gawade
źródło
2
z pewnością działałoby to tylko ze zdarzeniem onkeyup? tzn. jeśli wykonałeś żądanie ajax, a następnie zmieniłeś innerHTML przez JavaScript, to nie zadziałałoby
Zach Smith
1

Późno na imprezę, szczególnie odkąd napisałem 2 libi związane miesiące / lata temu, wspomnę o nich później, ale nadal wydaje mi się odpowiedni. Krótko mówiąc, spoiler to technologie, które wybrałem:

  • Proxy do obserwacji modelu
  • MutationObserver do śledzenia zmian DOM (ze względów wiążących, nie zmian wartości)
  • zmiany wartości (widok przepływu modelu) są obsługiwane przez zwykłe addEventListenerprocedury obsługi

IMHO, oprócz PO, ważne jest, aby wdrożenie wiązania danych:

  • obsługiwać różne przypadki cyklu życia aplikacji (najpierw HTML, potem JS, najpierw JS, potem HTML, zmiana atrybutów dynamicznych itp.)
  • pozwalają na głębokie wiązanie modelu, aby można było związać user.address.block
  • Macierze jako model powinien być obsługiwany prawidłowo ( shift, splicei podobne)
  • obsłużyć ShadowDOM
  • starać się być możliwie jak najłatwiejszym do wymiany technologii, dlatego wszelkie podjęzyki szablonów są podejściem, które nie sprzyja przyszłym zmianom, ponieważ jest zbyt mocno połączone z frameworkiem

Biorąc to wszystko pod uwagę, moim zdaniem uniemożliwia rzucenie kilkudziesięciu linii JS. Próbowałem to zrobić raczej jako wzór niż lib - nie działało to dla mnie.

Następnie Object.observeusuwa się posiadanie , a jednak biorąc pod uwagę, że obserwacja modelu jest kluczową częścią - cała ta część MUSI być oddzielona zmartwieniem od innej biblioteki. Teraz do rzeczy, w których podjąłem ten problem - dokładnie tak, jak OP poprosił:

Model (część JS)

Moim zdaniem do obserwacji modelu jest Proxy , jest to jedyny rozsądny sposób, aby działał, IMHO. W pełni funkcjonalna observerzasługuje na własną bibliotekę, więc opracowałem object-observerbibliotekę wyłącznie w tym celu.

Model (y) należy zarejestrować za pomocą dedykowanego interfejsu API, w którym zamieniają się POJO Observable s, nie widać tutaj żadnego skrótu. Elementy DOM, które są uważane za powiązane widoki (patrz poniżej), są aktualizowane najpierw wartościami modelu / modeli, a następnie przy każdej zmianie danych.

Widoki (część HTML)

IMHO, najczystszym sposobem wyrażenia wiązania, jest użycie atrybutów. Wielu zrobiło to wcześniej, a wielu zrobi później, więc nie ma tu żadnych wiadomości, jest to po prostu właściwy sposób, aby to zrobić. W moim przypadku zastosowałem następującą składnię: <span data-tie="modelKey:path.to.data => targerProperty"></span>ale jest to mniej ważne. Co to jest dla mnie ważne, brak złożonej składni skryptów w HTML - to znowu źle, IMHO.

Najpierw gromadzone są wszystkie elementy wyznaczone jako widoki ograniczone. Z punktu widzenia wydajności zarządzanie wewnętrznym mapowaniem między modelami i widokami wydaje mi się nieuniknione, wydaje się właściwym przypadkiem, w którym pamięć + zarządzanie należy poświęcić, aby zapisać wyszukiwania i aktualizacje środowiska wykonawczego.

Widoki są aktualizowane na początku z modelu, jeśli są dostępne, a następnie z późniejszymi zmianami modelu, jak powiedzieliśmy. Co więcej, cały DOM powinien być obserwowany za pomocą MutationObserver, aby zareagować (powiązać / rozpiąć) na dynamicznie dodawanych / usuwanych / zmienianych elementach. Co więcej, wszystko to należy powielić w ShadowDOM (oczywiście jeden), aby nie pozostawiać niezwiązanych czarnych dziur.

Lista szczegółów może rzeczywiście pójść dalej, ale moim zdaniem są to główne zasady, które sprawiłyby, że wiązanie danych byłoby realizowane z zachowaniem równowagi między kompletnością funkcji z jednej i zdrowej prostoty z drugiej strony.

I tak, oprócz object-observerwspomnianego powyżej, napisałem rzeczywiście także data-tierbibliotekę, która implementuje wiązanie danych zgodnie z wyżej wymienionymi koncepcjami.

GullerYA
źródło
0

W ciągu ostatnich 7 lat wiele się zmieniło. W większości przeglądarek mamy natywne komponenty sieciowe. IMO sedno problemu polega na współdzieleniu stanu między elementami, gdy tylko będzie to trywialne, aby zaktualizować interfejs użytkownika, gdy stan się zmienia i odwrotnie.

Aby współdzielić dane między elementami, możesz utworzyć klasę StateObserver i rozszerzyć z niej komponenty sieciowe. Minimalna implementacja wygląda mniej więcej tak:

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

bawić się tutaj

Podoba mi się to podejście, ponieważ:

  • brak przejścia do domu, aby znaleźć data-właściwości
  • brak Object.observe (przestarzałe)
  • brak serwera proxy (który zapewnia hak, ale i tak nie ma mechanizmu komunikacji)
  • brak zależności (innych niż wielokrotne wypełnianie w zależności od docelowych przeglądarek)
  • jest dość scentralizowany i modułowy ... opisuje stan w HTML, a słuchacze na całym świecie bardzo szybko się popsują.
  • jest rozszerzalny. Ta podstawowa implementacja składa się z 20 linii kodu, ale można łatwo zbudować wygodę, niezmienność i magię kształtu stanu, aby ułatwić pracę.
Mr5o1
źródło