Cómo implementar el enlace de datos DOM en JavaScript

244

Por favor trate esta pregunta como estrictamente educativa. Todavía estoy interesado en escuchar nuevas respuestas e ideas para implementar esto

tl; dr

¿Cómo implementaría el enlace de datos bidireccional con JavaScript?

Enlace de datos al DOM

Por enlace de datos al DOM me refiero, por ejemplo, a tener un objeto JavaScript acon una propiedad b. Luego tener un <input>elemento DOM (por ejemplo), cuando el elemento DOM cambia, acambia y viceversa (es decir, quiero decir enlace de datos bidireccional).

Aquí hay un diagrama de AngularJS sobre cómo se ve esto:

enlace de datos bidireccional

Así que básicamente tengo JavaScript similar a:

var a = {b:3};

Luego, un elemento de entrada (u otra forma) como:

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

Me gustaría que el valor de entrada sea a.bel valor de (por ejemplo), y cuando el texto de entrada cambia, también me gustaría a.bcambiarlo. Cuando a.bcambia en JavaScript, la entrada cambia.

La pregunta

¿Cuáles son algunas técnicas básicas para lograr esto en JavaScript simple?

En concreto, me gustaría una buena respuesta para referirme a:

  • ¿Cómo funcionaría el enlace para los objetos?
  • ¿Cómo podría funcionar escuchar el cambio en la forma?
  • ¿Es posible, de una manera simple, modificar solo el HTML en el nivel de plantilla? Me gustaría no hacer un seguimiento del enlace en el documento HTML en sí, sino solo en JavaScript (con eventos DOM y JavaScript manteniendo referencia a los elementos DOM utilizados).

Que he probado

Soy un gran fanático del bigote, así que intenté usarlo para crear plantillas. Sin embargo, me encontré con problemas al intentar realizar el enlace de datos en sí, ya que Moustache procesa HTML como una cadena, por lo que después de obtener su resultado no tengo referencia a dónde están los objetos en mi modelo de vista. La única solución que pude pensar para esto fue modificar la cadena HTML (o el árbol DOM creado) con atributos. No me importa usar un motor de plantillas diferente.

Básicamente, tuve la fuerte sensación de que estaba complicando el problema en cuestión y hay una solución simple.

Nota: No proporcione respuestas que utilicen bibliotecas externas, especialmente aquellas que son miles de líneas de código. He usado (¡y me gusta!) AngularJS y KnockoutJS. Realmente no quiero respuestas en la forma 'use framework x'. De manera óptima, me gustaría un futuro lector que no sepa cómo usar muchos marcos para comprender cómo implementar el enlace de datos bidireccional. No espero una respuesta completa , pero sí una que transmita la idea.

Benjamin Gruenbaum
fuente
2
Basé CrazyGlue en el diseño de Benjamin Gruenbaum. También es compatible con SELECCIONAR, casilla de verificación y etiquetas de radio. jQuery es una dependencia.
JohnSz
12
Esta pregunta es totalmente asombrosa. Si alguna vez se cierra por estar fuera de tema o alguna otra tontería tonta, voy a estar seriamente molesto.
OCDev
@JohnSz gracias por mencionar su proyecto CrazyGlue. He estado buscando un simple archivador de datos bidireccional durante mucho tiempo. Parece que no estás utilizando Object.observe, por lo que el soporte de tu navegador debería ser excelente. Y no estás usando plantillas de bigote, así que es perfecto.
Gavin
@Benjamin ¿Qué terminaste haciendo?
Johnny
@johnny en mi opinión, el enfoque correcto es crear el DOM en JS (como React) y no al revés. Creo que eventualmente eso es lo que haremos.
Benjamin Gruenbaum

Respuestas:

106
  • ¿Cómo funcionaría el enlace para los objetos?
  • ¿Cómo podría funcionar escuchar el cambio en la forma?

Una abstracción que actualiza ambos objetos.

Supongo que hay otras técnicas, pero en última instancia, tendría un objeto que tiene referencia a un elemento DOM relacionado y proporciona una interfaz que coordina las actualizaciones de sus propios datos y su elemento relacionado.

El .addEventListener()proporciona una interfaz muy agradable para esto. Puede darle un objeto que implemente la eventListenerinterfaz, e invocará a sus controladores con ese objeto como thisvalor.

Esto le brinda acceso automático tanto al elemento como a sus datos relacionados.

Definiendo tu objeto

