Implementieren der DOM-Datenbindung in JavaScript


244

Bitte behandeln Sie diese Frage als streng lehrreich. Ich bin immer noch daran interessiert, neue Antworten und Ideen zu hören, um dies umzusetzen

tl; dr

Wie würde ich die bidirektionale Datenbindung mit JavaScript implementieren?

Datenbindung an das DOM

Mit Datenbindung an das DOM meine ich beispielsweise ein JavaScript-Objekt amit einer Eigenschaft b. Dann ein <input>DOM-Element haben (zum Beispiel), wenn sich das DOM-Element ändert, aändert und umgekehrt (das heißt, ich meine bidirektionale Datenbindung).

Hier ist ein Diagramm von AngularJS, wie dies aussieht:

bidirektionale Datenbindung

Im Grunde habe ich JavaScript ähnlich wie:

var a = {b:3};

Dann ein Eingabeelement (oder ein anderes Formularelement) wie:

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

Ich möchte, dass der Wert der Eingabe a.b(zum Beispiel) der Wert ist, und wenn sich der Eingabetext ändert, möchte ich auch a.bändern. Bei a.bÄnderungen in JavaScript ändert sich die Eingabe.

Die Frage

Was sind einige grundlegende Techniken, um dies in einfachem JavaScript zu erreichen?

Im Einzelnen möchte ich eine gute Antwort auf:

  • Wie würde das Binden für Objekte funktionieren?
  • Wie könnte es funktionieren, Änderungen in der Form zuzuhören?
  • Ist es auf einfache Weise möglich, den HTML-Code nur auf Vorlagenebene ändern zu lassen? Ich möchte die Bindung nicht im HTML-Dokument selbst verfolgen, sondern nur in JavaScript (mit DOM-Ereignissen und JavaScript, das auf die verwendeten DOM-Elemente verweist).

Was habe ich versucht?

Ich bin ein großer Fan von Moustache, also habe ich versucht, es als Vorlage zu verwenden. Beim Versuch, die Datenbindung selbst durchzuführen, sind jedoch Probleme aufgetreten, da Moustache HTML als Zeichenfolge verarbeitet. Nachdem ich das Ergebnis erhalten habe, habe ich keinen Hinweis darauf, wo sich die Objekte in meinem Ansichtsmodell befinden. Die einzige Problemumgehung, die ich mir vorstellen konnte, war das Ändern der HTML-Zeichenfolge (oder des erstellten DOM-Baums) selbst mit Attributen. Es macht mir nichts aus, eine andere Template-Engine zu verwenden.

Grundsätzlich hatte ich das starke Gefühl, das vorliegende Problem zu komplizieren, und es gibt eine einfache Lösung.

Hinweis: Bitte geben Sie keine Antworten an, die externe Bibliotheken verwenden, insbesondere solche mit Tausenden von Codezeilen. Ich habe AngularJS und KnockoutJS verwendet (und mag es!). Ich möchte wirklich keine Antworten in der Form 'benutze Framework x'. Optimalerweise möchte ich einen zukünftigen Leser, der nicht weiß, wie man viele Frameworks verwendet, um zu verstehen, wie man bidirektionale Datenbindung selbst implementiert. Ich erwarte keine vollständige Antwort, sondern eine, die die Idee vermittelt.


2
Ich habe CrazyGlue auf Benjamin Gruenbaums Design basiert . Es unterstützt auch SELECT-, Checkbox- und Radio-Tags. jQuery ist eine Abhängigkeit.
JohnSz

12
Diese Frage ist total genial. Wenn es jemals geschlossen wird, weil es nicht zum Thema gehört oder ein anderer alberner Unsinn ist, werde ich ernsthaft abgehakt.
OCDev

@ JohnSz, danke, dass du dein CrazyGlue-Projekt erwähnt hast. Ich habe lange nach einem einfachen 2-Wege-Datenordner gesucht. Es sieht so aus, als würden Sie Object.observe nicht verwenden, daher sollte Ihre Browserunterstützung großartig sein. Und Sie verwenden keine Schnurrbart-Vorlagen, so dass es perfekt ist.
Gavin

@ Benjamin Was hast du am Ende gemacht?
Johnny

@ Johnny Meiner Meinung nach ist der richtige Ansatz, das DOM in JS (wie React) zu erstellen und nicht umgekehrt. Ich denke, dass wir das irgendwann tun werden.
Benjamin Gruenbaum

