Como implementar a ligação de dados DOM em JavaScript

244

Por favor, trate esta questão como estritamente educacional. Ainda estou interessado em ouvir novas respostas e idéias para implementar isso

tl; dr

Como eu implementaria a ligação de dados bidirecional com JavaScript?

Ligação de dados ao DOM

Por ligação de dados ao DOM, quero dizer, por exemplo, ter um objeto JavaScript acom uma propriedade b. Então, ter um <input>elemento DOM (por exemplo), quando o elemento DOM for alterado, aalterado e vice-versa (ou seja, a ligação de dados bidirecional).

Aqui está um diagrama do AngularJS sobre como isso se parece:

ligação de dados bidirecional

Então, basicamente, eu tenho JavaScript semelhante a:

var a = {b:3};

Em seguida, um elemento de entrada (ou outro formulário) como:

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

Gostaria que o valor da entrada fosse a.bo valor de (por exemplo) e, quando o texto da entrada mudar, eu também gostaria a.bde mudar. Quandoa.b JavaScript muda, a entrada muda.

A questão

Quais são algumas técnicas básicas para fazer isso em JavaScript simples?

Especificamente, gostaria de uma boa resposta para me referir a:

  • Como a ligação funcionaria para objetos?
  • Como ouvir mudanças no formulário pode funcionar?
  • De uma maneira simples, é possível ter apenas o HTML modificado no nível do modelo? Gostaria de não acompanhar a ligação no próprio documento HTML, mas apenas em JavaScript (com eventos DOM e JavaScript mantendo referência aos elementos DOM usados).

O que eu tentei?

Sou um grande fã de Bigode, então tentei usá-lo para modelar. No entanto, tive problemas ao tentar executar a ligação de dados em si, já que o Bigode processa o HTML como uma string. Assim, depois de obter o resultado, não tenho referência a onde estão os objetos no meu viewmodel. A única solução alternativa que pude pensar nisso foi modificar a sequência de caracteres HTML (ou a árvore DOM criada) com atributos. Não me importo de usar um mecanismo de modelo diferente.

Basicamente, tive a forte sensação de que estava complicando o problema em questão e há uma solução simples.

Nota: Não forneça respostas que usem bibliotecas externas, especialmente aquelas com milhares de linhas de código. Eu usei (e gosto!) AngularJS e KnockoutJS. Eu realmente não quero respostas no formulário 'use framework x'. Idealmente, eu gostaria de um futuro leitor que não saiba usar muitas estruturas para entender como implementar a ligação de dados bidirecional. Não espero uma resposta completa , mas uma que transmita a ideia.

Benjamin Gruenbaum
fonte
2
I com base CrazyGlue no design de Benjamin Gruenbaum. Ele também suporta SELECT, caixa de seleção e tags de rádio. jQuery é uma dependência.
precisa saber é o seguinte
12
Esta pergunta é totalmente incrível. Se alguma vez for fechado por ser fora de tópico ou por qualquer outra bobagem boba, ficarei seriamente irritado.
OCDev
@JohnSz obrigado por mencionar o seu projeto CrazyGlue. Eu estive procurando por um fichário de dados bidirecional simples por um longo tempo. Parece que você não está usando o Object.observe, portanto o suporte ao navegador deve ser ótimo. E você não está usando modelos de bigode, então é perfeito.
Gavin
@Benjamin O que você acabou fazendo?
31916 johnny
@ johnny na minha opinião, a abordagem correta é criar o DOM em JS (como React) e não vice-versa. Acho que, eventualmente, é isso que faremos.
Benjamin Gruenbaum 29/03

Respostas:

106
  • Como a ligação funcionaria para objetos?
  • Como ouvir mudanças no formulário pode funcionar?

Uma abstração que atualiza os dois objetos

Suponho que existem outras técnicas, mas, em última análise, eu teria um objeto que contém referência a um elemento DOM relacionado e fornece uma interface que coordena as atualizações de seus próprios dados e seu elemento relacionado.

O .addEventListener()fornece uma interface muito agradável para isso. Você pode atribuir a ele um objeto que implementa a eventListenerinterface e ele invocará seus manipuladores com esse objeto como thisvalor.