La herencia de prototipos es una buena manera de implementar esto, aunque no es obligatorio, por supuesto. Primero crearía un constructor que reciba su elemento y algunos datos iniciales.

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

Entonces, aquí el constructor almacena el elemento y los datos en las propiedades del nuevo objeto. También vincula un changeevento a lo dado element. Lo interesante es que pasa el nuevo objeto en lugar de una función como segundo argumento. Pero esto solo no funcionará.

Implementando la eventListenerinterfaz

Para que esto funcione, su objeto necesita implementar la eventListenerinterfaz. Todo lo que se necesita para lograr esto es darle al objeto un handleEvent()método.

Ahí es donde entra la herencia.

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

Hay muchas formas diferentes en que esto podría estructurarse, pero para su ejemplo de coordinación de actualizaciones, decidí hacer que el change()método solo acepte un valor y que handleEventpase ese valor en lugar del objeto de evento. De esta manera, change()se puede invocar sin un evento también.

Entonces, cuando changeocurra el evento, actualizará tanto el elemento como la .datapropiedad. Y lo mismo sucederá cuando llame .change()a su programa JavaScript.

Usando el código

Ahora simplemente crearía el nuevo objeto y dejaría que realizara actualizaciones. Las actualizaciones en el código JS aparecerán en la entrada, y los eventos de cambio en la entrada serán visibles para el código 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/

usuario1106925
fuente
55
+1 Enfoque muy limpio, muy simple y lo suficientemente simple para que la gente aprenda, mucho más limpio que lo que tenía. Un caso de uso común es usar plantillas en código para representar las vistas de los objetos. Me preguntaba cómo podría funcionar esto aquí. En motores como Moustache, hago algo Mustache.render(template,object), suponiendo que quiero mantener un objeto sincronizado con la plantilla (no específico para Moustache), ¿cómo continuaría con eso?
Benjamin Gruenbaum
3
@BenjaminGruenbaum: No he usado plantillas del lado del cliente, pero me imagino que Moustache tiene alguna sintaxis para identificar puntos de inserción, y que esa sintaxis incluye una etiqueta. Por lo tanto, creo que las partes "estáticas" de la plantilla se representarían en fragmentos de HTML almacenados en una matriz, y las partes dinámicas irían entre esos fragmentos. Luego, las etiquetas en los puntos de inserción se usarían como propiedades del objeto. Entonces, si alguno inputva a actualizar uno de esos puntos, habrá una asignación de la entrada a ese punto. Veré si puedo encontrar un ejemplo rápido.
1
@BenjaminGruenbaum: Hmmm ... No he pensado en cómo coordinar limpiamente dos elementos diferentes. Esto es un poco más complicado de lo que pensé al principio. Sin embargo, tengo curiosidad, por lo que es posible que necesite trabajar en esto un poco más tarde. :)
2
Verá que hay un Templateconstructor primario que realiza el análisis, contiene los diferentes MyCtorobjetos y proporciona una interfaz para actualizar cada uno por su identificador. Déjeme saber si usted tiene preguntas. :) EDITAR: ... use este enlace en su lugar ... Había olvidado que tenía un aumento exponencial en el valor de entrada cada 10 segundos para demostrar las actualizaciones de JS. Esto lo limita.
2
... versión completamente comentada más mejoras menores.
36

Entonces, decidí tirar mi propia solución en la olla. Aquí hay un violín que funciona . Tenga en cuenta que esto solo se ejecuta en navegadores muy modernos.

Que utiliza

Esta implementación es muy moderna: requiere un navegador (muy) moderno y los usuarios dos nuevas tecnologías:

  • MutationObservers para detectar cambios en el dom (también se utilizan oyentes de eventos)
  • Object.observepara detectar cambios en el objeto y notificar al dom. Peligro, dado que esta respuesta se ha escrito Oo ha sido discutido y decidido por el ECMAScript TC, considere un polyfill .

Cómo funciona

  • En el elemento, coloque una domAttribute:objAttributeasignación, por ejemplobind='textContent:name'
  • Lea eso en la función dataBind. Observe los cambios tanto en el elemento como en el objeto.
  • Cuando se produce un cambio, actualice el elemento relevante.

La solución

Aquí está la dataBindfunción, tenga en cuenta que solo son 20 líneas de código y podrían ser más cortas:

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

Aquí hay algunos usos:

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