Antworten:


106
  • Wie würde das Binden für Objekte funktionieren?
  • Wie könnte es funktionieren, Änderungen in der Form zuzuhören?

Eine Abstraktion, die beide Objekte aktualisiert

Ich nehme an, es gibt andere Techniken, aber letztendlich hätte ich ein Objekt, das auf ein verwandtes DOM-Element verweist und eine Schnittstelle bereitstellt, die Aktualisierungen seiner eigenen Daten und seines verwandten Elements koordiniert.

Das .addEventListener()bietet dafür eine sehr schöne Oberfläche. Sie können ihm ein Objekt geben, das die eventListenerSchnittstelle implementiert , und es ruft seine Handler mit diesem Objekt als thisWert auf.

Auf diese Weise erhalten Sie automatischen Zugriff auf das Element und die zugehörigen Daten.

Definieren Sie Ihr Objekt

Die prototypische Vererbung ist eine gute Möglichkeit, dies zu implementieren, obwohl dies natürlich nicht erforderlich ist. Zuerst würden Sie einen Konstruktor erstellen, der Ihr Element und einige Anfangsdaten empfängt.

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

Hier speichert der Konstruktor also das Element und die Daten zu den Eigenschaften des neuen Objekts. Es bindet auch ein changeEreignis an das Gegebene element. Das Interessante ist, dass es das neue Objekt anstelle einer Funktion als zweites Argument übergibt. Aber das allein wird nicht funktionieren.

Implementierung der eventListenerSchnittstelle

Damit dies funktioniert, muss Ihr Objekt die eventListenerSchnittstelle implementieren . Um dies zu erreichen, muss dem Objekt lediglich eine handleEvent()Methode zugewiesen werden.

Hier kommt die Vererbung ins Spiel.

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

Es gibt viele verschiedene Möglichkeiten, wie dies strukturiert werden kann. Für Ihr Beispiel für die Koordinierung von Aktualisierungen habe ich mich jedoch dafür entschieden, dass die change()Methode nur einen Wert akzeptiert und handleEventdiesen Wert anstelle des Ereignisobjekts übergibt. Auf diese Weise change()kann das auch ohne Ereignis aufgerufen werden.

Wenn das changeEreignis eintritt, werden sowohl das Element als auch die .dataEigenschaft aktualisiert . Das gleiche passiert, wenn Sie .change()Ihr JavaScript-Programm aufrufen .

Verwenden des Codes

Jetzt erstellen Sie einfach das neue Objekt und lassen es Aktualisierungen durchführen. Aktualisierungen im JS-Code werden in der Eingabe angezeigt, und Änderungsereignisse in der Eingabe sind für den JS-Code sichtbar.

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

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

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


5
+1 Sehr sauberer Ansatz, sehr einfach ausgedrückt und einfach genug, damit die Leute daraus lernen können, viel sauberer als das, was ich hatte. Ein häufiger Anwendungsfall ist die Verwendung von Vorlagen im Code zur Darstellung der Ansichten von Objekten. Ich habe mich gefragt, wie das hier funktionieren könnte. In Engines wie Moustache mache ich etwas Mustache.render(template,object), vorausgesetzt, ich möchte ein Objekt mit der Vorlage synchronisieren (nicht spezifisch für Moustache). Wie würde ich damit umgehen?
Benjamin Gruenbaum

3
@BenjaminGruenbaum: Ich habe keine clientseitigen Vorlagen verwendet, aber ich würde mir vorstellen, dass Moustache eine Syntax zum Identifizieren von Einfügepunkten hat und dass diese Syntax eine Beschriftung enthält. Ich würde also denken, dass die "statischen" Teile der Vorlage in HTML-Blöcke gerendert werden, die in einem Array gespeichert sind, und die dynamischen Teile zwischen diesen Blöcken liegen würden. Dann würden die Beschriftungen an den Einfügepunkten als Objekteigenschaften verwendet. Wenn dann inputeiner dieser Punkte aktualisiert werden soll, gibt es eine Zuordnung von der Eingabe zu diesem Punkt. Ich werde sehen, ob ich ein kurzes Beispiel finden kann.