Isso fornece acesso automático ao elemento e aos dados relacionados.

Definindo seu objeto

A herança prototípica é uma boa maneira de implementar isso, embora não seja necessária, é claro. Primeiro, você criaria um construtor que recebe seu elemento e alguns dados iniciais.

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

Então aqui o construtor armazena o elemento e os dados nas propriedades do novo objeto. Também liga umchange evento ao dado element. O interessante é que ele passa o novo objeto em vez de uma função como o segundo argumento. Mas isso por si só não vai funcionar.

Implementando a eventListenerinterface

Para fazer isso funcionar, seu objeto precisa implementar o eventListener interface. Tudo o que é necessário para fazer isso é fornecer um handleEvent()método ao objeto .

É aí que a herança entra.

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

Há muitas maneiras diferentes de estruturar isso, mas, para o seu exemplo de coordenação de atualizações, decidi fazer com que o change()método aceite apenas um valor e tenha ohandleEvent passe esse valor em vez do objeto de evento. Dessa forma, também change()pode ser chamado sem um evento.

Então agora, quando o changeevento acontecer, ele atualizará o elemento e a .datapropriedade. E o mesmo acontecerá quando você ligar.change() seu programa JavaScript.

Usando o código

Agora você acabou de criar o novo objeto e deixá-lo realizar atualizações. As atualizações no código JS aparecerão na entrada e os eventos de alteração na entrada serão visíveis para o 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/

user1106925
fonte
5
+1 Abordagem muito limpa, muito simples e simples o suficiente para as pessoas aprenderem, muito mais limpa do que eu tinha. Um caso de uso comum é usar modelos no código para representar as visualizações dos objetos. Eu queria saber como isso pode funcionar aqui? Em mecanismos como o Bigode, eu faço algo Mustache.render(template,object), assumindo que quero manter um objeto sincronizado com o modelo (não específico do Bigode), como eu continuaria com isso?
Benjamin Gruenbaum
3
@BenjaminGruenbaum: Eu não usei modelos do lado do cliente, mas imagino que o Bigode tenha alguma sintaxe para identificar pontos de inserção e que essa sintaxe inclua um rótulo. Então, eu pensaria que as partes "estáticas" do modelo seriam renderizadas em pedaços de HTML armazenados em uma matriz, e as partes dinâmicas iriam entre esses pedaços. Em seguida, os rótulos nos pontos de inserção seriam usados ​​como propriedades do objeto. Então, se alguns inputdevem atualizar um desses pontos, haveria um mapeamento da entrada para esse ponto. Vou ver se consigo criar um exemplo rápido.
1
@BenjaminGruenbaum: Hmmm ... não pensei em como coordenar de maneira limpa dois elementos diferentes. Isso é um pouco mais envolvido do que eu pensava a princípio. Estou curioso, então talvez precise trabalhar nisso um pouco mais tarde. :)
2
Você verá que há um Templateconstrutor primário que faz a análise, mantém os diferentes MyCtorobjetos e fornece uma interface para atualizar cada um por seu identificador. Deixe-me saber se você tem perguntas. :) EDIT: ... use esse link em vez disso ... Esqueci que havia um aumento exponencial no valor de entrada a cada 10 segundos para demonstrar as atualizações do JS. Isso limita isso.
2
... versão totalmente comentada, além de pequenas melhorias.
36

Então, eu decidi jogar minha própria solução no pote. Aqui está um violino de trabalho . Observe que isso é executado apenas em navegadores muito modernos.

O que ele usa

Essa implementação é muito moderna - requer um navegador (muito) moderno e os usuários duas novas tecnologias:

  • MutationObservers para detectar mudanças no dom (ouvintes de eventos também são usados)
  • Object.observepara detectar mudanças no objeto e notificar o dom. Perigo, uma vez que esta resposta foi escrita Oo foi discutido e decidido pelo ECMAScript TC, considere um polyfill .