Aquí hay un violín que funciona . Tenga en cuenta que esta solución es bastante genérica. Object.observe y mutation observador shimming está disponible.

Benjamin Gruenbaum
fuente
1
Acabo de escribir esto (es5) por diversión, si alguien lo encuentra útil - noqueos jsfiddle.net/P9rMm
Benjamin Gruenbaum
1
Tenga en cuenta que cuando obj.nametiene un setter no se puede observar externamente, pero debe transmitir que ha cambiado desde dentro del setter - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - un poco arroja una llave en las obras para Oo () si desea un comportamiento más complejo e interdependiente utilizando setters. Además, cuando obj.nameno es configurable, tampoco se permite redefinir su configurador (con varios trucos para agregar notificaciones), por lo que los genéricos con Oo () se eliminan por completo en ese caso específico.
Nolo
8
Object.observe se elimina de todos los navegadores: caniuse.com/#feat=object-observe
JvdBerg
1
Se puede usar un Proxy en lugar de Object.observe, o github.com/anywhichway/proxy-observe o gist.github.com/ebidel/1b553d571f924da2da06 o los polyfills más antiguos, también en github @JvdBerg
jimmont
29

Me gustaría agregar a mi preposter. Sugiero un enfoque ligeramente diferente que le permitirá simplemente asignar un nuevo valor a su objeto sin utilizar un método. Sin embargo, debe tenerse en cuenta que esto no es compatible con navegadores especialmente antiguos e IE9 aún requiere el uso de una interfaz diferente.

Lo más notable es que mi enfoque no hace uso de los eventos.

Getters y Setters

Mi propuesta hace uso de la característica relativamente joven de getters y setters , en particular solo setters. En términos generales, los mutadores nos permiten "personalizar" el comportamiento de cómo ciertas propiedades se les asigna un valor y se recuperan.

Una implementación que usaré aquí es el método Object.defineProperty . Funciona en Firefox, Google Chrome y, creo, IE9. No he probado otros navegadores, pero dado que esto es solo teoría ...

De todos modos, acepta tres parámetros. El primer parámetro es el objeto para el que desea definir una nueva propiedad, el segundo una cadena que se parece al nombre de la nueva propiedad y el último un "objeto descriptor" que proporciona información sobre el comportamiento de la nueva propiedad.

Dos descriptores particularmente interesantes son gety set. Un ejemplo se parecería a lo siguiente. Tenga en cuenta que el uso de estos dos prohíbe el uso de los otros 4 descriptores.

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

Ahora hacer uso de esto se vuelve ligeramente diferente:

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

Quiero enfatizar que esto solo funciona para los navegadores modernos.

Violín de trabajo: http://jsfiddle.net/Derija93/RkTMD/1/

Kiruse
fuente
2
Si solo tuviéramos Proxyobjetos Harmony :) Los setters parecen una buena idea, ¿pero eso no nos obligaría a modificar los objetos reales? Además, en una nota al margen, Object.createpodría usarse aquí (de nuevo, suponiendo que el navegador moderno permita el segundo parámetro). Además, el setter / getter podría usarse para 'proyectar' un valor diferente para el objeto y el elemento DOM :). Me pregunto si también tiene alguna idea sobre la plantilla, eso parece un verdadero desafío aquí, especialmente para estructurar bien :)
Benjamin Gruenbaum
Al igual que mi preposter, yo tampoco trabajo mucho con los motores de plantillas del lado del cliente, lo siento. :( Pero, ¿qué quieres decir con modificar los objetos reales ? Y me gustaría entender tus pensamientos sobre cómo llegaste a entender que el setter / getter podría usarse para ... Los getters / setters aquí no se usan para nada pero redirigiendo todas las entradas y recuperaciones del objeto al elemento DOM, básicamente como un Proxy, como dijiste;) Comprendí que el desafío era mantener sincronizadas dos propiedades distintas. Mi método elimina uno de los dos.
Kiruse
A Proxyeliminaría la necesidad de usar getters / setters, podría vincular elementos sin saber qué propiedades tienen. Lo que quise decir es que los captadores pueden cambiar más que bindTo.value, pueden contener lógica (y tal vez incluso una plantilla). La pregunta es cómo mantener este tipo de enlace bidireccional con una plantilla en mente. Digamos que estoy asignando mi objeto a un formulario, me gustaría mantener el elemento y el formulario sincronizados y me pregunto cómo continuaría con ese tipo de cosas. Puede ver cómo funciona eso en knockout learn.knockoutjs.com/#/?tutorial=intro por ejemplo
Benjamin Gruenbaum
@BenjaminGruenbaum Gotcha. Lo echaré un vistazo.
Kiruse
@BenjaminGruenbaum Veo lo que estás tratando de entender. Configurar todo esto con plantillas en mente resulta ser un poco más difícil. Estaré trabajando en este script por un tiempo (y lo reescribo continuamente). Pero por ahora, me estoy tomando un descanso. En realidad no tengo tiempo para esto.
Kiruse
7

