Come implementare l'associazione dati DOM in JavaScript


244

Si prega di trattare questa domanda come strettamente educativa. Sono ancora interessato a sentire nuove risposte e idee per implementare questo

tl; dr

Come implementare l'associazione dati bidirezionale con JavaScript?

Collegamento dati al DOM

Per associazione dei dati al DOM intendo ad esempio avere un oggetto JavaScript acon una proprietà b. Quindi avere un <input>elemento DOM (ad esempio), quando l'elemento DOM cambia, acambia e viceversa (cioè intendo l'associazione dati bidirezionale).

Ecco un diagramma di AngularJS su come appare:

associazione dati bidirezionale

Quindi sostanzialmente ho JavaScript simile a:

var a = {b:3};

Quindi un elemento di input (o altra forma) come:

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

Vorrei che il valore dell'input fosse a.bil valore (per esempio), e quando il testo di input cambia, anch'io vorrei a.bcambiare. Quando a.bcambia in JavaScript, l'input cambia.

La domanda

Quali sono alcune tecniche di base per raggiungere questo obiettivo in JavaScript?

In particolare, vorrei una buona risposta per fare riferimento a:

  • Come funzionerebbe l'associazione per gli oggetti?
  • Come potrebbe funzionare l'ascolto del cambiamento nel modulo?
  • È possibile in modo semplice modificare l'HTML solo a livello di modello? Vorrei non tenere traccia dell'associazione nel documento HTML stesso, ma solo in JavaScript (con eventi DOM e JavaScript mantenendo il riferimento agli elementi DOM utilizzati).

Cosa ho provato

Sono un grande fan di Moustache, quindi ho provato ad usarlo per il templating. Tuttavia, ho riscontrato problemi durante il tentativo di eseguire il binding dei dati stesso poiché Moustache elabora l'HTML come una stringa, quindi dopo aver ottenuto il risultato non ho alcun riferimento a dove si trovano gli oggetti nel mio modello di visualizzazione. L'unica soluzione che mi è venuta in mente è stata la modifica della stringa HTML (o della creazione di un albero DOM) con attributi. Non mi dispiace usare un motore di template diverso.

Fondamentalmente, ho avuto la netta sensazione che stavo complicando il problema a portata di mano e c'è una soluzione semplice.

Nota: non fornire risposte che utilizzano librerie esterne, in particolare quelle che sono migliaia di righe di codice. Ho usato (e mi piace!) AngularJS e KnockoutJS. Non voglio davvero risposte nel modulo "usa framework x". In modo ottimale, vorrei un futuro lettore che non sapesse come utilizzare molti framework per capire come implementare da solo il binding bidirezionale dei dati. Non mi aspetto una risposta completa , ma una che dia l'idea.


2
Ho basato CrazyGlue sul design di Benjamin Gruenbaum. Supporta anche i tag SELECT, checkbox e radio. jQuery è una dipendenza.
JohnSz,

12
Questa domanda è assolutamente fantastica. Se mai verrà chiuso per essere off-topic o altre sciocche sciocchezze, sarò seriamente segnato.
OCDev

@JohnSz grazie per aver menzionato il tuo progetto CrazyGlue. Ho cercato per molto tempo un semplice raccoglitore di dati a 2 vie. Sembra che tu non stia usando Object.observe, quindi il supporto del tuo browser dovrebbe essere eccezionale. E non stai usando il modello di baffi, quindi è perfetto.
Gavin,

@Benjamin Che cosa hai fatto?
Johnny,

@johnny secondo me l'approccio corretto è quello di creare il DOM in JS (come React) e non viceversa. Penso che alla fine è quello che faremo.
Benjamin Gruenbaum,

Risposte:


106
  • Come funzionerebbe l'associazione per gli oggetti?
  • Come potrebbe funzionare l'ascolto del cambiamento nel modulo?

Un'astrazione che aggiorna entrambi gli oggetti

Suppongo che ci siano altre tecniche, ma alla fine avrei un oggetto che contiene riferimenti a un elemento DOM correlato e fornisce un'interfaccia che coordina gli aggiornamenti ai propri dati e ai suoi elementi correlati.