1
@BenjaminGruenbaum: Hmmm ... Ich habe nicht darüber nachgedacht, wie man zwei verschiedene Elemente sauber koordiniert. Das ist etwas komplizierter als ich zuerst dachte. Ich bin allerdings neugierig, daher muss ich möglicherweise etwas später daran arbeiten. :)

2
Sie werden sehen, dass es einen primären TemplateKonstruktor gibt, der die Analyse durchführt, die verschiedenen MyCtorObjekte enthält und eine Schnittstelle bereitstellt , über die die einzelnen Objekte anhand ihrer Kennung aktualisiert werden können. Lass es mich wissen, falls du fragen hast. :) BEARBEITEN: ... benutze stattdessen diesen Link ... Ich hatte vergessen, dass der Eingabewert alle 10 Sekunden exponentiell anstieg, um JS-Updates zu demonstrieren. Dies begrenzt es.

2
... vollständig kommentierte Version plus kleinere Verbesserungen.

36

Also beschloss ich, meine eigene Lösung in den Topf zu werfen. Hier ist eine funktionierende Geige . Beachten Sie, dass dies nur in sehr modernen Browsern ausgeführt wird.

Was es benutzt

Diese Implementierung ist sehr modern - sie erfordert einen (sehr) modernen Browser und Benutzer zwei neue Technologien:

  • MutationObservers , um Änderungen im Dom zu erkennen (Ereignis-Listener werden ebenfalls verwendet)
  • Object.observeum Änderungen im Objekt zu erkennen und den Dom zu benachrichtigen. Gefahr, da diese Antwort geschrieben wurde Oo wurde vom ECMAScript TC diskutiert und dagegen entschieden, erwägen Sie eine Polyfüllung .

Wie es funktioniert

  • Fügen Sie dem Element domAttribute:objAttributebeispielsweise eine Zuordnung hinzubind='textContent:name'
  • Lesen Sie das in der dataBind-Funktion. Beobachten Sie Änderungen sowohl am Element als auch am Objekt.
  • Wenn eine Änderung auftritt - aktualisieren Sie das entsprechende Element.

Die Lösung

Hier ist die dataBindFunktion, beachten Sie, dass es nur 20 Codezeilen sind und kürzer sein könnten:

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

Hier ist eine Verwendung:

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

Hier ist eine funktionierende Geige . Beachten Sie, dass diese Lösung ziemlich allgemein ist. Das Shimmen von Object.observe und Mutation Observer ist verfügbar.


1
Ich habe dies (es5) nur zum Spaß geschrieben, wenn jemand es nützlich findet - schlagen Sie sich aus jsfiddle.net/P9rMm
Benjamin Gruenbaum

1
Denken Sie daran, dass obj.nameein Setter , wenn er vorhanden ist, nicht extern beobachtet werden kann, sondern senden muss, dass er sich innerhalb des Setters geändert hat - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - wirft einen Schraubenschlüssel in die Arbeit für Oo (), wenn Sie ein komplexeres, voneinander abhängiges Verhalten mit Setzern wünschen. Wenn dies obj.namenicht konfigurierbar ist, ist eine Neudefinition des Setters (mit verschiedenen Tricks zum Hinzufügen von Benachrichtigungen) ebenfalls nicht zulässig. Daher werden Generika mit Oo () in diesem speziellen Fall vollständig verschrottet.
Nolo

8
Object.observe wird aus allen Browsern entfernt: caniuse.com/#feat=object-observe
JvdBerg

1
Ein Proxy kann anstelle von Object.observe oder github.com/anywhichway/proxy-observe oder gist.github.com/ebidel/1b553d571f924da2da06 oder den älteren Polyfills verwendet werden, auch auf github @JvdBerg
jimmont

29

Ich möchte meinen Preposter ergänzen. Ich schlage einen etwas anderen Ansatz vor, mit dem Sie Ihrem Objekt einfach einen neuen Wert zuweisen können, ohne eine Methode zu verwenden. Es muss jedoch beachtet werden, dass dies von besonders älteren Browsern nicht unterstützt wird und IE9 weiterhin die Verwendung einer anderen Schnittstelle erfordert.

Am bemerkenswertesten ist, dass mein Ansatz keine Ereignisse nutzt.

Getter und Setter

Mein Vorschlag nutzt das relativ junge Merkmal von Gettern und Setzern , insbesondere nur Setzern. Im Allgemeinen können wir mit Mutatoren das Verhalten anpassen, mit dem bestimmten Eigenschaften ein Wert zugewiesen und abgerufen wird.