Creo que mi respuesta será más técnica, pero no diferente, ya que los demás presentan lo mismo usando diferentes técnicas.
Entonces, primero lo primero, la solución a este problema es el uso de un patrón de diseño conocido como "observador", que le permite desacoplar sus datos de su presentación, haciendo que el cambio en una cosa sea transmitido a sus oyentes, pero en este caso Está hecho de dos vías.

Para la forma DOM a JS

Para vincular los datos del DOM al objeto js, ​​puede agregar marcado en forma de dataatributos (o clases si necesita compatibilidad), de esta manera:

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

De esta manera se puede acceder a través de js usando querySelectorAll(o el viejo amigo getElementsByClassNamepara compatibilidad).

Ahora puede vincular el evento escuchando los cambios de diferentes maneras: un oyente por objeto o un gran oyente al contenedor / documento. La vinculación al documento / contenedor activará el evento por cada cambio realizado en él o su hijo, tendrá una huella de memoria más pequeña pero generará llamadas de eventos.
El código se verá así:

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

Para el modo JS do DOM

Necesitará dos cosas: un metaobjeto que contendrá las referencias del elemento DOM de la bruja está vinculado a cada objeto / atributo js y una forma de escuchar los cambios en los objetos. Básicamente es de la misma manera: debe tener una forma de escuchar los cambios en el objeto y luego vincularlo al nodo DOM, ya que su objeto "no puede tener" metadatos necesitará otro objeto que contenga metadatos de alguna manera que el nombre de la propiedad se asigna a las propiedades del objeto de metadatos. El código será algo como esto:

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

Espero haber sido de ayuda.

madcampos
fuente
¿No hay un problema de comparabilidad con el uso del .observer?
Mohsen Shakiba
por ahora necesita una cuña o polyfill Object.observeya que el soporte está presente solo en Chrome por ahora. caniuse.com/#feat=object-observe
madcampos
99
Object.observe está muerto. Solo pensé que lo notaría aquí.
Benjamin Gruenbaum
@BenjaminGruenbaum ¿Qué es lo correcto para usar ahora, ya que esto está muerto?
Johnny
1
@johnny, si no me equivoco, serían trampas proxy, ya que permiten un control más granular de lo que puedo hacer con un objeto, pero tengo que investigar eso.
madcampos
7

Ayer, comencé a escribir mi propia forma de vincular datos.

Es muy divertido jugar con eso.

Creo que es hermoso y muy útil. Al menos en mis pruebas con Firefox y Chrome, Edge también debe funcionar. No estoy seguro acerca de los demás, pero si son compatibles con Proxy, creo que funcionará.

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

Aquí está el código:

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

Luego, para configurar, solo:

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

Por ahora, acabo de agregar el enlace de valor HTMLInputElement.

Avísame si sabes cómo mejorarlo.

tonelada
fuente
6

Hay una implementación básica muy simple de enlace de datos bidireccional en este enlace " Enlace de datos bidireccional fácil en JavaScript"

El enlace anterior junto con ideas de knockoutjs, backbone.js y agility.js, condujo a este marco MVVM ligero y rápido, ModelView.js basado en jQuery que juega muy bien con jQuery y del cual soy el autor humilde (o tal vez no tan humilde).

Reproduciendo el código de muestra a continuación (del enlace de la publicación del blog ):

Código de muestra para 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;
}

Para lo que concierne al objeto JavaScript, una implementación mínima de un modelo de Usuario en aras de este experimento podría ser la siguiente:

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

Ahora, cada vez que queramos vincular la propiedad de un modelo a una parte de la interfaz de usuario, solo tenemos que establecer un atributo de datos apropiado en el elemento HTML correspondiente:

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

