275 votes

Comment implémenter la liaison de données DOM en JavaScript

Veuillez considérer cette question comme strictement éducative. Je suis toujours intéressé par de nouvelles réponses et de nouvelles idées pour mettre en œuvre ce projet.

tl;dr

Comment mettre en œuvre la liaison de données bidirectionnelle avec JavaScript ?

Liaison de données au DOM

Par liaison de données au DOM, je veux dire par exemple, avoir un objet JavaScript a avec une propriété b . Ensuite, avoir un <input> élément du DOM (par exemple), lorsque l'élément du DOM change, a et vice versa (c'est-à-dire que je parle de liaison bidirectionnelle des données).

Voici un diagramme d'AngularJS sur ce à quoi cela ressemble :

two way data binding

Donc, en gros, j'ai un JavaScript similaire à :

var a = {b:3};

Puis un élément de saisie (ou autre formulaire) comme :

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

Je voudrais que la valeur de l'entrée soit a.b (par exemple), et lorsque le texte de l'entrée change, je voudrais que a.b de changer aussi. Quand a.b change en JavaScript, l'entrée change.

La question

Quelles sont les techniques de base pour y parvenir en JavaScript ?

Plus précisément, j'aimerais avoir une bonne réponse à laquelle me référer :

  • Comment la liaison fonctionnerait-elle pour les objets ?
  • Comment écouter le changement de forme pourrait-il fonctionner ?
  • Est-il possible, de manière simple, de ne faire modifier le HTML qu'au niveau du modèle ? J'aimerais ne pas garder la trace de la liaison dans le document HTML lui-même, mais seulement en JavaScript (avec des événements DOM, et JavaScript gardant la référence aux éléments DOM utilisés).

Qu'est-ce que j'ai essayé ?

Je suis un grand fan de Mustache, j'ai donc essayé de l'utiliser comme modèle. Cependant, j'ai rencontré des problèmes lorsque j'ai essayé d'effectuer la liaison de données elle-même, car Mustache traite le HTML comme une chaîne de caractères, de sorte qu'après avoir obtenu son résultat, je n'ai aucune référence à l'emplacement des objets de mon modèle de vue. La seule solution à laquelle j'ai pu penser était de modifier la chaîne HTML (ou l'arbre DOM créé) elle-même avec des attributs. Cela ne me dérange pas d'utiliser un autre moteur de création de modèles.

En fait, j'ai eu le sentiment que je compliquais le problème et qu'il existait une solution simple.

Nota: Veuillez ne pas fournir de réponses qui utilisent des bibliothèques externes, en particulier celles qui représentent des milliers de lignes de code. J'ai utilisé (et j'aime !) AngularJS et KnockoutJS. Je ne veux vraiment pas de réponses du type "utilisez le framework x". Idéalement, j'aimerais qu'un futur lecteur qui ne sait pas comment utiliser de nombreux frameworks saisisse lui-même comment mettre en œuvre le data-binding bidirectionnel. Je n'attends pas d'un complet réponse, mais une qui fait passer l'idée.

2 votes

Je me suis basé CrazyGlue sur la conception de Benjamin Gruenbaum. Il prend également en charge les balises SELECT, checkbox et radio. jQuery est une dépendance.

19 votes

Cette question est totalement géniale. Si elle est fermée parce qu'elle est hors sujet ou pour d'autres raisons absurdes, je serai très mécontent.

0 votes

@JohnSz merci d'avoir mentionné votre projet CrazyGlue. Cela fait longtemps que je cherche un simple liant de données à deux voies. Il semble que tu n'utilises pas Object.observe, donc le support de ton navigateur devrait être excellent. Et comme tu n'utilises pas le templating mustache, c'est parfait.

117voto

squint Points 28293
  • Comment la liaison fonctionnerait-elle pour les objets ?
  • Comment écouter le changement de forme pourrait-il fonctionner ?

Une abstraction qui met à jour les deux objets

Je suppose qu'il existe d'autres techniques, mais en fin de compte, j'aurais un objet qui fait référence à un élément DOM connexe, et qui fournit une interface qui coordonne les mises à jour de ses propres données et de l'élément connexe.

El .addEventListener() fournit une interface très agréable pour cela. Vous pouvez lui donner un objet qui implémente l'interface eventListener et il invoquera ses gestionnaires avec cet objet en tant qu'objet de référence. this valeur.

