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

244

Veuillez traiter cette question comme strictement éducative. Je suis toujours intéressé à entendre de nouvelles réponses et idées pour mettre en œuvre ce

tl; dr

Comment implémenter 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 aavec une propriété b. Ensuite, avoir un <input>élément DOM (par exemple), lorsque l'élément DOM change, achange et vice versa (c'est-à-dire, je veux dire la liaison de données bidirectionnelle).

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

liaison de données bidirectionnelle

Donc, fondamentalement, j'ai JavaScript similaire à:

var a = {b:3};

Ensuite, un élément d'entrée (ou autre formulaire) comme:

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

Je voudrais que la valeur de l'entrée soit a.bla valeur de (par exemple), et lorsque le texte d'entrée change, je voudrais a.baussi le changer. En cas de a.bmodification de JavaScript, l'entrée change.

La question

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

En particulier, j'aimerais qu'une bonne réponse fasse référence à:

  • Comment la reliure fonctionnerait-elle pour les objets?
  • Comment l'écoute du changement dans le formulaire pourrait-elle fonctionner?
  • Est-il possible de manière simple de ne modifier le code HTML qu'au niveau du modèle? Je ne voudrais pas garder la trace de la liaison dans le document HTML lui-même, mais uniquement en JavaScript (avec les événements DOM et JavaScript en faisant référence aux éléments DOM utilisés).

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

Je suis un grand fan de Moustache, j'ai donc essayé de l'utiliser pour les modèles. Cependant, j'ai rencontré des problèmes lorsque j'essayais d'effectuer la liaison de données elle-même, car Moustache traite le HTML comme une chaîne, donc après avoir obtenu son résultat, je n'ai aucune référence à l'endroit où se trouvent les objets dans mon modèle de vue. La seule solution de contournement à laquelle je pouvais penser était de modifier la chaîne HTML (ou l'arborescence DOM créée) elle-même avec des attributs. Cela ne me dérange pas d'utiliser un moteur de modélisation différent.

Fondamentalement, j'ai eu le sentiment fort de compliquer le problème et il existe une solution simple.

Remarque: veuillez ne pas fournir de réponses utilisant des bibliothèques externes, en particulier des milliers de lignes de code. J'ai utilisé (et j'aime!) AngularJS et KnockoutJS. Je ne veux vraiment pas de réponses sous la forme «utiliser le cadre x». De manière optimale, j'aimerais qu'un futur lecteur ne sachant pas comment utiliser de nombreux frameworks pour comprendre comment implémenter la liaison de données bidirectionnelle elle-même. Je ne m'attends pas à une réponse complète , mais à une idée claire.

Benjamin Gruenbaum
la source
2
J'ai basé CrazyGlue sur le design de Benjamin Gruenbaum. Il prend également en charge les balises SELECT, checkbox et radio. jQuery est une dépendance.
JohnSz
12
Cette question est totalement géniale. Si jamais il est fermé pour être hors sujet ou pour un autre non-sens stupide, je vais être sérieusement coché.
OCDev
@JohnSz merci d'avoir mentionné votre projet CrazyGlue. Je recherche depuis longtemps un simple classeur de données bidirectionnel. Il semble que vous n'utilisiez pas Object.observe, donc la prise en charge de votre navigateur devrait être excellente. Et vous n'utilisez pas de modèle de moustache, donc c'est parfait.
Gavin
@Benjamin Qu'avez-vous fini par faire?
johnny
@johnny à mon avis, la bonne approche consiste à créer le DOM dans JS (comme React) et non l'inverse. Je pense que finalement c'est ce que nous ferons.
Benjamin Gruenbaum

Réponses:

106
  • Comment la reliure fonctionnerait-elle pour les objets?
  • Comment l'écoute du changement dans le formulaire pourrait-elle 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 contenant une référence à un élément DOM associé et fournissant une interface qui coordonne les mises à jour de ses propres données et de son élément associé.