Il .addEventListener()fornisce un'interfaccia molto piacevole per questo. Puoi assegnargli un oggetto che implementa l' eventListenerinterfaccia e invocherà i suoi gestori con quell'oggetto come thisvalore.

Ciò consente di accedere automaticamente sia all'elemento sia ai relativi dati.

Definire il tuo oggetto

L'eredità prototipale è un bel modo per implementarlo, anche se ovviamente non è necessario. Per prima cosa crei un costruttore che riceve il tuo elemento e alcuni dati iniziali.

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

Quindi qui il costruttore memorizza l'elemento e i dati sulle proprietà del nuovo oggetto. Inoltre lega un changeevento al dato element. La cosa interessante è che passa il nuovo oggetto invece di una funzione come secondo argomento. Ma questo da solo non funzionerà.

Implementazione eventListenerdell'interfaccia

Per farlo funzionare, il tuo oggetto deve implementare l' eventListenerinterfaccia. Tutto ciò che serve per raggiungere questo obiettivo è fornire all'oggetto un handleEvent()metodo.

Ecco dove entra in gioco l'eredità.

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

Esistono molti modi in cui questo può essere strutturato, ma per il tuo esempio di coordinamento degli aggiornamenti, ho deciso di fare in modo che il change()metodo accetti solo un valore e abbia il handleEventpassaggio quel valore anziché l'oggetto evento. In questo modo è change()possibile invocare anche senza un evento.

Quindi ora, quando si verifica l' changeevento, aggiornerà sia l'elemento che la .dataproprietà. E lo stesso accadrà quando chiami .change()nel tuo programma JavaScript.

Usando il codice

Ora dovresti semplicemente creare il nuovo oggetto e lasciarlo eseguire gli aggiornamenti. Gli aggiornamenti nel codice JS appariranno sull'input e gli eventi di modifica sull'input saranno visibili sul codice JS.

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

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

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


5
+1 Approccio molto pulito, molto semplice e abbastanza semplice per le persone da cui imparare, molto più pulito di quello che avevo. Un caso d'uso comune sta usando i template nel codice per rappresentare le viste degli oggetti. Mi chiedevo come potesse funzionare qui? In motori come Moustache faccio qualcosa Mustache.render(template,object), supponendo di voler mantenere un oggetto sincronizzato con il modello (non specifico di Moustache), come potrei procedere?
Benjamin Gruenbaum,

3
@BenjaminGruenbaum: non ho usato modelli lato client, ma immagino che Moustache abbia una sintassi per identificare i punti di inserimento e che quella sintassi includa un'etichetta. Quindi penserei che le parti "statiche" del modello verrebbero trasformate in blocchi di HTML archiviati in un array e le parti dinamiche andrebbero tra quei blocchi. Quindi le etichette sui punti di inserimento verranno utilizzate come proprietà dell'oggetto. Quindi, se alcuni inputdevono aggiornare uno di quei punti, ci sarebbe una mappatura dall'input a quel punto. Vedrò se riesco a trovare un rapido esempio.

1
@BenjaminGruenbaum: Hmmm ... Non ho pensato a come coordinare in modo pulito due diversi elementi. Questo è un po 'più coinvolto di quanto pensassi all'inizio. Sono curioso però, quindi potrei aver bisogno di lavorarci un po 'più tardi. :)

2
Vedrai che esiste un Templatecostruttore principale che esegue l'analisi, contiene i diversi MyCtoroggetti e fornisce un'interfaccia su cui aggiornare ciascuno tramite il suo identificatore. Fammi sapere se hai domande. :) EDIT: ... usa questo link invece ... Avevo dimenticato di avere un aumento esponenziale del valore di input ogni 10 secondi per dimostrare gli aggiornamenti di JS. Questo lo limita.

2
... versione completamente commentata più piccoli miglioramenti.

36

Quindi, ho deciso di gettare la mia soluzione nel piatto. Ecco un violino funzionante . Nota che funziona solo su browser molto moderni.

Quello che usa

Questa implementazione è molto moderna - richiede un browser (molto) moderno e agli utenti due nuove tecnologie:

  • MutationObservers per rilevare le modifiche nel dom (vengono utilizzati anche i listener di eventi)
  • Object.observeper rilevare le modifiche nell'oggetto e avvisare il dom. Pericolo, poiché questa risposta è stata scritta Oo è stata discussa e decisa dall'ECMAScript TC, considera un polyfill .