Cela vous donne un accès automatique à la fois à l'élément et à ses données connexes.

Définir votre objet

L'héritage prototypique est une bonne façon d'implémenter cela, bien qu'il ne soit pas obligatoire bien sûr. Tout d'abord, vous créez un constructeur qui reçoit votre élément et quelques données initiales.

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

Donc ici le constructeur stocke l'élément et les données sur les propriétés du nouvel objet. Il lie également un change à l'événement donné element . La chose intéressante est qu'il passe le nouvel objet au lieu d'une fonction comme deuxième argument. Mais cela ne suffit pas.

Mise en œuvre de la eventListener interface

Pour que cela fonctionne, votre objet doit implémenter la fonction eventListener l'interface. Pour ce faire, il suffit de donner à l'objet un nom de fichier handleEvent() méthode.

C'est là que l'héritage entre en jeu.

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

Il existe de nombreuses manières différentes de structurer ce système, mais pour votre exemple de coordination des mises à jour, j'ai décidé de faire de l'élément change() n'acceptent qu'une valeur, et que la méthode handleEvent passer cette valeur au lieu de l'objet événement. De cette façon, l'objet change() peut également être invoqué sans événement.

Donc maintenant, quand le change se produit, il met à jour à la fois l'élément et l'événement .data propriété. Et la même chose se produira lorsque vous appellerez .change() dans votre programme JavaScript.

Utilisation du code

Maintenant, il suffit de créer le nouvel objet et de le laisser effectuer les mises à jour. Les mises à jour dans le code JS apparaîtront sur l'entrée, et les événements de changement sur l'entrée seront visibles pour le code 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 votes

+1 Approche très propre, très simplement présentée et suffisamment simple pour que les gens puissent apprendre, beaucoup plus propre que ce que j'avais. Un cas d'utilisation courant consiste à utiliser des modèles dans le code pour représenter les vues des objets. Je me demandais comment cela pourrait fonctionner ici ? Dans des moteurs comme Mustache, je fais quelque chose comme Mustache.render(template,object) En supposant que je veuille garder un objet synchronisé avec le modèle (pas spécifique à Mustache), comment dois-je m'y prendre ?

3 votes

@BenjaminGruenbaum : Je n'ai pas utilisé de modèles côté client, mais j'imagine que Mustache a une syntaxe pour identifier les points d'insertion, et que cette syntaxe inclut une étiquette. Je pense donc que les parties "statiques" du modèle seraient rendues sous forme de morceaux de HTML stockés dans un tableau, et que les parties dynamiques seraient placées entre ces morceaux. Ensuite, les étiquettes sur les points d'insertion seraient utilisées comme propriétés de l'objet. Ensuite, si certains input est de mettre à jour l'un de ces points, il y aurait une correspondance entre l'entrée et ce point. Je vais voir si je peux trouver un exemple rapide.

1 votes

@BenjaminGruenbaum : Hmmm... Je n'ai pas pensé à la façon de coordonner proprement deux éléments différents. C'est un peu plus compliqué que je ne le pensais au départ. Je suis curieux cependant, donc je pourrais avoir besoin de travailler sur ce sujet un peu plus tard. :)

38voto

Benjamin Gruenbaum Points 51406

J'ai donc décidé de mettre ma propre solution dans le pot. Voici une violon de travail . Notez que cela ne fonctionne que sur les navigateurs très modernes.

Ce qu'il utilise