Le .addEventListener()fournit une interface très agréable pour cela. Vous pouvez lui donner un objet qui implémente l' eventListenerinterface, et il invoquera ses gestionnaires avec cet objet comme thisvaleur.

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

Définir votre objet

L'héritage prototypique est un bon moyen de l'implémenter, bien que cela ne soit pas obligatoire bien sûr. Vous devez d'abord créer un constructeur qui reçoit votre élément et certaines données initiales.

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

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

Implémentation de l' eventListenerinterface

Pour que cela fonctionne, votre objet doit implémenter l' eventListenerinterface. Pour cela, il suffit de donner à l'objet une 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 façons différentes de structurer cela, mais pour votre exemple de coordination des mises à jour, j'ai décidé de faire en sorte que la change()méthode n'accepte qu'une valeur et handleEventque cette valeur soit transmise au lieu de l'objet événement. De cette façon, le change()peut également être invoqué sans événement.

Alors maintenant, lorsque l' changeévénement se produit, il mettra à jour à la fois l'élément et la .datapropriété. Et la même chose se produira lorsque vous appellerez .change()votre programme JavaScript.

Utiliser le code

Il vous suffit maintenant de créer le nouvel objet et de le laisser effectuer des mises à jour. Les mises à jour dans le code JS apparaîtront sur l'entrée et les événements de modification 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);

DÉMO: http://jsfiddle.net/RkTMD/

1106925
la source
5
+1 Approche très propre, très simple et assez simple pour que les gens apprennent, beaucoup plus propre que ce que j'avais. Un cas d'utilisation courant utilise 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 Moustache, je fais quelque chose Mustache.render(template,object), en supposant que je souhaite garder un objet synchronisé avec le modèle (non spécifique à Moustache), comment pourrais-je procéder à ce sujet?
Benjamin Gruenbaum
3
@BenjaminGruenbaum: Je n'ai pas utilisé de modèles côté client, mais j'imagine que Moustache a une syntaxe pour identifier les points d'insertion, et que cette syntaxe comprend une étiquette. Je pense donc que les parties "statiques" du modèle seraient rendues en morceaux de HTML stockés dans un tableau, et les parties dynamiques passeraient entre ces morceaux. Ensuite, les étiquettes sur les points d'insertion seraient utilisées comme propriétés d'objet. Ensuite, si certains inputdoivent mettre à jour l'un de ces points, il y aurait un mappage de l'entrée à ce point. Je vais voir si je peux trouver un exemple rapide.
1
@BenjaminGruenbaum: Hmmm ... Je n'ai pas pensé à coordonner proprement deux éléments différents. C'est un peu plus compliqué que je ne le pensais au début. Je suis curieux cependant, je devrai peut-être y travailler un peu plus tard. :)
2
Vous verrez qu'il existe un Templateconstructeur principal qui effectue l'analyse, contient les différents MyCtorobjets et fournit une interface pour mettre à jour chacun par son identifiant. Faites moi savoir si vous avez des questions. :) EDIT: ... utilisez ce lien à la place ... J'avais oublié que j'avais une augmentation exponentielle de la valeur d'entrée toutes les 10 secondes pour démontrer les mises à jour JS. Cela le limite.
2
... version entièrement commentée et améliorations mineures.
36

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

Ce qu'il utilise