Eine Implementierung, die ich hier verwenden werde, ist die Object.defineProperty- Methode. Es funktioniert in FireFox, GoogleChrome und - glaube ich - IE9. Ich habe andere Browser nicht getestet, aber da dies nur Theorie ist ...

Auf jeden Fall werden drei Parameter akzeptiert. Der erste Parameter ist das Objekt, für das Sie eine neue Eigenschaft definieren möchten, der zweite eine Zeichenfolge, die dem Namen der neuen Eigenschaft ähnelt, und der letzte ein "Deskriptorobjekt", das Informationen zum Verhalten der neuen Eigenschaft bereitstellt.

Zwei besonders interessante Deskriptoren sind getund set. Ein Beispiel würde ungefähr so ​​aussehen. Beachten Sie, dass die Verwendung dieser beiden die Verwendung der anderen 4 Deskriptoren verbietet.

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

Jetzt wird es etwas anders, dies zu nutzen:

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

Ich möchte betonen, dass dies nur für moderne Browser funktioniert.

Arbeitsgeige: http://jsfiddle.net/Derija93/RkTMD/1/


2
Wenn wir nur Harmony- ProxyObjekte hätten :) Setter scheinen eine gute Idee zu sein, aber müssten wir dann nicht die tatsächlichen Objekte modifizieren? Nebenbei bemerkt - Object.createkönnte hier verwendet werden (wiederum unter der Annahme eines modernen Browsers, der den zweiten Parameter zulässt). Der Setter / Getter kann auch verwendet werden, um einen anderen Wert als das Objekt und das DOM-Element zu "projizieren" :). Ich frage mich, ob Sie auch Einblicke in das Templating haben, das scheint hier eine echte Herausforderung zu sein, insbesondere um gut zu strukturieren :)
Benjamin Gruenbaum

Genau wie mein Preposter arbeite auch ich nicht viel mit clientseitigen Template-Engines, sorry. :( Aber was meinst du mit Modifizieren der tatsächlichen Objekte ? Und ich würde gerne deine Gedanken darüber verstehen, wie du verstehen musstest , dass der Setter / Getter verwendet werden kann, um ... Die Getter / Setter hier werden für nichts verwendet aber alle Eingaben zum und Abrufen vom Objekt zum DOM-Element umzuleiten, im Grunde wie a Proxy, wie Sie sagten .;) Ich verstand die Herausforderung darin, zwei unterschiedliche Eigenschaften synchron zu halten. Meine Methode eliminiert eines von beiden.
Kiruse

A Proxywürde die Notwendigkeit der Verwendung von Gettern / Setzern beseitigen, Sie könnten Elemente binden, ohne zu wissen, welche Eigenschaften sie haben. Was ich damit gemeint habe, ist, dass die Getter mehr ändern können als bindTo.value, sie können Logik (und vielleicht sogar eine Vorlage) enthalten. Die Frage ist, wie diese Art der bidirektionalen Bindung unter Berücksichtigung einer Vorlage aufrechterhalten werden kann. Nehmen wir an, ich ordne mein Objekt einem Formular zu. Ich möchte sowohl das Element als auch das Formular synchron halten und frage mich, wie ich mit so etwas umgehen soll. Sie können zum Beispiel überprüfen, wie das auf Knockout funktioniert. Learn.knockoutjs.com/#/?tutorial=intro
Benjamin Gruenbaum

@BenjaminGruenbaum Gotcha. Ich werde es mir ansehen.
Kiruse

@BenjaminGruenbaum Ich sehe, was du zu verstehen versuchst. Das Einrichten mit Vorlagen ist etwas schwieriger. Ich werde eine Weile an diesem Skript arbeiten (und es kontinuierlich neu starten). Aber jetzt mache ich eine Pause. Ich habe eigentlich nicht ganz die Zeit dafür.
Kiruse

7

Ich denke, meine Antwort wird technischer sein, aber nicht anders, da die anderen dasselbe mit unterschiedlichen Techniken präsentieren.
Als Erstes besteht die Lösung für dieses Problem in der Verwendung eines als "Beobachter" bezeichneten Entwurfsmusters. Sie können Ihre Daten von Ihrer Präsentation entkoppeln und die Änderung in einer Sache an ihre Zuhörer senden, aber in diesem Fall Es ist in beide Richtungen gemacht.