Come funziona

  • Sull'elemento, inserisci una domAttribute:objAttributemappatura, ad esempiobind='textContent:name'
  • Leggilo nella funzione dataBind. Osservare le modifiche sia all'elemento che all'oggetto.
  • Quando si verifica una modifica, aggiorna l'elemento pertinente.

La soluzione

Ecco la dataBindfunzione, nota che sono solo 20 righe di codice e potrebbero essere più brevi:

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

Ecco un po 'di utilizzo:

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

Ecco un violino funzionante . Nota che questa soluzione è piuttosto generica. È disponibile lo shimming di Object.observe e dell'osservatore di mutazione.


1
Mi è capitato di scrivere questo (es5) per divertimento, se qualcuno lo trova utile - buttati
Benjamin Gruenbaum

1
Tieni presente che quando obj.nameha un setter non può essere osservato esternamente, ma deve trasmettere che è cambiato dall'interno del setter - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - kinda lancia una chiave inglese nelle opere per Oo () se si desidera un comportamento più complesso e interdipendente utilizzando i setter. Inoltre, quando obj.namenon è configurabile, non è consentito ridefinire il suo setter (con vari trucchi per aggiungere la notifica), quindi i generici con Oo () sono totalmente eliminati in quel caso specifico.
Nolo,

8
Object.observe viene rimosso da tutti i browser: caniuse.com/#feat=object-observe
JvdBerg

1
È possibile utilizzare un proxy al posto di Object.observe o github.com/anywhichway/proxy-observe o gist.github.com/ebidel/1b553d571f924da2da06 o i precedenti polyfill, anche su github @JvdBerg
jimmont

29

Vorrei aggiungere al mio preposter. Suggerisco un approccio leggermente diverso che ti permetterà di assegnare semplicemente un nuovo valore al tuo oggetto senza usare un metodo. Va notato però che questo non è supportato da browser particolarmente vecchi e IE9 richiede ancora l'uso di un'interfaccia diversa.

In particolare, il mio approccio non fa uso di eventi.

Getter e setter

La mia proposta si avvale della caratteristica relativamente giovane di getter e setter , in particolare solo setter. In generale, i mutatori ci consentono di "personalizzare" il comportamento di come a determinate proprietà viene assegnato un valore e recuperato.

Un'implementazione che userò qui è il metodo Object.defineProperty . Funziona con FireFox, GoogleChrome e - penso - IE9. Non ho testato altri browser, ma poiché questa è solo teoria ...

Ad ogni modo, accetta tre parametri. Il primo parametro è l'oggetto per il quale si desidera definire una nuova proprietà, il secondo una stringa simile al nome della nuova proprietà e l'ultimo un "oggetto descrittore" che fornisce informazioni sul comportamento della nuova proprietà.

Due descrittori particolarmente interessanti sono gete set. Un esempio sarebbe simile al seguente. Si noti che l'utilizzo di questi due proibisce l'uso degli altri 4 descrittori.

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

Ora sfruttarlo diventa leggermente diverso:

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

Voglio sottolineare che questo funziona solo per i browser moderni.

Violino di lavoro: http://jsfiddle.net/Derija93/RkTMD/1/


2
Se solo avessimo Proxyoggetti Harmony :) I setter sembrano una bella idea, ma ciò non ci imporrebbe di modificare gli oggetti reali? Inoltre, su una nota a margine - Object.createpotrebbe essere utilizzato qui (di nuovo, assumendo il browser moderno che ha consentito il secondo parametro). Inoltre, setter / getter potrebbe essere utilizzato per 'proiettare' un valore diverso sull'oggetto e sull'elemento DOM :). Mi chiedo se anche tu hai qualche idea sul templating, che qui sembra una vera sfida, soprattutto per strutturarsi bene :)
Benjamin Gruenbaum,