Cette implémentation est très moderne - elle nécessite un navigateur (très) moderne et les utilisateurs de deux nouvelles technologies:

  • MutationObservers pour détecter les changements dans le dom (les écouteurs d'événements sont également utilisés)
  • Object.observepour détecter les changements dans l'objet et notifier le dom. Danger, puisque cette réponse a été écrite Oo a été discutée et décidée par le TC ECMAScript, envisagez un polyfill .

Comment ça fonctionne

  • Sur l'élément, mettez un domAttribute:objAttributemappage - par exemplebind='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 pertinent.

La solution

Voici la dataBindfonction, notez que ce n'est que 20 lignes de code et pourrait être plus court:

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 utilisations:

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 Object.observe et l'observateur de mutation est disponible.

Benjamin Gruenbaum
la source
1
Je viens d'écrire ceci (es5) pour le plaisir, si quelqu'un le trouve utile - assommez
Benjamin Gruenbaum
1
Gardez à l'esprit que lorsqu'il obj.namea un setter, il ne peut pas être observé de l'extérieur, mais doit diffuser qu'il a changé de l'intérieur du setter - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - jette une clé dans les travaux pour Oo () si vous voulez un comportement plus complexe et interdépendant en utilisant des setters. De plus, lorsqu'il obj.namen'est pas configurable, la redéfinition de son setter (avec diverses astuces pour ajouter une notification) n'est pas non plus autorisée - les génériques avec Oo () sont donc totalement supprimés dans ce cas spécifique.
Nolo
8
Object.observe est supprimé de tous les navigateurs: caniuse.com/#feat=object-observe
JvdBerg
1
Un proxy peut être utilisé à la place d'Object.observe, ou github.com/anywhichway/proxy-observe ou gist.github.com/ebidel/1b553d571f924da2da06 ou les anciens polyfills, également sur github @JvdBerg
jimmont
29

Je voudrais ajouter à ma pré-publication. Je suggère une approche légèrement différente qui vous permettra d'attribuer simplement une nouvelle valeur à votre objet sans utiliser de méthode. Il convient de noter cependant que cela n'est pas pris en charge par les navigateurs particulièrement anciens et IE9 nécessite toujours l'utilisation d'une interface différente.

Plus particulièrement, mon approche ne fait pas appel aux événements.

Getters et Setters

Ma proposition utilise la caractéristique relativement récente des getters et setters , en particulier des setters uniquement. De manière générale, les mutateurs nous permettent de «personnaliser» le comportement de l'attribution et de la récupération de certaines propriétés.

Une implémentation que j'utiliserai ici est la méthode Object.defineProperty . Cela fonctionne dans FireFox, GoogleChrome et - je pense - IE9. Je n'ai pas testé d'autres navigateurs, mais comme ce n'est qu'une 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 getet set. Un exemple ressemblerait à quelque chose comme ceci. Notez que l'utilisation de ces deux 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;
        }
    });
}

Maintenant, utiliser cela devient légèrement différent:

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/