Como funciona

  • No elemento, coloque um domAttribute:objAttributemapeamento - por exemplobind='textContent:name'
  • Leia isso na função dataBind. Observe as alterações no elemento e no objeto.
  • Quando ocorrer uma alteração - atualize o elemento relevante.

A solução

Aqui está a dataBindfunção, observe que são apenas 20 linhas de código e podem ser mais curtas:

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

Aqui está um pouco de uso:

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

Aqui está um violino de trabalho . Observe que esta solução é bastante genérica. O shovel observador Object.observe e mutation está disponível.

Benjamin Gruenbaum
fonte
1
Aconteceu de eu escrever este (ES5) para se divertir, se alguém acha útil - bater-se jsfiddle.net/P9rMm
Benjamin Gruenbaum
1
Lembre-se de que quando obj.nameum setter não pode ser observado externamente, mas deve transmitir que ele mudou de dentro do setter - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - meio que dispara uma chave nos trabalhos para Oo () se você quiser um comportamento mais complexo e interdependente usando setters. Além disso, quando obj.namenão é configurável, redefinir seu setter (com vários truques para adicionar notificação) também não é permitido - portanto, os genéricos com Oo () são totalmente descartados nesse caso específico.
Nolo 20/10
8
Object.observe é removido de todos os navegadores: caniuse.com/#feat=object-observe
JvdBerg
1
Um proxy pode ser usado em vez de Object.observe ou github.com/anywhichway/proxy-observe ou gist.github.com/ebidel/1b553d571f924da2da06 ou os polyfills mais antigos, também no github @JvdBerg
jimmont
29

Eu gostaria de adicionar ao meu preposter. Sugiro uma abordagem ligeiramente diferente que permitirá que você simplesmente atribua um novo valor ao seu objeto sem usar um método. Note-se, no entanto, que isso não é suportado por navegadores especialmente mais antigos e o IE9 ainda requer o uso de uma interface diferente.

O mais notável é que minha abordagem não utiliza eventos.

Getters e Setters

Minha proposta utiliza o recurso relativamente jovem de getters e setters , particularmente apenas de criadores. De um modo geral, os mutadores nos permitem "personalizar" o comportamento de como determinadas propriedades recebem um valor e são recuperadas.

Uma implementação que vou usar aqui é o método Object.defineProperty . Funciona no FireFox, GoogleChrome e - eu acho - IE9. Ainda não testei outros navegadores, mas como isso é apenas uma teoria ...

De qualquer forma, ele aceita três parâmetros. O primeiro parâmetro é o objeto para o qual você deseja definir uma nova propriedade, o segundo uma sequência semelhante ao nome da nova propriedade e o último um "objeto descritor" fornecendo informações sobre o comportamento da nova propriedade.

Dois descritores particularmente interessantes são gete set. Um exemplo seria algo como o seguinte. Observe que o uso desses dois proíbe o uso dos outros 4 descritores.

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

Agora, fazer uso disso se torna um pouco diferente:

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

Quero enfatizar que isso só funciona para navegadores modernos.

Violino de trabalho: http://jsfiddle.net/Derija93/RkTMD/1/