Proprio come il mio preposter, anche io non lavoro molto con i motori di template lato client, mi dispiace. :( Ma cosa intendi per modificare gli oggetti reali ? E mi piacerebbe capire i tuoi pensieri su come hai capito che il setter / getter poteva essere usato per ... I getter / setter qui non sono usati per niente ma reindirizzando tutti gli input e i recuperi dall'oggetto all'elemento DOM, sostanzialmente come un Proxy, come hai detto tu;;) Ho capito che la sfida era quella di mantenere sincronizzate due proprietà distinte. Il mio metodo elimina uno di entrambi.
Kiruse,

A Proxyeliminerebbe la necessità di usare getter / setter, potresti legare gli elementi senza sapere quali proprietà hanno. Quello che intendevo dire è che i getter possono cambiare più di bindTo.value e possono contenere la logica (e forse anche un modello). La domanda è: come mantenere questo tipo di associazione bidirezionale tenendo presente un modello? Diciamo che sto mappando il mio oggetto su un modulo, mi piacerebbe mantenere sincronizzati sia l'elemento che il modulo e mi chiedo come andrei avanti su quel genere di cose. Puoi vedere come funziona knockout learn.knockoutjs.com/#/?tutorial=intro per esempio
Benjamin Gruenbaum,

@BenjaminGruenbaum Gotcha. Lo darò un'occhiata.
Kiruse,

@BenjaminGruenbaum Vedo cosa stai cercando di capire. L'impostazione di tutto questo con i modelli in mente risulta essere un po 'più difficile. Lavorerò su questo copione per un po '(e continuamente rebase esso). Ma per ora, mi sto prendendo una pausa. In realtà non ho abbastanza tempo per questo.
Kiruse,

7

Penso che la mia risposta sarà più tecnica, ma non diversa in quanto le altre presentano la stessa cosa usando tecniche diverse.
Quindi, per prima cosa, la soluzione a questo problema è l'uso di un modello di progettazione noto come "osservatore", ti consente di separare i tuoi dati dalla tua presentazione, facendo in modo che il cambiamento in una cosa sia trasmesso ai loro ascoltatori, ma in questo caso è fatto in due direzioni.

Per il modo da DOM a JS

Per associare i dati dal DOM all'oggetto js è possibile aggiungere markup sotto forma di dataattributi (o classi se è necessaria la compatibilità), in questo modo:

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

In questo modo è possibile accedervi tramite js utilizzando querySelectorAll(o il vecchio amico getElementsByClassNameper compatibilità).

Ora puoi associare l'evento ascoltando le modifiche ai modi: un ascoltatore per oggetto o un ascoltatore grande al contenitore / documento. Il collegamento al documento / contenitore attiverà l'evento per ogni modifica apportata in esso o è figlio, avrà un footprint di memoria più piccolo ma genererà chiamate agli eventi.
Il codice sarà simile al seguente:

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

Per il modo JS do DOM

Avrai bisogno di due cose: un meta oggetto che conterrà i riferimenti dell'elemento stregone DOM è associato a ciascun oggetto / attributo js e un modo per ascoltare i cambiamenti negli oggetti. È fondamentalmente allo stesso modo: devi avere un modo per ascoltare i cambiamenti nell'oggetto e poi collegarlo al nodo DOM, poiché il tuo oggetto "non può avere" metadati avrai bisogno di un altro oggetto che contiene metadati in un modo che il nome della proprietà si associa alle proprietà dell'oggetto metadati. Il codice sarà qualcosa del genere:

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

Spero di essere stato di aiuto.


non c'è problema di comparabilità con l'utilizzo di .observer?
Mohsen Shakiba,

per ora ha bisogno di uno spessore o polyfill per Object.observecome supporto è presente solo in Chrome per ora. caniuse.com/#feat=object-observe
madcampos il

9
Object.observe è morto. Ho pensato di notarlo qui.
Benjamin Gruenbaum,

@BenjaminGruenbaum Qual è la cosa corretta da usare ora, poiché è morta?
Johnny,

1
@johnny se non sbaglio sarebbero trappole per procura in quanto consentono un controllo più granulare di cosa posso fare con un oggetto, ma devo investigarlo.
madcampos

7

Ieri ho iniziato a scrivere il mio modo di associare i dati.

È molto divertente giocarci.

Penso che sia bellissimo e molto utile. Almeno nei miei test con Firefox e Chrome, anche Edge deve funzionare. Non sono sicuro degli altri, ma se supportano il proxy, penso che funzionerà.

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