Kiruse
la source
2
Si seulement nous avions des Proxyobjets Harmony :) Les setters semblent être une bonne idée, mais cela ne nous obligerait-il pas à modifier les objets réels? En outre, sur une note secondaire - Object.createpourrait être utilisé ici (encore une fois, en supposant que le navigateur moderne autorise le deuxième paramètre). En outre, le setter / getter pourrait être utilisé pour «projeter» une valeur différente pour l'objet et l'élément DOM :). Je me demande si vous avez aussi des idées sur les modèles, cela semble être un vrai défi ici, surtout pour bien structurer :)
Benjamin Gruenbaum
Tout comme mon pré-publication, je ne travaille pas beaucoup non plus avec les moteurs de modélisation côté client, désolé. :( Mais qu'entendez-vous par modifier les objets réels ? Et j'aimerais comprendre comment vous avez compris que le setter / getter pouvait être utilisé pour .... Les getters / setters ici ne servent à rien mais en redirigeant toutes les entrées et les récupérations de l'objet vers l'élément DOM, essentiellement 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'un des deux.
Kiruse
A Proxyéliminerait la nécessité d'utiliser des getters / setters, vous pouvez lier des éléments sans savoir quelles propriétés ils ont. Ce que je voulais dire, c'est que les getters peuvent changer plus que bindTo.value, ils peuvent contenir de la logique (et peut-être même un modèle). La question est de savoir comment conserver ce type de liaison bidirectionnelle avec un modèle en tête? Disons que je mappe mon objet à un formulaire, je voudrais maintenir à la fois l'élément et le formulaire synchronisés et je me demande comment je procéderais à ce sujet. Vous pouvez vérifier comment cela fonctionne sur knockout learn.knockoutjs.com/#/?tutorial=intro par exemple
Benjamin Gruenbaum
@BenjaminGruenbaum Gotcha. Je vais y jeter un œil.
Kiruse
@BenjaminGruenbaum Je vois ce que vous essayez de comprendre. La mise en place de tout cela avec des modèles à l'esprit s'avère un peu plus difficile. Je vais travailler sur ce script pendant un certain temps (et le rebaser en continu). Mais pour l'instant, je fais une pause. Je n'ai pas vraiment le temps pour ça.
Kiruse
7

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.
Donc, tout d'abord, la solution à ce problème est l'utilisation d'un modèle de conception connu sous le nom d '"observateur", il vous permet de découpler vos données de votre présentation, en faisant en sorte que le changement d'une chose soit diffusé à leurs auditeurs, mais dans ce cas c'est fait dans les deux sens.

Pour la voie DOM vers JS

Pour lier les données du DOM à l'objet js, vous pouvez ajouter du balisage sous forme d' dataattributs (ou de 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, il est accessible via js en utilisant querySelectorAll(ou le vieil ami getElementsByClassNamepour la compatibilité).

Vous pouvez maintenant lier l'événement en écoutant les modifications de différentes manières: un écouteur par objet ou un gros écouteur au conteneur / document. La liaison au document / conteneur déclenchera l'événement pour chaque modification apportée à celui-ci ou à son enfant, il aura une empreinte mémoire plus petite mais générera des appels d'événement.
Le code ressemblera à 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 voie JS do DOM

Vous aurez besoin de deux choses: un méta-objet qui contiendra les références de l'élément DOM sorcière est lié à chaque objet / attribut js et un moyen d'écouter les changements dans les objets. C'est fondamentalement la même manière: vous devez avoir un moyen d'écouter les changements dans l'objet et ensuite le lier au nœud DOM, car votre objet "ne peut pas avoir" de métadonnées, vous aurez besoin d'un autre objet qui contient les métadonnées d'une manière que le nom de la propriété correspond aux propriétés de l'objet de métadonnées. Le code ressemblera à 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 été utile.

madcampos
la source
n'y a-t-il pas de problème de comparabilité avec l'utilisation du .observer?
Mohsen Shakiba
pour l'instant il a besoin d'une cale ou d'un polyfill Object.observecar le support n'est présent qu'en chrome pour l'instant. caniuse.com/#feat=object-observe
madcampos
9
Object.observe est mort. Je pensais juste le noter ici.
Benjamin Gruenbaum
@BenjaminGruenbaum Quelle est la bonne chose à utiliser maintenant, car c'est mort?
johnny
1
@johnny si je ne me trompe pas, ce serait des pièges à proxy car ils permettent un contrôle plus granulaire de ce que je peux faire avec un objet, mais je dois enquêter là-dessus.
madcampos
7

Hier, j'ai commencé à écrire ma propre façon de lier des données.

C'est très drôle de jouer avec.

Je pense que c'est beau et très utile. Au moins lors de mes tests avec Firefox et Chrome, Edge doit aussi fonctionner. Je ne suis pas sûr des autres, mais s'ils prennent en charge Proxy, je pense que cela fonctionnera.

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

Voici le code:

(function(){
    if ( ! ( 'SmartBind' in window ) ) { // never run more than once
        // This hack sets a "proxy" property for HTMLInputElement.value set property
        var nativeHTMLInputElementValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
        var newDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
        newDescriptor.set=function( value ){
            if ( 'settingDomBind' in this )
                return;
            var hasDataBind=this.hasAttribute('data-bind');
            if ( hasDataBind ) {
                this.settingDomBind=true;
                var dataBind=this.getAttribute('data-bind');
                if ( ! this.hasAttribute('data-bind-context-id') ) {
                    console.error("Impossible to recover data-bind-context-id attribute", this, dataBind );
                } else {
                    var bindContextId=this.getAttribute('data-bind-context-id');
                    if ( bindContextId in SmartBind.contexts ) {
                        var bindContext=SmartBind.contexts[bindContextId];
                        var dataTarget=SmartBind.getDataTarget(bindContext, dataBind);
                        SmartBind.setDataValue( dataTarget, value);
                    } else {
                        console.error( "Invalid data-bind-context-id attribute", this, dataBind, bindContextId );
                    }
                }
                delete this.settingDomBind;
            }
            nativeHTMLInputElementValue.set.bind(this)( value );
        }
        Object.defineProperty(HTMLInputElement.prototype, 'value', newDescriptor);

    var uid= function(){
           return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
               var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
               return v.toString(16);
          });
   }

        // SmartBind Functions
        window.SmartBind={};
        SmartBind.BindContext=function(){
            var _data={};
            var ctx = {
                "id" : uid()    /* Data Bind Context Id */
                , "_data": _data        /* Real data object */
                , "mapDom": {}          /* DOM Mapped objects */
                , "mapDataTarget": {}       /* Data Mapped objects */
            }
            SmartBind.contexts[ctx.id]=ctx;
            ctx.data=new Proxy( _data, SmartBind.getProxyHandler(ctx, "data"))  /* Proxy object to _data */
            return ctx;
        }

        SmartBind.getDataTarget=function(bindContext, bindPath){
            var bindedObject=
                { bindContext: bindContext
                , bindPath: bindPath 
                };
            var dataObj=bindContext;
            var dataObjLevels=bindPath.split('.');
            for( var i=0; i<dataObjLevels.length; i++ ) {
                if ( i == dataObjLevels.length-1 ) { // last level, set value
                    bindedObject={ target: dataObj
                    , item: dataObjLevels[i]
                    }
                } else {    // digg in
                    if ( ! ( dataObjLevels[i] in dataObj ) ) {
                        console.warn("Impossible to get data target object to map bind.", bindPath, bindContext);
                        break;
                    }
                    dataObj=dataObj[dataObjLevels[i]];
                }
            }
            return bindedObject ;
        }

        SmartBind.contexts={};
        SmartBind.add=function(bindContext, domObj){
            if ( typeof domObj == "undefined" ){
                console.error("No DOM Object argument given ", bindContext);
                return;
            }
            if ( ! domObj.hasAttribute('data-bind') ) {
                console.warn("Object has no data-bind attribute", domObj);
                return;
            }
            domObj.setAttribute("data-bind-context-id", bindContext.id);
            var bindPath=domObj.getAttribute('data-bind');
            if ( bindPath in bindContext.mapDom ) {
                bindContext.mapDom[bindPath][bindContext.mapDom[bindPath].length]=domObj;
            } else {
                bindContext.mapDom[bindPath]=[domObj];
            }
            var bindTarget=SmartBind.getDataTarget(bindContext, bindPath);
            bindContext.mapDataTarget[bindPath]=bindTarget;
            domObj.addEventListener('input', function(){ SmartBind.setDataValue(bindTarget,this.value); } );
            domObj.addEventListener('change', function(){ SmartBind.setDataValue(bindTarget, this.value); } );
        }

        SmartBind.setDataValue=function(bindTarget,value){
            if ( ! ( 'target' in bindTarget ) ) {
                var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
                if ( 'target' in lBindTarget ) {
                    bindTarget.target=lBindTarget.target;
                    bindTarget.item=lBindTarget.item;
                } else {
                    console.warn("Still can't recover the object to bind", bindTarget.bindPath );
                }
            }
            if ( ( 'target' in bindTarget ) ) {
                bindTarget.target[bindTarget.item]=value;
            }
        }
        SmartBind.getDataValue=function(bindTarget){
            if ( ! ( 'target' in bindTarget ) ) {
                var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
                if ( 'target' in lBindTarget ) {
                    bindTarget.target=lBindTarget.target;
                    bindTarget.item=lBindTarget.item;
                } else {
                    console.warn("Still can't recover the object to bind", bindTarget.bindPath );
                }
            }
            if ( ( 'target' in bindTarget ) ) {
                return bindTarget.target[bindTarget.item];
            }
        }
        SmartBind.getProxyHandler=function(bindContext, bindPath){
            return  {
                get: function(target, name){
                    if ( name == '__isProxy' )
                        return true;
                    // just get the value
                    // console.debug("proxy get", bindPath, name, target[name]);
                    return target[name];
                }
                ,
                set: function(target, name, value){
                    target[name]=value;
                    bindContext.mapDataTarget[bindPath+"."+name]=value;
                    SmartBind.processBindToDom(bindContext, bindPath+"."+name);
                    // console.debug("proxy set", bindPath, name, target[name], value );
                    // and set all related objects with this target.name
                    if ( value instanceof Object) {
                        if ( !( name in target) || ! ( target[name].__isProxy ) ){
                            target[name]=new Proxy(value, SmartBind.getProxyHandler(bindContext, bindPath+'.'+name));
                        }
                        // run all tree to set proxies when necessary
                        var objKeys=Object.keys(value);
                        // console.debug("...objkeys",objKeys);
                        for ( var i=0; i<objKeys.length; i++ ) {
                            bindContext.mapDataTarget[bindPath+"."+name+"."+objKeys[i]]=target[name][objKeys[i]];
                            if ( typeof value[objKeys[i]] == 'undefined' || value[objKeys[i]] == null || ! ( value[objKeys[i]] instanceof Object ) || value[objKeys[i]].__isProxy )
                                continue;
                            target[name][objKeys[i]]=new Proxy( value[objKeys[i]], SmartBind.getProxyHandler(bindContext, bindPath+'.'+name+"."+objKeys[i]));
                        }
                        // TODO it can be faster than run all items
                        var bindKeys=Object.keys(bindContext.mapDom);
                        for ( var i=0; i<bindKeys.length; i++ ) {
                            // console.log("test...", bindKeys[i], " for ", bindPath+"."+name);
                            if ( bindKeys[i].startsWith(bindPath+"."+name) ) {
                                // console.log("its ok, lets update dom...", bindKeys[i]);
                                SmartBind.processBindToDom( bindContext, bindKeys[i] );
                            }
                        }
                    }
                    return true;
                }
            };
        }
        SmartBind.processBindToDom=function(bindContext, bindPath) {
            var domList=bindContext.mapDom[bindPath];
            if ( typeof domList != 'undefined' ) {
                try {
                    for ( var i=0; i < domList.length ; i++){
                        var dataTarget=SmartBind.getDataTarget(bindContext, bindPath);
                        if ( 'target' in dataTarget )
                            domList[i].value=dataTarget.target[dataTarget.item];
                        else
                            console.warn("Could not get data target", bindContext, bindPath);
                    }
                } catch (e){
                    console.warn("bind fail", bindPath, bindContext, e);
                }
            }
        }
    }
})();