Kiruse
fonte
2
Se ao menos tivéssemos Proxyobjetos Harmony :) Setters parecem uma boa idéia, mas isso não exigiria que modificássemos os objetos reais? Além disso, em uma nota lateral - Object.createpoderia ser usado aqui (novamente, assumindo um navegador moderno que permitisse o segundo parâmetro). Além disso, o setter / getter pode ser usado para 'projetar' um valor diferente para o objeto e o elemento DOM :). Eu estou querendo saber se você tem quaisquer conhecimentos sobre templates também, que parece ser um verdadeiro desafio aqui, especialmente a estrutura bem :)
Benjamin Gruenbaum
Assim como meu preposter, eu também não trabalho muito com mecanismos de modelos do lado do cliente, desculpe. :( Mas o que você quer dizer com modificar os objetos reais ? E eu gostaria de entender seus pensamentos sobre como você conseguiu entender que o setter / getter poderia ser usado para ... Os getters / setters aqui são usados ​​para nada mas redirecionando todas as entradas e recuperações do objeto para o elemento DOM, basicamente como a Proxy, como você disse.;) Entendi o desafio de manter duas propriedades distintas sincronizadas. Meu método elimina um dos dois.
10133 Kiruse
A Proxyeliminaria a necessidade de usar getters / setters, você poderia ligar elementos sem saber quais propriedades eles têm. O que eu quis dizer é que os getters podem mudar mais do que bindTo.value e podem conter lógica (e talvez até um modelo). A questão é como manter esse tipo de ligação bidirecional com um modelo em mente? Digamos que estou mapeando meu objeto para um formulário, gostaria de manter o elemento e o formulário sincronizados e estou me perguntando como eu continuaria com esse tipo de coisa. Você pode verificar como isso funciona na knockout learn.knockoutjs.com/#/?tutorial=intro por exemplo
Benjamin Gruenbaum
@BenjaminGruenbaum Gotcha. Vou dar uma olhada.
22413 Kiruse
@BenjaminGruenbaum Entendo o que você está tentando entender. Configurar tudo isso com modelos em mente acaba sendo um pouco mais difícil. Estarei trabalhando nesse script por um tempo (e continuamente o rebase). Mas, por enquanto, estou dando um tempo. Na verdade, não tenho tempo para isso.
10133 Kiruse
7

Acho que minha resposta será mais técnica, mas não diferente, pois as outras apresentam a mesma coisa usando técnicas diferentes.
Então, para começar, a solução para esse problema é o uso de um padrão de design conhecido como "observador", permite dissociar os dados da apresentação, fazendo com que a alteração de uma coisa seja transmitida aos ouvintes, mas neste caso é feito de mão dupla.

Pela maneira DOM para JS

Para vincular os dados do DOM ao objeto js, ​​você pode adicionar marcações na forma de dataatributos (ou classes, se precisar de compatibilidade), assim:

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

Dessa forma, ele pode ser acessado via js usando querySelectorAll(ou o velho amigo getElementsByClassNamepara compatibilidade).

Agora você pode vincular o evento ouvindo as alterações de maneiras: um ouvinte por objeto ou um grande ouvinte no contêiner / documento. A ligação ao documento / contêiner disparará o evento para cada alteração feita nele ou em um filho, ele terá um espaço menor de memória, mas gerará chamadas de eventos.
O código será algo como isto:

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

Pela maneira JS do DOM

Você precisará de duas coisas: um meta-objeto que conterá as referências do elemento DOM da bruxa será vinculado a cada objeto / atributo js e uma maneira de ouvir as alterações nos objetos. É basicamente da mesma maneira: você precisa ter uma maneira de ouvir as alterações no objeto e vinculá-lo ao nó DOM, pois seu objeto "não pode ter" metadados, você precisará de outro objeto que contenha metadados de uma maneira que o nome da propriedade mapeia para as propriedades do objeto de metadados. O código será algo como isto:

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 ter ajudado.

madcampos
fonte
não há problema de comparabilidade com o uso do .observer?
Mohsen Shakiba
por enquanto, ele precisa de um calço ou polyfill, Object.observepois o suporte está presente apenas no chrome por enquanto. caniuse.com/#feat=object-observe
madcampos
9
Object.observe está morto. Só pensei em anotar isso aqui.
Benjamin Gruenbaum
@BenjaminGruenbaum Qual é a coisa correta a ser usada agora, já que está morta?
31916 johnny
1
@ johnny se não estiver errado, seriam armadilhas de proxy, pois permitem um controle mais granular do que posso fazer com um objeto, mas tenho que investigar isso.
madcampos
7

Ontem, comecei a escrever minha própria maneira de vincular dados.

É muito engraçado brincar com isso.

Eu acho bonito e muito útil. Pelo menos nos meus testes usando Firefox e Chrome, o Edge também deve funcionar. Não tenho certeza sobre os outros, mas se eles suportam Proxy, acho 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' />

Aqui está o 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);
                }
            }
        }
    }
})();

Então, para definir, apenas:

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 enquanto, acabei de adicionar a ligação de valor HTMLInputElement.

Deixe-me saber se você sabe como melhorá-lo.

tonelada
fonte
6