<!-- html -->
<input type="number" data-bind-123="name" />
Nikos M.
fuente
Si bien este enlace puede responder la pregunta, es mejor incluir aquí las partes esenciales de la respuesta y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden dejar de ser válidas si la página vinculada cambia.
Sam Hanley
@sphanley, señaló, probablemente actualizaré cuando tenga más tiempo, ya que es un código bastante largo para una publicación de respuesta
Nikos M.
@sphanley, código de muestra reproducido en la respuesta del enlace referenciado (aunque creo que esto crea contenido duplicado la mayor parte del tiempo, de todos modos)
Nikos M.
1
Definitivamente crea contenido duplicado, pero ese es el punto: los enlaces de blog a menudo pueden romperse con el tiempo, y al duplicar el contenido relevante aquí se asegura de que estará disponible y será útil para futuros lectores. ¡La respuesta se ve genial ahora!
Sam Hanley
3

Cambiar el valor de un elemento puede desencadenar un evento DOM . Los oyentes que responden a eventos se pueden usar para implementar el enlace de datos en JavaScript.

Por ejemplo:

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

Aquí hay un código y una demostración que muestra cómo los elementos DOM pueden vincularse entre sí o con un objeto JavaScript.

divertido
fuente
3

Vincula cualquier entrada html

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

define dos funciones:

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

usa las funciones:

var myObject = proxify('element-to-bind')
bindValue(myObject);
Ollie Williams
fuente
3

Aquí hay una idea con la Object.definePropertycual se modifica directamente la forma en que se accede a una propiedad.

Código:

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"

violín: aquí

Thornkey
fuente
2

He revisado algunos ejemplos básicos de javascript usando los controladores de eventos onkeypress y onchange para hacer una vista vinculante a nuestros js y js para ver

Aquí ejemplo plunker http://plnkr.co/edit/7hSOIFRTvqLAvdZT4Bcc?p=preview

<!DOCTYPE html>
<html>
<body>

    <p>Two way binding data.</p>

    <p>Binding data from  view to JS</p>

    <input type="text" onkeypress="myFunction()" id="myinput">
    <p id="myid"></p>
    <p>Binding data from  js to view</p>
    <input type="text" id="myid2" onkeypress="myFunction1()" oninput="myFunction1()">
    <p id="myid3" onkeypress="myFunction1()" id="myinput" oninput="myFunction1()"></p>

    <script>

        document.getElementById('myid2').value="myvalue from script";
        document.getElementById('myid3').innerHTML="myvalue from script";
        function myFunction() {
            document.getElementById('myid').innerHTML=document.getElementById('myinput').value;
        }
        document.getElementById("myinput").onchange=function(){

            myFunction();

        }
        document.getElementById("myinput").oninput=function(){

            myFunction();

        }

        function myFunction1() {

            document.getElementById('myid3').innerHTML=document.getElementById('myid2').value;
        }
    </script>

</body>
</html>
macha devendher
fuente
2
<!DOCTYPE html>
<html>
<head>
    <title>Test</title>
</head>
<body>

<input type="text" id="demo" name="">
<p id="view"></p>
<script type="text/javascript">
    var id = document.getElementById('demo');
    var view = document.getElementById('view');
    id.addEventListener('input', function(evt){
        view.innerHTML = this.value;
    });

</script>
</body>
</html>
Anthony Newlineinfo
fuente
2

Una forma simple de vincular una variable a una entrada (enlace bidireccional) es simplemente acceder directamente al elemento de entrada en el captador y definidor:

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

En HTML:

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

Y para usar:

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


Una forma más elegante de hacer lo anterior sin getter / setter:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Usar:

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

Es un enlace de datos bidireccional muy simple en vainilla javascript ...

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

<div id="name">

</div>

Subodh Gawade
fuente
2
¿seguramente esto solo funcionaría con el evento onkeyup? es decir, si realizó una solicitud ajax y luego cambió el innerHTML a través de JavaScript, entonces esto no funcionaría
Zach Smith
1

Tarde a la fiesta, especialmente desde que escribí 2 libs relacionadas hace meses / años, las mencionaré más tarde, pero aún me parece relevante. Para hacerlo realmente corto, las tecnologías de mi elección son:

  • Proxy para observación del modelo
  • MutationObserver para el seguimiento de cambios de DOM (por razones vinculantes, no cambios de valor)
  • los cambios de valor (vista al flujo del modelo) se manejan a través de addEventListenercontroladores regulares