Ensuite, pour régler, juste:

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

Pour l'instant, je viens d'ajouter la liaison de valeur HTMLInputElement.

Faites-moi savoir si vous savez comment l'améliorer.

tonne
la source
6

Il existe une implémentation barebones très simple de la liaison de données bidirectionnelle dans ce lien "Liaison de données bidirectionnelle facile en JavaScript"

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

Reproduire l'exemple de code ci-dessous (à partir du lien de 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 concerne l'objet JavaScript, une implémentation minimale d'un modèle utilisateur pour le bien 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 la propriété d'un modèle à un morceau de l'interface utilisateur, nous devons simplement définir un attribut de données approprié sur l'élément HTML correspondant:

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

<!-- html -->
<input type="number" data-bind-123="name" />
Nikos M.
la source
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 de référence. Les réponses de lien uniquement peuvent devenir invalides si la page liée change.
Sam Hanley
@sphanley, a noté, je mettrai probablement à jour quand j'aurai plus de temps, car c'est un code assez long pour un message de réponse
Nikos M.
@sphanley, exemple de code reproduit sur la réponse du lien référencé (bien que je thinbk cela crée du contenu duplicarte la plupart du temps, de toute façon)
Nikos M.
1
Cela crée certainement du contenu en double, mais c'est le point - les liens de blog peuvent souvent rompre avec le temps, et en dupliquant le contenu pertinent ici, il garantit qu'il sera disponible et utile pour les futurs lecteurs. La réponse est superbe maintenant!
Sam Hanley
3