Há uma implementação muito simples de ligação de dados bidirecional neste link "Ligação de dados bidirecional fácil em JavaScript"

O link anterior, juntamente com as idéias de knockoutjs, backbone.js e agility.js, levaram a essa estrutura MVVM leve e rápida, ModelView.js baseado em jQuery que joga bem com jQuery e do qual eu sou o humilde (ou talvez não tão humilde) autor.

Reproduzindo o código de exemplo abaixo (no link da postagem do blog ):

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

No que diz respeito ao objeto JavaScript, uma implementação mínima de um modelo de Usuário para o experimento pode ser a seguinte:

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

Agora, sempre que queremos vincular a propriedade de um modelo a uma parte da interface do usuário, basta definir um atributo de dados apropriado no elemento HTML correspondente:

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

<!-- html -->
<input type="number" data-bind-123="name" />
Nikos M.
fonte
Embora esse link possa responder à pergunta, é melhor incluir aqui as partes essenciais da resposta e fornecer o link para referência. As respostas somente para links podem se tornar inválidas se a página vinculada for alterada.
21715 Sam
@ sphanley, notei que provavelmente atualizarei quando tiver mais tempo, pois é um código bastante longo para uma resposta
Nikos M.
@sphanley, reproduzido código de exemplo na resposta de ligação referenciado (embora i thinbk isso cria conteúdo duplicarte maior parte do tempo, de qualquer maneira)
Nikos M.
1
Definitivamente, ele cria conteúdo duplicado, mas esse é o ponto - os links de blog geralmente podem quebrar com o tempo e, duplicando o conteúdo relevante aqui, garante que ele estará disponível e será útil para futuros leitores. A resposta está ótima agora!
Sam Hanley
3

Alterar o valor de um elemento pode acionar um evento DOM . Ouvintes que respondem a eventos podem ser usados ​​para implementar a ligação de dados em JavaScript.

Por exemplo:

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

Aqui está o código e uma demonstração que mostra como os elementos DOM podem ser ligados entre si ou com um objeto JavaScript.

divertido
fonte
3

Vincular qualquer entrada html

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

defina duas funções:

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

use as funções:

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

Aqui está uma idéia usando a Object.definePropertyqual modifica diretamente a maneira como uma propriedade é acessada.

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"

violino: Aqui

Thornkey
fonte
2

Passei por um exemplo básico de javascript usando manipuladores de eventos onkeypress e onchange para criar uma visualização vinculativa aos nossos js e js para visualizar

Aqui exemplo de 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
fonte
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
fonte
2

Uma maneira simples de vincular uma variável a uma entrada (ligação bidirecional) é acessar diretamente diretamente o elemento de entrada no getter e setter:

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

Em HTML:

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

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


Uma maneira mais sofisticada de fazer o que precede sem 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
fonte
1

É uma ligação de dados bidirecional muito simples em javascript vanilla ....

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

<div id="name">

</div>

Subodh Gawade
fonte
2
certamente isso funcionaria apenas com o evento onkeyup? ou seja, se você fez uma solicitação ajax, e em seguida, mudou o innerHTML via JavaScript, então isso não iria funcionar
Zach Smith
1

Tarde para a festa, especialmente desde que eu escrevi 2 libs relacionadas há meses / anos atrás, eu as mencionarei mais tarde, mas ainda parece relevante para mim. Para torná-lo um spoiler muito curto, as tecnologias de minha escolha são:

  • Proxy para observação do modelo
  • MutationObserver para as alterações de rastreamento do DOM (por motivos vinculativos, não alterações de valor)
  • alterações de valor (visualização do fluxo do modelo) são tratadas por meio de addEventListenermanipuladores regulares

IMHO, além do PO, é importante que a implementação da vinculação de dados:

  • lidar com diferentes casos de ciclo de vida de aplicativos (HTML primeiro, depois JS, JS primeiro e HTML, alteração de atributos dinâmicos etc.)
  • permitir ligação profunda do modelo, para que se possa ligar user.address.block
  • matrizes como um modelo deve ser apoiada corretamente ( shift, splicee afins)
  • lidar com ShadowDOM
  • tente ser o mais fácil possível para a substituição da tecnologia, portanto, qualquer sub-idioma de modelo é uma abordagem que não muda para o futuro, uma vez que é muito acoplada à estrutura