En mi humilde opinión, además del OP, es importante que la implementación de enlace de datos:

  • manejar diferentes casos de ciclo de vida de la aplicación (HTML primero, luego JS, JS primero luego HTML, cambio dinámico de atributos, etc.)
  • Permitir la unión profunda del modelo, para que uno pueda unirse user.address.block
  • Las matrices como modelo deben ser compatibles correctamente ( shift,splice y por igual)
  • manejar ShadowDOM
  • Intente ser lo más fácil posible para el reemplazo de la tecnología, por lo tanto, cualquier sub-lenguaje de plantillas es un enfoque amigable para los cambios futuros, ya que está demasiado asociado con el marco

Tomando todo eso en consideración, en mi opinión, hace imposible lanzar algunas docenas de líneas JS. Traté de hacerlo como un patrón en lugar de lib , no funcionó para mí.

A continuación, la Object.observeeliminación se elimina y, sin embargo, dado que la observación del modelo es una parte crucial, toda esta parte DEBE estar separada de la preocupación por otra lib. Ahora hasta el punto de los principales de cómo tomé este problema, exactamente como OP preguntó:

Modelo (parte JS)

Mi opinión sobre la observación del modelo es Proxy , es la única forma sensata de hacerlo funcionar, en mi humilde opinión. Completamente presentado observermerece su propia biblioteca, por lo que he desarrollado una object-observerbiblioteca con ese único propósito.

El / los modelo / s deben registrarse a través de alguna API dedicada, ese es el punto donde los POJO se convierten en Observables, no puedo ver ningún acceso directo aquí. Los elementos DOM que se consideran vistas enlazadas (ver a continuación), se actualizan con los valores del modelo / s al principio y luego con cada cambio de datos.

Vistas (parte HTML)

En mi humilde opinión, la forma más limpia de expresar el enlace, es a través de atributos. Muchos hicieron esto antes y muchos lo harán después, así que no hay noticias aquí, esta es solo una forma correcta de hacerlo. En mi caso, elegí la siguiente sintaxis: <span data-tie="modelKey:path.to.data => targerProperty"></span>pero esto es menos importante. Lo que es importante para mí, no hay una sintaxis de secuencias de comandos compleja en el HTML; esto está mal, de nuevo, en mi humilde opinión.

Todos los elementos designados para ser vistas vinculadas se recopilarán al principio. Me parece inevitable desde el punto de vista del rendimiento administrar un mapeo interno entre los modelos y las vistas, parece un caso correcto donde se debería sacrificar la memoria + algo de administración para guardar las búsquedas y actualizaciones en tiempo de ejecución.

Las vistas se actualizan primero desde el modelo, si está disponible y luego de cambios posteriores, como dijimos. Más aún, todo el DOM debe observarse por medio de MutationObserverpara reaccionar (vincular / desvincular) en los elementos añadidos / eliminados / modificados dinámicamente. Además, todo esto debe replicarse en ShadowDOM (abrir uno, por supuesto) para no dejar agujeros negros sin unir.

La lista de detalles puede ir más allá, pero en mi opinión, esos son los principios principales que harían que el enlace de datos se implementara con un buen equilibrio de integridad de características de uno y la simplicidad sensata del otro lado.

Y por lo tanto, además de lo object-observermencionado anteriormente, también he escrito una data-tierbiblioteca, que implementa el enlace de datos a lo largo de los conceptos mencionados anteriormente.

GullerYA
fuente
0

Las cosas han cambiado mucho en los últimos 7 años, ahora tenemos componentes web nativos en la mayoría de los navegadores. En mi opinión, el núcleo del problema es compartir el estado entre los elementos, una vez que tiene que es trivial actualizar la interfaz de usuario cuando el estado cambia y viceversa.

Para compartir datos entre elementos, puede crear una clase StateObserver y ampliar sus componentes web a partir de eso. Una implementación mínima se parece a esto:

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

violín aquí

Me gusta este enfoque porque:

  • sin recorrido transversal para encontrar data-propiedades
  • no Object.observe (en desuso)
  • sin proxy (que proporciona un enlace pero no hay mecanismo de comunicación de todos modos)
  • sin dependencias (que no sea un polyfill según los navegadores de destino)
  • es razonablemente centralizado y modular ... describe el estado en html, y tener oyentes en todas partes se volvería desordenado muy rápidamente.
  • Es extensible. Esta implementación básica es de 20 líneas de código, pero podría desarrollar fácilmente algo de conveniencia, inmutabilidad y magia de forma de estado para que sea más fácil trabajar con él.
Mr5o1
fuente