La modification de la valeur d'un élément peut déclencher un événement DOM . Les écouteurs qui répondent aux événements peuvent être utilisés pour implémenter la liaison de données en JavaScript.

Par exemple:

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

Voici du code et une démo qui montre comment les éléments DOM peuvent être liés entre eux ou avec un objet JavaScript.

amuseur
la source
3

Lier n'importe quelle entrée html

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

définir deux fonctions:

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

utiliser les fonctions:

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

Voici une idée Object.definePropertyqui modifie directement la façon dont une propriété est accessible.

Code:

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

Usage:

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

p.variable="yes"

violon: ici

Thornkey
la source
2

J'ai parcouru un exemple de base de javascript en utilisant onkeypress et des gestionnaires d'événements onchange pour créer une vue de liaison avec nos js et js à afficher

Voici un exemple 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
la source
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
la source
2

Un moyen simple de lier une variable à une entrée (liaison bidirectionnelle) consiste à accéder directement à l'élément d'entrée dans le getter et le setter:

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

Et pour utiliser:

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


Une façon plus sophistiquée de faire ce qui précède sans getter / setter:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Utiliser:

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

Il s'agit d'une liaison de données bidirectionnelle très simple en javascript vanille ....

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

<div id="name">

</div>

Subodh Gawade
la source
2
cela ne fonctionnerait sûrement qu'avec l'événement onkeyup? c'est-à-dire si vous avez fait une demande ajax, puis changé le innerHTML via JavaScript, cela ne fonctionnerait pas
Zach Smith
1