Für den DOM zu JS Weg

Um die Daten aus dem DOM an das js-Objekt zu binden, können Sie Markups in Form von dataAttributen (oder Klassen, wenn Sie Kompatibilität benötigen) wie folgt hinzufügen :

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

Auf diese Weise kann über js mit querySelectorAll(oder dem alten Freund getElementsByClassNameaus Kompatibilitätsgründen) darauf zugegriffen werden .

Jetzt können Sie das Ereignis, das die Änderungen abhört, auf folgende Weise binden: einen Listener pro Objekt oder einen großen Listener an den Container / das Dokument. Durch die Bindung an das Dokument / den Container wird das Ereignis für jede Änderung ausgelöst, die an dem Dokument oder dem untergeordneten Dokument vorgenommen wurde. Es hat einen geringeren Speicherbedarf, führt jedoch zu Ereignisaufrufen.
Der Code sieht ungefähr so ​​aus:

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

Für den JS do DOM Weg

Sie benötigen zwei Dinge: Ein Metaobjekt, das die Referenzen des Hex-DOM-Elements enthält, ist an jedes js-Objekt / Attribut gebunden und bietet eine Möglichkeit, Änderungen in Objekten abzuhören. Grundsätzlich ist es genauso: Sie müssen eine Möglichkeit haben, Änderungen im Objekt abzuhören und es dann an den DOM-Knoten zu binden, da Ihr Objekt keine Metadaten haben kann. Sie benötigen ein anderes Objekt, das Metadaten in gewisser Weise enthält dass der Eigenschaftsname den Eigenschaften des Metadatenobjekts zugeordnet ist. Der Code wird ungefähr so ​​aussehen:

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

Ich hoffe, dass ich geholfen habe.


Gibt es kein Vergleichbarkeitsproblem mit der Verwendung des .observers?
Mohsen Shakiba

Für den Moment benötigt es eine Unterlegscheibe oder eine Polyfüllung, Object.observeda die Unterstützung derzeit nur in Chrom vorhanden ist. caniuse.com/#feat=object-observe
madcampos

9
Object.observe ist tot. Ich dachte nur, ich würde das hier bemerken.
Benjamin Gruenbaum

@BenjaminGruenbaum Was ist jetzt das Richtige, da dies tot ist?
Johnny

1
@ Johnny, wenn ich mich nicht irre, wären es Proxy-Fallen, da sie eine genauere Kontrolle darüber ermöglichen, was ich mit einem Objekt tun kann, aber ich muss das untersuchen.
Madcampos

7

Gestern habe ich angefangen, meinen eigenen Weg zu schreiben, um Daten zu binden.

Es ist sehr lustig damit zu spielen.

Ich finde es schön und sehr nützlich. Zumindest bei meinen Tests mit Firefox und Chrome muss Edge auch funktionieren. Ich bin mir bei anderen nicht sicher, aber wenn sie Proxy unterstützen, wird es funktionieren.

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

Hier ist der Code:

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

Dann, um zu setzen, einfach:

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

Im Moment habe ich gerade die HTMLInputElement-Wertebindung hinzugefügt.

Lassen Sie mich wissen, wenn Sie wissen, wie Sie es verbessern können.


6

Es gibt eine sehr einfache Barebone-Implementierung der 2-Wege-Datenbindung in diesem Link "Einfache Zwei- Wege-Datenbindung in JavaScript".

Der vorherige Link führte zusammen mit Ideen von knockoutjs, backbone.js und agility.js zu diesem leichten und schnellen MVVM-Framework, ModelView.js basierend auf jQuery das spielt gut mit jQuery und von dem ich der bescheidene (oder vielleicht nicht so bescheidene) Autor bin.

Beispielcode unten reproduzieren (vom Blog-Post-Link ):

Beispielcode für 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;
}

Für das JavaScript-Objekt könnte eine minimale Implementierung eines Benutzermodells für dieses Experiment Folgendes sein:

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

Wenn wir nun die Eigenschaft eines Modells an einen Teil der Benutzeroberfläche binden möchten, müssen wir nur ein entsprechendes Datenattribut für das entsprechende HTML-Element festlegen:

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

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

Während dieser Link die Frage beantworten kann, ist es besser, die wesentlichen Teile der Antwort hier aufzunehmen und den Link als Referenz bereitzustellen. Nur-Link-Antworten können ungültig werden, wenn sich die verknüpfte Seite ändert.
Sam Hanley