Levando tudo isso em consideração, na minha opinião, é impossível lançar apenas algumas dezenas de linhas JS. Eu tentei fazer isso como um padrão e não como lib - não funcionou para mim.

Em seguida, ter Object.observeé removido e, ainda assim, dado que a observação do modelo é parte crucial - toda essa parte DEVE ser separada por uma outra lib. Agora, ao ponto de diretores de como eu resolvi esse problema - exatamente como o OP perguntou:

Modelo (parte JS)

Minha opinião sobre a observação do modelo é Proxy , é a única maneira sensata de fazê-lo funcionar, IMHO. O recurso completo observermerece sua própria biblioteca, então eu desenvolvi a object-observerbiblioteca para esse único objetivo.

O (s) modelo (s) deve (m) ser registrado (s) através de uma API dedicada, esse é o ponto em que os POJOs se transformam em Observables, não pode ver nenhum atalho aqui. Os elementos DOM que são considerados vistas limitadas (veja abaixo) são atualizados com os valores do (s) modelo (s) primeiro e depois a cada alteração de dados.

Visualizações (parte HTML)

IMHO, a maneira mais limpa de expressar a ligação, é através de atributos. Muitos fizeram isso antes e muitos farão depois; portanto, não há notícias aqui, esta é apenas a maneira certa de fazer isso. No meu caso, segui a seguinte sintaxe:, <span data-tie="modelKey:path.to.data => targerProperty"></span>mas isso é menos importante. O que é importante para mim, nenhuma sintaxe de script complexa no HTML - isso está errado, novamente, IMHO.

Todos os elementos designados como vistas limitadas devem ser coletados primeiro. Parece-me inevitável, do ponto de vista do desempenho, gerenciar alguns mapeamentos internos entre os modelos e as visualizações, parece o caso certo em que memória + algum gerenciamento deve ser sacrificado para salvar pesquisas e atualizações em tempo de execução.

As visualizações são atualizadas a partir do modelo, se disponíveis, e após alterações posteriores, como dissemos. Mais ainda, todo o DOM deve ser observado por meio de MutationObserver, a fim de reagir (ligar / desligar) nos elementos adicionados / removidos / alterados dinamicamente. Além disso, tudo isso deve ser replicado no ShadowDOM (um aberto, é claro) para não deixar buracos negros não ligados.

De fato, a lista de detalhes pode ir mais longe, mas esses são, na minha opinião, os principais princípios que implementariam a ligação de dados com um bom equilíbrio entre a integridade dos recursos de um e a simplicidade sadia do outro lado.

E assim, além do object-observermencionado acima, eu escrevi também uma data-tierbiblioteca, que implementa a ligação de dados ao longo dos conceitos mencionados acima.

GullerYA
fonte
0

As coisas mudaram muito nos últimos 7 anos, temos componentes da Web nativos na maioria dos navegadores agora. Na IMO, o núcleo do problema é o compartilhamento de estado entre elementos, uma vez que é trivial atualizar a interface do usuário quando o estado muda e vice-versa.

Para compartilhar dados entre elementos, você pode criar uma classe StateObserver e estender seus componentes da Web a partir disso. Uma implementação mínima se parece com isso:

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

mexer aqui

Eu gosto dessa abordagem porque:

  • no dom traversal para encontrar data-propriedades
  • no Object.observe (descontinuado)
  • sem proxy (que fornece um gancho, mas nenhum mecanismo de comunicação)
  • sem dependências (exceto um polyfill, dependendo dos navegadores de destino)
  • é razoavelmente centralizado e modular ... descrevendo o estado em html e ter ouvintes em todos os lugares ficaria bagunçado muito rapidamente.
  • é extensível. Essa implementação básica é de 20 linhas de código, mas você pode criar facilmente alguma mágica de conveniência, imutabilidade e forma de estado para facilitar o trabalho.
Mr5o1
fonte