Ecco il codice:

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

Quindi, per impostare, basta:

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

Per ora, ho appena aggiunto il bind del valore HTMLInputElement.

Fammi sapere se sai come migliorarlo.


6

Esiste un'implementazione barebone molto semplice di associazione dati bidirezionale in questo collegamento "Facile associazione dati bidirezionale in JavaScript"

Il link precedente insieme alle idee di knockoutjs, backbone.js e agility.js, ha portato a questo framework MVVM leggero e veloce, ModelView.js basato su jQuery che gioca bene con jQuery e di cui io sono l'autore umile (o forse non così umile).

Riproduzione del codice di esempio riportato di seguito (dal collegamento al post di blog ):

Codice di esempio per 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;
}

Per quanto riguarda l'oggetto JavaScript, un'implementazione minima di un modello utente per il bene di questo esperimento potrebbe essere la seguente:

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

Ora, ogni volta che vogliamo associare la proprietà di un modello a una parte dell'interfaccia utente, dobbiamo solo impostare un attributo di dati appropriato sull'elemento HTML corrispondente:

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

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

Sebbene questo collegamento possa rispondere alla domanda, è meglio includere qui le parti essenziali della risposta e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia.
Sam Hanley,

@sphanley, ho notato, probabilmente aggiornerò quando avrò più tempo, in quanto è un codice piuttosto lungo per un post di risposta
Nikos M.

@sphanley, codice di esempio riprodotto sulla risposta dal link di riferimento (anche se ho creato un thinbk per la maggior parte del tempo, comunque)
Nikos M.

1
Crea sicuramente contenuti duplicati, ma questo è il punto: i collegamenti ai blog possono spesso rompersi nel tempo e duplicando i contenuti pertinenti qui garantisce che siano disponibili e utili per i futuri lettori. La risposta ora è fantastica!
Sam Hanley,

3

La modifica del valore di un elemento può attivare un evento DOM . I listener che rispondono agli eventi possono essere utilizzati per implementare l'associazione dei dati in JavaScript.

Per esempio:

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

Ecco il codice e una demo che mostra come gli elementi DOM possono essere associati tra loro o con un oggetto JavaScript.


3

Associa qualsiasi input HTML

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

definire due funzioni:

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

utilizzare le funzioni:

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

3

Ecco un'idea Object.definePropertyche utilizza che modifica direttamente il modo in cui si accede a una proprietà.

Codice:

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

Uso:

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

p.variable="yes"

violino: qui


2

Ho passato alcuni esempi javascript di base usando i gestori di eventi onkeypress e onchange per rendere la vista vincolante per i nostri js e js per visualizzare

Qui esempio 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

Un modo semplice di associare una variabile a un input (associazione a due vie) è semplicemente accedere direttamente all'elemento di input nel getter e nel 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" />

E usare:

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


Un modo più elaborato di fare quanto sopra senza getter / setter:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Usare:

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

1

È un'associazione di dati bidirezionale molto semplice in javascript alla vaniglia ....

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

<div id="name">

</div>


2
sicuramente questo funzionerebbe solo con l'evento onkeyup? vale a dire se hai fatto una richiesta Ajax e poi hai modificato il metodo HTML interno tramite JavaScript, allora non funzionerebbe
Zach Smith

1

In ritardo alla festa, soprattutto da quando ho scritto 2 librerie relative mesi / anni fa, le menzionerò più avanti, ma mi sembra ancora rilevante. Per rendere lo spoiler davvero breve, le tecnologie di mia scelta sono:

  • Proxy per l'osservazione del modello
  • MutationObserver per il rilevamento delle modifiche di DOM (per motivi vincolanti, non modifiche di valore)
  • le variazioni di valore (vista sul flusso del modello) vengono gestite tramite addEventListenergestori regolari

IMHO, oltre al PO, è importante che l'implementazione dell'associazione dei dati:

  • gestire diversi casi del ciclo di vita delle app (prima HTML, poi JS, JS, poi HTML, modifica degli attributi dinamici ecc.)
  • consentire una profonda rilegatura del modello, in modo che si possa legare user.address.block
  • array come un modello devono essere supportate correttamente ( shift, splicee simili)
  • gestire ShadowDOM
  • cerca di essere il più semplice possibile per la sostituzione della tecnologia, quindi qualsiasi sotto-lingua dei modelli è un approccio favorevole ai cambiamenti non futuri poiché è troppo pesantemente associato al framework