@sphanley, bemerkt, ich werde wahrscheinlich aktualisieren, wenn ich mehr Zeit habe, da es ein ziemlich langer Code für einen Antwortbeitrag ist
Nikos M.

@sphanley, reproduzierter Beispielcode bei Antwort von einem Link, auf den verwiesen wird (obwohl ich denke, dass dies ohnehin die meiste Zeit Duplicarte-Inhalte erzeugt)
Nikos M.

1
Es werden definitiv doppelte Inhalte erstellt, aber das ist der Punkt - Blog-Links können oft mit der Zeit brechen, und durch Duplizieren der relevanten Inhalte hier wird sichergestellt, dass sie für zukünftige Leser verfügbar und nützlich sind. Die Antwort sieht jetzt gut aus!
Sam Hanley

3

Das Ändern des Werts eines Elements kann ein DOM-Ereignis auslösen . Listener, die auf Ereignisse reagieren, können zum Implementieren der Datenbindung in JavaScript verwendet werden.

Beispielsweise:

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

Hier finden Sie Code und eine Demo, die zeigt, wie DOM-Elemente miteinander oder mit einem JavaScript-Objekt verknüpft werden können.


3

Binden Sie alle HTML-Eingaben

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

Definieren Sie zwei Funktionen:

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

Verwenden Sie die Funktionen:

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

3

Hier ist eine Idee, mit Object.definePropertyder der Zugriff auf eine Eigenschaft direkt geändert wird.

Code:

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

Verwendung:

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

p.variable="yes"

Geige: Hier


2

Ich habe einige grundlegende Javascript-Beispiele mit onkeypress- und onchange-Ereignishandlern durchgearbeitet, um eine verbindliche Ansicht für unsere js und js zu erstellen

Hier Beispiel 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

Eine einfache Möglichkeit, eine Variable an eine Eingabe zu binden (bidirektionale Bindung), besteht darin, direkt auf das Eingabeelement im Getter und Setter zuzugreifen:

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

Und zu verwenden:

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


Eine schickere Art, das oben Genannte ohne Getter / Setter zu tun:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Benutzen:

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

1

Es ist eine sehr einfache bidirektionale Datenbindung in Vanille-Javascript ....

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

<div id="name">

</div>


2
würde das sicher nur mit dem onkeyup event funktionieren? Wenn Sie also eine Ajax-Anfrage gestellt und dann das innerHTML über JavaScript geändert haben, würde dies nicht funktionieren
Zach Smith

1

Spät zur Party, besonders da ich vor Monaten / Jahren 2 Bibliotheken geschrieben habe, werde ich sie später erwähnen, aber sie sehen für mich immer noch relevant aus. Um es wirklich kurz zu machen, sind die Technologien meiner Wahl:

  • Proxy zur Beobachtung des Modells
  • MutationObserver für die Verfolgung von Änderungen von DOM (aus verbindlichen Gründen, keine Wertänderungen)
  • Wertänderungen (Ansicht zum Modellfluss) werden über reguläre addEventListenerHandler behandelt

IMHO ist es zusätzlich zum OP wichtig, dass die Implementierung der Datenbindung:

  • Behandeln Sie verschiedene Fälle des App-Lebenszyklus (zuerst HTML, dann JS, zuerst JS, dann HTML, Änderung dynamischer Attribute usw.)
  • erlauben tiefe Bindung des Modells, so dass man binden kann user.address.block
  • Arrays als Modell sollten richtig (unterstützt werden shift, spliceund gleichermaßen)
  • Behandle ShadowDOM
  • Versuchen Sie, den Austausch von Technologien so einfach wie möglich zu gestalten. Daher sind alle Vorlagen-Subsprachen ein nicht zukunftsorientierter Ansatz, da sie zu stark mit dem Framework gekoppelt sind

Unter Berücksichtigung all dieser Faktoren ist es meiner Meinung nach unmöglich, nur ein paar Dutzend JS-Zeilen zu werfen. Ich habe versucht, es eher als Muster als als lib zu machen - hat bei mir nicht funktioniert.

Als nächstes Object.observewird das Entfernen entfernt, und dennoch ist die Beobachtung des Modells ein entscheidender Teil - dieser ganze Teil MUSS von einer anderen Bibliothek getrennt sein. Nun zu den Prinzipien, wie ich dieses Problem aufgenommen habe - genau so, wie OP gefragt hat:

Modell (JS-Teil)

Meine Einstellung zur Modellbeobachtung ist Proxy , es ist der einzig vernünftige Weg, es zum Laufen zu bringen, IMHO. Voll ausgestattet obserververdient eine eigene Bibliothek, daher habe ich eine object-observerBibliothek nur für diesen Zweck entwickelt.

Die Modelle sollten über eine dedizierte API registriert werden. Dies ist der Punkt, an dem POJOs zu Observables werden. Hier kann keine Verknüpfung angezeigt werden. Die DOM-Elemente, die als gebundene Ansichten betrachtet werden (siehe unten), werden zuerst und dann bei jeder Datenänderung mit den Werten des Modells / der Modelle aktualisiert.

Ansichten (HTML-Teil)

IMHO, der sauberste Weg, um die Bindung auszudrücken, ist über Attribute. Viele haben dies vorher getan und viele werden es danach tun, also keine Neuigkeiten hier, dies ist nur ein richtiger Weg, dies zu tun. In meinem Fall habe ich die folgende Syntax gewählt : <span data-tie="modelKey:path.to.data => targerProperty"></span>, aber das ist weniger wichtig. Was ist wichtig für mich, keine komplexe Skriptsyntax im HTML - das ist falsch, wieder, IMHO.

Alle Elemente, die als gebundene Ansichten bezeichnet werden, werden zunächst gesammelt. Von der Leistungsseite aus scheint es mir unvermeidlich, eine interne Zuordnung zwischen den Modellen und den Ansichten zu verwalten. Dies scheint ein richtiger Fall zu sein, in dem Speicher + eine gewisse Verwaltung geopfert werden sollte, um Laufzeitsuchen und -aktualisierungen zu speichern.

Die Ansichten werden zunächst vom Modell aus aktualisiert, sofern verfügbar, und bei späteren Modelländerungen, wie bereits erwähnt. Noch mehr sollte das gesamte DOM mittels beobachtet werden, MutationObserverum auf die dynamisch hinzugefügten / entfernten / geänderten Elemente zu reagieren (binden / entbinden). Darüber hinaus sollte all dies in das ShadowDOM repliziert werden (offenes natürlich), um keine ungebundenen schwarzen Löcher zu hinterlassen.

Die Liste der Einzelheiten mag zwar noch weiter gehen, aber dies sind meiner Meinung nach die Hauptprinzipien, die dazu geführt haben, dass die Datenbindung mit einem ausgewogenen Verhältnis zwischen Vollständigkeit der Funktionen von der einen und vernünftiger Einfachheit von der anderen Seite implementiert wurde.

Und so object-observerhabe ich zusätzlich zu den oben genannten auch tatsächlich eine data-tierBibliothek geschrieben, die die Datenbindung entlang der oben genannten Konzepte implementiert.


0

Die Dinge haben sich in den letzten 7 Jahren sehr verändert. Wir haben jetzt native Webkomponenten in den meisten Browsern. IMO ist der Kern des Problems das Teilen des Status zwischen Elementen, sobald Sie das haben, ist es trivial, die Benutzeroberfläche zu aktualisieren, wenn sich der Status ändert und umgekehrt.

Um Daten zwischen Elementen auszutauschen, können Sie eine StateObserver-Klasse erstellen und Ihre Webkomponenten daraus erweitern. Eine minimale Implementierung sieht ungefähr so ​​aus:

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

hier fummeln

Ich mag diesen Ansatz, weil:

  • Keine Dom-Durchquerung, um data-Eigenschaften zu finden
  • nein Object.observe (veraltet)
  • kein Proxy (der einen Hook bietet, aber trotzdem keinen Kommunikationsmechanismus)
  • Keine Abhängigkeiten (außer einer Polyfüllung, abhängig von Ihren Zielbrowsern)
  • Es ist einigermaßen zentralisiert und modular aufgebaut. Es beschreibt den Status in HTML, und Hörer überall zu haben, würde sehr schnell chaotisch werden.
  • es ist erweiterbar. Diese grundlegende Implementierung besteht aus 20 Codezeilen, aber Sie können leicht Komfort, Unveränderlichkeit und Zustandsformmagie aufbauen, um die Arbeit zu vereinfachen.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.