Cette mise en œuvre est très moderne - elle nécessite un navigateur (très) moderne et utilise deux nouvelles technologies :

  • MutationObserver s pour détecter les changements dans le domaine (des écouteurs d'événements sont également utilisés)
  • Object.observe pour détecter les changements dans l'objet et en informer le dom. Danger, depuis que cette réponse a été écrite O.o a été discuté et décidé contre par le ECMAScript TC, envisager un polyfill .

Comment cela fonctionne

  • Sur l'élément, mettez un domAttribute:objAttribute cartographie - par exemple bind='textContent:name'
  • Lisez cela dans la fonction dataBind. Observez les modifications apportées à l'élément et à l'objet.
  • Lorsqu'un changement se produit, mettez à jour l'élément concerné.

La solution

Voici le dataBind Notez qu'elle ne comporte que 20 lignes de code et qu'elle pourrait être plus courte :

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

Voici quelques exemples d'utilisation :

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

Voici un violon de travail . Notez que cette solution est assez générique. Le calage de Object.observe et de mutation observer est disponible.

1 votes

J'ai juste écrit ceci (es5) pour le plaisir, si quelqu'un le trouve utile - faites-vous plaisir. jsfiddle.net/P9rMm

1 votes

Gardez à l'esprit que lorsque obj.name a un setter, elle ne peut pas être observée de l'extérieur, mais doit signaler qu'elle a été modifiée depuis le setter. html5rocks.com/fr/tutorials/es7/observe/#toc-notifications - Ce type d'utilisation met un frein à l'utilisation de O.o() si vous souhaitez un comportement plus complexe et interdépendant à l'aide de setters. En outre, lorsque obj.name n'est pas configurable, la redéfinition de son paramètre (avec diverses astuces pour ajouter une notification) n'est pas non plus autorisée - les génériques avec O.o() sont donc totalement abandonnés dans ce cas précis.

9 votes

Object.observe est supprimé de tous les navigateurs : caniuse.com/#feat=objet-observe

30voto

Derija93 Points 1073

Je voudrais ajouter quelque chose à ma préposée. Je propose une approche légèrement différente qui vous permettra d'attribuer simplement une nouvelle valeur à votre objet sans utiliser de méthode. Il faut cependant noter que cette méthode n'est pas supportée par les navigateurs les plus anciens et qu'IE9 nécessite toujours l'utilisation d'une interface différente.

Le plus important est que mon approche ne fait pas appel aux événements.

Getters et Setters

Ma proposition fait appel à la fonctionnalité relativement jeune de getters et setters en particulier les setters uniquement. D'une manière générale, les mutateurs nous permettent de "personnaliser" le comportement de la manière dont certaines propriétés se voient attribuer une valeur et sont récupérées.

Une implémentation que je vais utiliser ici est le Objet.defineProperty méthode. Elle fonctionne dans FireFox, GoogleChrome et - je pense - IE9. Je n'ai pas testé d'autres navigateurs, mais comme il ne s'agit que de théorie...

Quoi qu'il en soit, il accepte trois paramètres. Le premier paramètre étant l'objet pour lequel vous souhaitez définir une nouvelle propriété, le second une chaîne ressemblant au nom de la nouvelle propriété et le dernier un "objet descripteur" fournissant des informations sur le comportement de la nouvelle propriété.

Deux descripteurs particulièrement intéressants sont get y set . Un exemple ressemblerait à ce qui suit. Notez que l'utilisation de ces deux descripteurs interdit l'utilisation des 4 autres descripteurs.

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

L'utilisation de ce système est légèrement différente :

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

Je tiens à souligner que cela ne fonctionne que pour les navigateurs modernes.

Violon de travail : http://jsfiddle.net/Derija93/RkTMD/1/

2 votes

Si seulement nous avions Harmony Proxy objets :) Les Setters semblent être une bonne idée, mais cela ne nous obligerait-il pas à modifier les objets eux-mêmes ? Par ailleurs, en passant - Object.create pourrait être utilisé ici (encore une fois, en supposant que le navigateur moderne autorise le second paramètre). De même, le setter/getter pourrait être utilisé pour "projeter" une valeur différente à l'objet et à l'élément DOM :) . Je me demande si vous avez des idées sur le templating aussi, cela semble être un vrai défi ici, surtout pour bien structurer :)

0 votes

Tout comme mon prédécesseur, je ne travaille pas beaucoup avec les moteurs de templating côté client, désolé :( Mais que voulez-vous dire par modifier les objets réels ? Et j'aimerais comprendre vos pensées sur la façon dont vous avez compris que le setter/getter pourrait être utilisé pour ... . Les getters/setters ici ne servent à rien d'autre qu'à rediriger toutes les entrées et récupérations de l'objet vers l'élément du DOM, comme un Proxy comme vous l'avez dit ;) J'ai compris que le défi était de garder deux propriétés distinctes synchronisées. Ma méthode élimine l'une des deux.

0 votes