Tard dans la soirée, surtout depuis que j'ai écrit 2 livres liés il y a des mois / années, je les mentionnerai plus tard, mais ça me semble toujours pertinent. Pour faire un spoiler vraiment court, les technologies de mon choix sont:

  • Proxy pour l'observation du modèle
  • MutationObserver pour le suivi des modifications du DOM (pour des raisons contraignantes, pas des modifications de valeur)
  • les changements de valeur (vue vers le flux du modèle) sont gérés via des addEventListenergestionnaires réguliers

À mon humble avis, en plus du PO, il est important que la mise en œuvre de la liaison de données:

  • gérer différents cas de cycle de vie des applications (HTML d'abord, puis JS, JS d'abord puis HTML, changement d'attributs dynamiques, etc.)
  • permettre une liaison profonde du modèle, afin que l'on puisse se lier user.address.block
  • les tableaux en tant que modèle doivent être pris en charge correctement ( shift, spliceet similaires)
  • gérer ShadowDOM
  • essayer d'être aussi facile que possible pour le remplacement de la technologie, ainsi tous les sous-langages de modèles sont une approche non favorable aux changements futurs car ils sont trop fortement couplés avec le cadre

Compte tenu de tous ces éléments, à mon avis, il est impossible de lancer simplement quelques dizaines de lignes JS. J'ai essayé de le faire comme modèle plutôt que lib - ça n'a pas marché pour moi.

Ensuite, l'avoir Object.observeest supprimé, et pourtant, étant donné que l'observation du modèle est une partie cruciale - cette partie entière DOIT être séparée des préoccupations d'une autre lib. Maintenant, au point de savoir comment j'ai pris ce problème - exactement comme OP l'a demandé:

Modèle (partie JS)

Ma prise pour l'observation du modèle est Proxy , c'est la seule façon sensée de le faire fonctionner, à mon humble avis. La fonctionnalité complète observermérite sa propre bibliothèque, j'ai donc développé une object-observerbibliothèque dans ce seul but.

Le ou les modèles doivent être enregistrés via une API dédiée, c'est le point où les POJO se transforment en Observables, vous ne pouvez voir aucun raccourci ici. Les éléments DOM qui sont considérés comme des vues liées (voir ci-dessous), sont mis à jour avec les valeurs du / des modèle (s) dans un premier temps puis à chaque changement de données.

Vues (partie HTML)

À mon humble avis, la façon la plus propre d'exprimer la liaison est via les attributs. Beaucoup l'ont fait avant et beaucoup le feront après, donc pas de nouvelles ici, c'est juste une bonne façon de le faire. Dans mon cas, je suis allé avec la syntaxe suivante:, <span data-tie="modelKey:path.to.data => targerProperty"></span>mais c'est moins important. Ce qui est important pour moi, pas de syntaxe de script complexe dans le HTML - c'est faux, encore une fois, à mon humble avis.

Tous les éléments désignés comme des vues liées doivent être collectés dans un premier temps. Du point de vue des performances, il me semble inévitable de gérer un mappage interne entre les modèles et les vues, semble un bon cas où la mémoire + une certaine gestion devraient être sacrifiées pour économiser les recherches et les mises à jour d'exécution.

Les vues sont mises à jour dans un premier temps à partir du modèle, si disponibles et lors de modifications ultérieures du modèle, comme nous l'avons dit. Plus encore, l'ensemble du DOM doit être observé au moyen de MutationObserverafin de réagir (lier / délier) sur les éléments ajoutés / supprimés / modifiés dynamiquement. De plus, tout cela doit être répliqué dans le ShadowDOM (un ouvert, bien sûr) afin de ne pas laisser de trous noirs non liés.

La liste des détails peut aller plus loin en effet, mais ce sont à mon avis les principaux principes qui rendraient la liaison de données implémentée avec un bon équilibre de l'exhaustivité des fonctionnalités d'une part et d'une simplicité saine de l'autre côté.

Et donc, en plus de ce qui est object-observermentionné ci-dessus, j'ai également écrit une data-tierbibliothèque qui implémente la liaison de données le long des concepts mentionnés ci-dessus.

GullerYA
la source
0

Les choses ont beaucoup changé au cours des 7 dernières années, nous avons maintenant des composants Web natifs dans la plupart des navigateurs. OMI, le cœur du problème est le partage d'état entre les éléments, une fois que vous avez que c'est trivial de mettre à jour l'interface utilisateur lorsque l'état change et vice versa.

Pour partager des données entre des éléments, vous pouvez créer une classe StateObserver et étendre vos composants Web à partir de cela. Une implémentation minimale ressemble à ceci:

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

jouer du violon ici

J'aime cette approche car:

  • pas de traversée dom pour trouver des data-propriétés
  • aucun Object.observe (obsolète)
  • pas de proxy (qui fournit un crochet mais aucun mécanisme de communication de toute façon)
  • pas de dépendances, (autre qu'un polyfill selon vos navigateurs cibles)
  • il est raisonnablement centralisé et modulaire ... décrivant l'état en html, et avoir des auditeurs partout deviendrait très rapidement désordonné.
  • c'est extensible. Cette implémentation de base est composée de 20 lignes de code, mais vous pouvez facilement créer une certaine commodité, une immuabilité et une magie de forme d'état pour la rendre plus facile à utiliser.
Mr5o1
la source