Prendendo in considerazione tutti questi aspetti, secondo me rende impossibile lanciare solo poche dozzine di linee JS. Ho provato a farlo come un modello piuttosto che con la lib - non ha funzionato per me.

Successivamente, averlo Object.observerimosso, e tuttavia dato che l'osservazione del modello è una parte cruciale, l'intera parte DEVE essere separata dalle preoccupazioni in un'altra lib. Ora al punto di principio di come ho preso questo problema - esattamente come OP ha chiesto:

Modello (parte JS)

La mia opinione sull'osservazione del modello è Proxy , è l'unico modo sano per farlo funzionare, IMHO. Le funzionalità complete observermeritano la propria libreria, quindi ho sviluppato una object-observerlibreria per quel solo scopo.

Il / i modello / i dovrebbero essere registrati tramite alcune API dedicate, questo è il punto in cui i POJO si trasformano in Observables, non possono vedere alcun collegamento qui. Gli elementi DOM che sono considerati viste associate (vedere di seguito), vengono inizialmente aggiornati con i valori del / i modello / i, quindi a ogni modifica dei dati.

Viste (parte HTML)

IMHO, il modo più pulito per esprimere l'associazione, è tramite attributi. Molti lo hanno fatto prima e molti lo faranno dopo, quindi nessuna notizia qui, questo è solo un modo giusto per farlo. Nel mio caso sono andato con la seguente sintassi:, <span data-tie="modelKey:path.to.data => targerProperty"></span>ma questo è meno importante. Ciò che è importante per me, nessuna sintassi di scripting complessa in HTML - questo è di nuovo sbagliato, IMHO.

Tutti gli elementi designati come viste rilegate devono essere raccolti all'inizio. Mi sembra inevitabile dal punto di vista delle prestazioni gestire un po 'di mapping interno tra i modelli e le viste, sembra un caso giusto in cui la memoria + un po' di gestione dovrebbe essere sacrificata per salvare le ricerche e gli aggiornamenti di runtime.

Le viste vengono inizialmente aggiornate dal modello, se disponibile e in seguito a modifiche del modello, come abbiamo detto. Inoltre, l'intero DOM dovrebbe essere osservato MutationObserverper reagire (legare / separare) gli elementi aggiunti / rimossi / modificati dinamicamente. Inoltre, tutto questo, dovrebbe essere replicato in ShadowDOM (uno aperto, ovviamente) per non lasciare buchi neri non associati.

L'elenco di specifiche potrebbe andare oltre, ma quelli sono secondo me i principali principi che renderebbero implementata l'associazione dei dati con un buon equilibrio tra completezza delle funzionalità da una e sana semplicità dall'altra parte.

E così, oltre a quanto object-observersopra menzionato, ho scritto davvero anche una data-tierlibreria, che implementa l'associazione dei dati lungo i concetti sopra menzionati.


0

Le cose sono cambiate molto negli ultimi 7 anni, ora abbiamo componenti web nativi nella maggior parte dei browser. IMO il nocciolo del problema è la condivisione dello stato tra gli elementi, una volta che è banale aggiornare l'interfaccia utente quando lo stato cambia e viceversa.

Per condividere i dati tra elementi è possibile creare una classe StateObserver ed estenderne i componenti Web. Un'implementazione minima è simile a questa:

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

violino qui

Mi piace questo approccio perché:

  • nessun attraversamento dom per trovare data-proprietà
  • no Object.observe (obsoleto)
  • nessun proxy (che fornisce un hook ma nessun meccanismo di comunicazione comunque)
  • nessuna dipendenza, (diverso da un polyfill a seconda dei browser di destinazione)
  • è ragionevolmente centralizzato e modulare ... descrivendo lo stato in html e avere ascoltatori dappertutto diventerebbe molto disordinato.
  • è estensibile. Questa implementazione di base è composta da 20 righe di codice, ma potresti facilmente creare un po 'di praticità, immutabilità e forma magica per semplificare il lavoro.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.