A Proxy éliminerait la nécessité d'utiliser des getters/setters, vous pourriez lier des éléments sans savoir quelles propriétés ils possèdent. Ce que je voulais dire, c'est que les getters peuvent changer plus que bindTo.value, ils peuvent contenir une logique (et peut-être même un modèle). La question est de savoir comment maintenir cette sorte de liaison bidirectionnelle avec un modèle à l'esprit ? Imaginons que j'associe mon objet à un formulaire, j'aimerais que l'élément et le formulaire soient synchronisés et je me demande comment procéder. Vous pouvez vérifier comment cela fonctionne sur knockout learn.knockoutjs.com/#/?tutorial=intro par exemple

7voto

madcampos Points 81

Je pense que ma réponse sera plus technique, mais pas différente car les autres présentent la même chose en utilisant des techniques différentes.
Tout d'abord, la solution à ce problème est l'utilisation d'un modèle de conception appelé "observateur", qui vous permet de découpler vos données de votre présentation, en faisant en sorte que le changement d'une chose soit diffusé à ses auditeurs, mais dans ce cas, il est bidirectionnel.

Pour le passage de DOM à JS

Pour lier les données du DOM à l'objet js, vous pouvez ajouter des balises sous la forme de data (ou des classes si vous avez besoin de compatibilité), comme ceci :

<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 cette façon, on peut y accéder via js en utilisant querySelectorAll (ou le vieil ami getElementsByClassName pour la compatibilité).

Vous pouvez maintenant lier l'événement d'écoute des changements de deux façons : un écouteur par objet ou un écouteur global pour le conteneur/document. Lier l'événement au document/conteneur déclenchera l'événement pour chaque modification apportée à celui-ci ou à son enfant, ce qui réduira l'empreinte mémoire mais engendrera des appels d'événements.
Le code ressemblera à quelque chose comme ceci :

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

Pour la méthode JS do DOM

Vous aurez besoin de deux choses : un méta-objet qui contiendra les références de l'élément DOM lié à chaque objet/attribut js et un moyen d'écouter les changements dans les objets. C'est essentiellement la même chose : vous devez avoir un moyen d'écouter les changements dans l'objet et ensuite le lier au noeud DOM, comme votre objet "ne peut pas avoir" de métadonnées, vous aurez besoin d'un autre objet qui contient des métadonnées de telle sorte que le nom de la propriété correspond aux propriétés de l'objet de métadonnées. Le code sera quelque chose comme ceci :

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

J'espère que j'ai pu vous aider.

0 votes

L'utilisation de l'observateur ne pose-t-elle pas un problème de comparabilité ?

0 votes

Pour l'instant, il a besoin d'un shim ou d'un polyfill pour Object.observe car le support n'est présent qu'en chrome pour le moment. caniuse.com/#feat=objet-observe

9 votes

Object.observe est mort. J'ai juste pensé que je devais le noter ici.

6voto

Nikos M. Points 171

Ce lien propose une mise en œuvre très simple de la liaison de données à deux voies. "Liaison de données bidirectionnelle facile en JavaScript"

Le lien précédent ainsi que des idées provenant de knockoutjs, backbone.js et agility.js, ont conduit à ce cadre MVVM léger et rapide, ModelView.js basé sur jQuery qui joue joliment avec jQuery et dont je suis l'humble (ou peut-être pas si humble) auteur.

Reproduction de l'exemple de code ci-dessous (à partir de lien vers l'article de blog ) :

Exemple de code pour 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;
}

Pour ce qui est de l'objet JavaScript, une implémentation minimale d'un objet modèle utilisateur pour les besoins de cette expérience pourrait être la suivante :

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

Maintenant, chaque fois que nous voulons lier une propriété d'un modèle à un élément de l'IU, nous il suffit de définir un attribut de données approprié sur l'élément correspondant :

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

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

0 votes

Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et de fournir le lien pour référence. Les réponses ne comportant qu'un lien peuvent devenir invalides si la page liée change.

0 votes

@sphanley, c'est noté, je mettrai probablement à jour quand j'aurai plus de temps, car c'est un code plutôt long pour un post de réponse.

0 votes

@sphanley, j'ai reproduit l'exemple de code sur la réponse du lien référencé (bien que je pense que cela crée du contenu dupliqué la plupart du temps, de toute façon).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X