Как реализовать связывание данных DOM в JavaScript

244

Пожалуйста, относитесь к этому вопросу как к чисто образовательному. Мне все еще интересно услышать новые ответы и идеи для реализации этого

ТЛ; др

Как мне реализовать двунаправленное связывание данных с JavaScript?

Привязка данных к DOM

Под привязкой данных к DOM я имею в виду, например, наличие объекта JavaScript aсо свойством b. Затем наличие <input>элемента DOM (например), когда элемент DOM изменяется, aизменяется и наоборот (то есть я имею в виду двунаправленную привязку данных).

Вот диаграмма AngularJS о том, как это выглядит:

двусторонняя привязка данных

Так что в основном у меня есть JavaScript, похожий на:

var a = {b:3};

Затем элемент ввода (или другой формы), такой как:

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

Я хотел бы, чтобы значение ввода было a.bзначением (например), и когда изменяется текст ввода, я бы тоже хотел a.bизменить. Когда a.bизменения в JavaScript, входные данные изменяются.

Вопрос

Каковы некоторые основные методы для достижения этой цели в простом JavaScript?

В частности, я хотел бы получить хороший ответ, чтобы сослаться на:

  • Как будет работать привязка для объектов?
  • Как может работать прослушивание изменений в форме?
  • Можно ли простым способом изменить только HTML на уровне шаблона? Я хотел бы не отслеживать привязку в самом HTML-документе, а только в JavaScript (с событиями DOM и JavaScript, сохраняющими ссылку на используемые элементы DOM).

Что я пробовал?

Я большой поклонник усов, поэтому я попытался использовать его для шаблонов. Тем не менее, я столкнулся с проблемами при попытке выполнить привязку данных сам, так как Mustache обрабатывает HTML как строку, поэтому после получения результата у меня нет ссылки на то, где находятся объекты в моей модели представления. Единственный обходной путь, о котором я мог подумать, - это изменение самой строки HTML (или создание дерева DOM) с помощью атрибутов. Я не против использовать другой шаблонизатор.

По сути, у меня возникло сильное ощущение, что я усложняю проблему, и есть простое решение.

Примечание. Пожалуйста, не предоставляйте ответы, которые используют внешние библиотеки, особенно те, которые состоят из тысяч строк кода. Я использовал (и мне нравится!) AngularJS и KnockoutJS. Я действительно не хочу ответов в форме «использовать рамки х». Оптимально, я бы хотел, чтобы будущий читатель не знал, как использовать многие фреймворки, чтобы понять, как реализовать двунаправленную привязку данных. Я не ожидаю полного ответа, но тот, который передает идею.

Бенджамин Грюнбаум
источник
2
Я основал CrazyGlue на дизайне Бенджамина Грюнбаума. Он также поддерживает SELECT, флажок и радио-теги. JQuery это зависимость.
JohnSz
12
Этот вопрос совершенно потрясающий. Если это когда-нибудь закроют за то, что он не по теме или какой-то другой глупости, я буду серьезно отмечен галочкой.
OCDev
@JohnSz спасибо за упоминание вашего проекта CrazyGlue. Я долго искал простую двухстороннюю привязку данных. Похоже, вы не используете Object.observe, поэтому поддержка вашего браузера должна быть отличной. И вы не используете шаблоны для усов, так что это идеально.
Гэвин
@ Бенджамин Что ты в итоге сделал?
Джонни
@ johnny, на мой взгляд, правильный подход заключается в создании DOM в JS (например, React), а не наоборот. Я думаю, что в конечном итоге это то, что мы будем делать.
Бенджамин Грюнбаум

Ответы:

106
  • Как будет работать привязка для объектов?
  • Как может работать прослушивание изменений в форме?

Абстракция, которая обновляет оба объекта

Я предполагаю, что есть и другие методы, но в конечном итоге у меня будет объект, который содержит ссылку на связанный элемент DOM и предоставляет интерфейс, который координирует обновления своих собственных данных и соответствующего элемента.

.addEventListener()Обеспечивает очень хороший интерфейс для этого. Вы можете дать ему объект, который реализует eventListenerинтерфейс, и он вызовет свои обработчики с этим объектом в качестве thisзначения.

Это дает вам автоматический доступ как к элементу, так и к связанным с ним данным.

Определение вашего объекта

Наследование прототипов - хороший способ реализовать это, хотя, конечно, не обязательно. Сначала вы должны создать конструктор, который получает ваш элемент и некоторые исходные данные.

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

Поэтому здесь конструктор хранит элемент и данные о свойствах нового объекта. Это также связывает changeсобытие с данным element. Интересно то, что он передает новый объект вместо функции в качестве второго аргумента. Но это само по себе не сработает.

Реализация eventListenerинтерфейса

Чтобы это работало, ваш объект должен реализовать eventListenerинтерфейс. Все, что нужно для этого - дать объекту handleEvent()метод.

Вот где приходит наследство.

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

Есть много разных способов, которыми это можно структурировать, но для вашего примера координации обновлений я решил, что change()метод должен принимать только значение и handleEventпередавать его вместо объекта события. Таким образом, change()можно вызывать и без события.

Так что теперь, когда changeсобытие происходит, оно обновит и элемент, и .dataсвойство. И то же самое произойдет, когда вы вызовете .change()в своей программе JavaScript.

Используя код

Теперь вы просто создадите новый объект и позволите ему выполнять обновления. Обновления в коде JS появятся на входе, а события изменения на входе будут видны для кода 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);

ДЕМО: http://jsfiddle.net/RkTMD/

user1106925
источник
5
+1 Очень чистый подход, очень простой и достаточно простой, чтобы люди могли учиться, намного чище, чем у меня. Распространенным вариантом использования является использование шаблонов в коде для представления видов объектов. Мне было интересно, как это может работать здесь? В таких движках, как Mustache, я делаю что-то Mustache.render(template,object), предполагая, что я хочу синхронизировать объект с шаблоном (не специфичным для Mustache), как бы я продолжил это?
Бенджамин Грюнбаум
3
@BenjaminGruenbaum: я не использовал шаблоны на стороне клиента, но я бы предположил, что усы имеют некоторый синтаксис для определения точек вставки, и что этот синтаксис включает метку. Поэтому я думаю, что «статические» части шаблона будут преобразованы в фрагменты HTML, хранящиеся в массиве, и динамические части будут проходить между этими фрагментами. Тогда метки на точках вставки будут использоваться в качестве свойств объекта. Затем, если кто-то inputобновит одну из этих точек, будет сопоставление от входа до этой точки. Я посмотрю, смогу ли я привести быстрый пример.
1
@BenjaminGruenbaum: Хммм ... Я не думал о том, как правильно координировать два разных элемента. Это немного сложнее, чем я думал сначала. Мне любопытно, поэтому, возможно, мне придется поработать над этим чуть позже. :)
2
Вы увидите, что есть основной Templateконструктор, который выполняет синтаксический анализ, содержит различные MyCtorобъекты и предоставляет интерфейс для обновления каждого по его идентификатору. Дайте мне знать, если у вас есть вопросы. :) РЕДАКТИРОВАТЬ: ... используйте эту ссылку вместо этого ... Я забыл, что у меня экспоненциальное увеличение входного значения каждые 10 секунд, чтобы продемонстрировать обновления JS. Это ограничивает это.
2
... полностью прокомментированная версия плюс небольшие улучшения.
36

Итак, я решил бросить свое собственное решение в банк. Вот рабочая скрипка . Обратите внимание, что это работает только в очень современных браузерах.

Что он использует

Эта реализация очень современна - для этого требуется (очень) современный браузер и пользователи двух новых технологий:

  • MutationObservers для обнаружения изменений в домене (также используются слушатели событий)
  • Object.observeобнаруживать изменения в объекте и уведомлять дом. Опасность, так как этот ответ был написан. Oo обсуждался и решался ECMAScript TC, рассмотрите возможность заполнения .

Как это устроено

  • На элемент поставьте domAttribute:objAttributeотображение - напримерbind='textContent:name'
  • Прочтите это в функции dataBind. Наблюдайте за изменениями как элемента, так и объекта.
  • Когда произойдет изменение - обновите соответствующий элемент.

Решение

Вот dataBindфункция, обратите внимание, что это всего лишь 20 строк кода и может быть короче:

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

Вот некоторое использование:

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

Вот рабочая скрипка . Обратите внимание, что это решение довольно общее. Object.observe и мутации наблюдателя шимминга доступны.

Бенджамин Грюнбаум
источник
1
Я просто случайно написал это (es5) для удовольствия, если кто-нибудь найдет это полезным - вырубите
Бенджамин Грюнбаум
1
Имейте в виду, что когда obj.nameесть сеттер, его нельзя наблюдать извне, но он должен сообщить, что он изменился из сеттера - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - своего рода бросает гаечный ключ в работах для Oo (), если вы хотите более сложное, взаимозависимое поведение с использованием сеттеров. Кроме того, когда obj.nameэто не настраивается, переопределение его установщика (с различными приемами для добавления уведомлений) также не допускается - поэтому дженерики с Oo () полностью отменяются в этом конкретном случае.
Ноло
8
Object.observe удален из всех браузеров: caniuse.com/#feat=object-observe
JvdBerg,
1
Прокси может использоваться вместо Object.observe или github.com/anywhichway/proxy-observe или gist.github.com/ebidel/1b553d571f924da2da06 или более старых полифилов, также на github @JvdBerg
jimmont
29

Я хотел бы добавить к своему составителю. Я предлагаю немного другой подход, который позволит вам просто присвоить новое значение вашему объекту без использования метода. Однако следует отметить, что это не поддерживается особенно старыми браузерами, и IE9 все еще требует использования другого интерфейса.

В частности, мой подход не использует события.

Добытчики и сеттеры

Мое предложение использует сравнительно молодую функцию геттеров и сеттеров , особенно только сеттеров. Вообще говоря, мутаторы позволяют нам «настраивать» поведение того, как определенным свойствам присваивается значение и производится их извлечение.

Одна из реализаций, которую я буду использовать здесь - это метод Object.defineProperty . Это работает в FireFox, GoogleChrome и - я думаю - IE9. Не проверял другие браузеры, но так как это только теория ...

В любом случае, он принимает три параметра. Первый параметр - это объект, для которого вы хотите определить новое свойство, второй - строка, напоминающая имя нового свойства, а последний - «объект дескриптора», предоставляющий информацию о поведении нового свойства.

Два особенно интересных дескриптора есть getи set. Пример будет выглядеть примерно так: Обратите внимание, что использование этих двух запрещает использование других 4 дескрипторов.

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

Теперь использование этого становится немного другим:

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

Я хочу подчеркнуть, что это работает только для современных браузеров.

Рабочая скрипка: http://jsfiddle.net/Derija93/RkTMD/1/

Kiruse
источник
2
Если бы только у нас были Proxyобъекты Harmony :) Сеттеры кажутся хорошей идеей, но разве это не потребует от нас модификации реальных объектов? Кроме того, на заметку - Object.createздесь можно использовать (опять же, если принять во внимание современный браузер, который допускает второй параметр). Кроме того, setter / getter может использоваться для «проецирования» другого значения на объект и элемент DOM :). Я задаюсь вопросом, есть ли у вас какие-либо идеи по поводу шаблонов, это кажется настоящим испытанием, особенно для хорошей структуры :)
Бенджамин Грюнбаум
Извините, я, как и мой предатель, не слишком много работаю с шаблонизаторами на стороне клиента. :( Но что вы подразумеваете под изменением реальных объектов ? И я хотел бы понять ваши мысли о том, как вы поняли, что setter / getter может использоваться для .... Здесь getters / setters используются ни для чего. но перенаправляя все входные данные и извлечения из объекта в элемент DOM, в основном как a Proxy, как вы сказали.;) Я понял, что задача состоит в том, чтобы синхронизировать два разных свойства. Мой метод устраняет одно из обоих.
Кирусе
А Proxyизбавит от необходимости использовать геттеры / сеттеры, вы можете связывать элементы, не зная, какими свойствами они обладают. Я имел в виду, что геттеры могут изменить больше, чем bindTo.value, они могут содержать логику (и, возможно, даже шаблон). Вопрос в том, как поддерживать двустороннюю привязку такого типа с учетом шаблона? Допустим, я сопоставляю свой объект с формой, я хотел бы сохранить синхронизацию как элемента, так и формы, и мне интересно, как бы я поступил с такими вещами. Вы можете проверить, как это работает на нокауте learn.knockoutjs.com/#/?tutorial=intro, например
Бенджамин Грюнбаум
@BenjaminGruenbaum Gotcha. Я посмотрю.
Кирусе
@BenjaminGruenbaum Я понимаю, что вы пытаетесь понять. Настройка всего этого с учетом шаблонов оказывается немного сложнее. Я поработаю над этим сценарием некоторое время (и постоянно буду перебазировать его). Но сейчас я делаю перерыв. На самом деле у меня нет времени на это.
Кирусе
7

Я думаю, что мой ответ будет более техническим, но не отличаться, так как другие представляют то же самое, используя разные методы.
Итак, обо всем по порядку, решение этой проблемы - использование шаблона проектирования, известного как «наблюдатель», он позволяет вам отделить ваши данные от презентации, сделав так, чтобы изменения в одной вещи передавались их слушателям, но в этом случае это сделано в двух направлениях.

Для DOM в JS путь

Чтобы привязать данные из DOM к объекту js, вы можете добавить разметку в форме dataатрибутов (или классов, если вам нужна совместимость), например:

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

Таким образом, к нему можно получить доступ через js используя querySelectorAll(или старого друга getElementsByClassNameдля совместимости).

Теперь вы можете привязать событие, слушая изменения, следующими способами: один слушатель на объект или один большой слушатель к контейнеру / документу. Привязка к документу / контейнеру вызовет событие для каждого изменения, внесенного в него или его дочерний элемент, уменьшит объем памяти, но вызовет вызовы событий.
Код будет выглядеть примерно так:

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

Для JS сделать DOM способ

Вам понадобятся две вещи: один мета-объект, который будет содержать ссылки на элемент ведьмы DOM, привязан к каждому объекту / атрибуту js и способ прослушивания изменений в объектах. Это в основном то же самое: вы должны иметь возможность прослушивать изменения в объекте и затем связывать его с узлом DOM, так как ваш объект «не может иметь» метаданные, вам понадобится другой объект, содержащий метаданные таким образом, что имя свойства отображается на свойства объекта метаданных. Код будет примерно таким:

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

Я надеюсь, что я помог.

madcampos
источник
Нет ли проблем с сопоставимостью при использовании .observer?
Мохсен Шакиба
на данный момент он нуждается в прокладке или полифилле, Object.observeпоскольку поддержка пока доступна только в Chrome. caniuse.com/#feat=object-observe
madcampos
9
Object.observe мертв. Просто подумал, что заметил бы это здесь.
Бенджамин Грюнбаум
@BenjaminGruenbaum Что правильно использовать сейчас, так как он мертв?
Джонни
1
@johnny, если я не ошибаюсь, это были бы прокси-ловушки, поскольку они позволяют более детально контролировать то, что я могу сделать с объектом, но я должен исследовать это.
madcampos
7

Вчера я начал писать свой собственный способ привязки данных.

С ним очень весело играть.

Я думаю, что это красиво и очень полезно. По крайней мере, в моих тестах, использующих Firefox и Chrome, Edge тоже должен работать. Не уверен насчет других, но если они поддерживают Прокси, думаю, это сработает.

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

Вот код:

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

Затем, чтобы установить, просто:

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

Сейчас я только что добавил привязку значения HTMLInputElement.

Дайте мне знать, если вы знаете, как его улучшить.

тонна
источник
6

В этой ссылке «Простое двухстороннее связывание данных в JavaScript» очень простая базовая реализация двухсторонней привязки данных.

Предыдущая ссылка вместе с идеями из knockoutjs, backbone.js и agility.js привела к этой легкой и быстрой инфраструктуре MVVM, ModelView.js основанный на JQuery который хорошо сочетается с jQuery и из которого я скромный (или, возможно, не такой скромный) автор.

Воспроизведение приведенного ниже примера кода (по ссылке в блоге ):

Пример кода для 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;
}

Что касается объекта JavaScript, минимальная реализация модели User для этого эксперимента может быть следующей:

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

Теперь, когда мы хотим связать свойство модели с частью пользовательского интерфейса, мы просто должны установить соответствующий атрибут данных для соответствующего элемента HTML:

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

<!-- html -->
<input type="number" data-bind-123="name" />
Никос М.
источник
Хотя эта ссылка может ответить на вопрос, лучше включить сюда основные части ответа и предоставить ссылку для справки. Ответы, содержащие только ссылки, могут стать недействительными, если связанная страница изменится.
Сэм Хэнли
@sphanley, отметил, я, вероятно, обновлю, когда у меня будет больше времени, так как это довольно длинный код для ответного сообщения
Никос М.
@sphanley, воспроизведенный пример кода в ответе по ссылочной ссылке (хотя в любом случае я создаю дублированный контент в большинстве случаев)
Никос М.
1
Это определенно создает дублированный контент, но в этом-то и дело - ссылки на блог часто могут порваться со временем, а дублируя соответствующий контент, он гарантирует, что он будет доступен и полезен для будущих читателей. Ответ выглядит великолепно сейчас!
Сэм Хэнли
3

Изменение значения элемента может вызвать событие DOM . Слушатели, которые отвечают на события, могут использоваться для реализации привязки данных в JavaScript.

Например:

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

Вот код и демонстрация, которая показывает, как элементы DOM могут быть связаны друг с другом или с объектом JavaScript.

amusingmaker
источник
3

Привязать любой ввод HTML

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

определить две функции:

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

использовать функции:

var myObject = proxify('element-to-bind')
bindValue(myObject);
Олли Уильямс
источник
3

Вот идея использования, Object.definePropertyкоторая напрямую изменяет способ доступа к свойству.

Код:

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

Использование:

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

p.variable="yes"

скрипка: здесь

Thornkey
источник
2

Я рассмотрел базовый пример javascript с использованием обработчиков событий onkeypress и onchange для привязки представления к нашим js и js для просмотра

Вот пример плункера http://plnkr.co/edit/7hSOIFRTvqLAvdZT4Bcc?p=preview

<!DOCTYPE html>
<html>
<body>

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

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

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

    <script>

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

            myFunction();

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

            myFunction();

        }

        function myFunction1() {

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

</body>
</html>
Мака Девендер
источник
2
<!DOCTYPE html>
<html>
<head>
    <title>Test</title>
</head>
<body>

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

</script>
</body>
</html>
Энтони Ньюлайнинфо
источник
2

Простой способ привязки переменной к входу (двусторонняя привязка) - просто получить прямой доступ к элементу ввода в методах получения и установки:

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

В HTML:

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

И использовать:

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


Причудливый способ сделать вышеупомянутое без getter / setter:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Использовать:

var v1 = new variable(document.getElementById("an-input"));
v1(10); // sets value to 20.
console.log(v1()); // reads value.
A-Sharabiani
источник
1

Это очень простая двусторонняя привязка данных в vanilla javascript ....

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

<div id="name">

</div>

Subodh Gawade
источник
2
конечно, это будет работать только с событием onkeyup? то есть, если вы выполнили запрос ajax, а затем изменили innerHTML через JavaScript, это не сработает
Зак Смит,
1

Поздно на вечеринке, особенно с учетом того, что я написал 2 libs, связанные месяцы / годы назад, я упомяну их позже, но все равно выглядит актуально для меня. Чтобы сделать его действительно коротким спойлером, технологии на мой выбор:

  • Proxy для наблюдения за моделью
  • MutationObserver для отслеживания изменений DOM (по причинам связывания, а не изменениям значений)
  • изменения значений (просмотр потока модели) обрабатываются с помощью обычных addEventListenerобработчиков

ИМХО, в дополнение к ФП, важно, чтобы реализация привязки данных:

  • обрабатывать разные случаи жизненного цикла приложения (сначала HTML, затем JS, сначала JS, затем HTML, динамические атрибуты и т. д.)
  • позволяют глубокое связывание модели, так что можно связать user.address.block
  • массивы как модель должны поддерживаться корректно ( shift,splice и тому подобное)
  • обрабатывать ShadowDOM
  • пытаться быть настолько простым для замены технологии, насколько это возможно, поэтому любые шаблонные подъязыки являются подходом, не зависящим от будущих изменений, поскольку он слишком тесно связан с каркасом

Учитывая все это, по моему мнению, невозможно просто выбросить несколько десятков линий JS. Я пытался сделать это как шаблон, а не как lib - у меня не получилось.

Затем, после Object.observeудаления, и, тем не менее, учитывая, что наблюдение за моделью является критической частью - вся эта часть ДОЛЖНА быть разделена по интересам для другой библиотеки. Теперь о принципах того, как я решил эту проблему - именно так, как ОП спросил:

Модель (часть JS)

Для наблюдения за моделями я беру прокси , это единственный разумный способ заставить его работать, ИМХО. Полнофункциональный observerзаслуживает своей собственной библиотеки, поэтому я разработалobject-observer библиотеку для этой единственной цели.

Модели должны регистрироваться через какой-то специальный API, и именно здесь POJO превращаются в Observable s, здесь не видно ярлыков. Элементы DOM, которые считаются связанными представлениями (см. Ниже), обновляются сначала значениями модели, а затем при каждом изменении данных.

Представления (часть HTML)

ИМХО, самый чистый способ выразить привязку, это через атрибуты. Многие делали это раньше, а многие будут делать после, так что никаких новостей здесь, это просто правильный способ сделать это. В моем случае я использовал следующий синтаксис:, <span data-tie="modelKey:path.to.data => targerProperty"></span>но это менее важно. Что это важно для меня, не сложный синтаксис сценариев в HTML - это не так, опять же , ИМХО.

Все элементы, обозначенные как связанные виды, должны быть собраны вначале. С точки зрения производительности, мне кажется неизбежным управлять каким-то внутренним отображением между моделями и представлениями, кажется правильным случаем, когда память + некоторое управление должны быть принесены в жертву для сохранения поисков и обновлений во время выполнения.

Представления обновляются сначала из модели, если она доступна, и при последующих изменениях модели, как мы уже говорили. Более того, весь DOM следует наблюдать с помощью MutationObserver, чтобы реагировать (связывать / отменять) на динамически добавляемые / удаляемые / измененные элементы. Кроме того, все это должно быть скопировано в ShadowDOM (конечно, открытый), чтобы не оставлять несвязанных черных дыр.

Список спецификаций действительно может пойти дальше, но это, на мой взгляд, основные принципы, которые позволили бы реализовать привязку данных с хорошим балансом полноты функций с одной и вразумительной простотой с другой стороны.

И поэтому, в дополнение к object-observerупомянутому выше, я написал также data-tierбиблиотеку, которая реализует привязку данных в соответствии с вышеупомянутыми концепциями.

GullerYA
источник
0

За последние 7 лет многое изменилось, сейчас у нас есть встроенные веб-компоненты в большинстве браузеров. IMO Суть проблемы заключается в разделении состояния между элементами, если у вас есть тривиальное обновление пользовательского интерфейса при изменении состояния и наоборот.

Для обмена данными между элементами вы можете создать класс StateObserver и расширить свои веб-компоненты. Минимальная реализация выглядит примерно так:

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

возиться здесь

Мне нравится этот подход, потому что:

  • нет обхода дом, чтобы найти data-свойства
  • нет Object.observe (не рекомендуется)
  • нет прокси (который обеспечивает ловушку, но в любом случае нет механизма связи)
  • нет зависимостей (кроме полифилла в зависимости от ваших целевых браузеров)
  • он достаточно централизованный и модульный ... описывающий состояние в html, а наличие слушателей повсюду очень быстро запутывается.
  • это расширяемое. Эта базовая реализация состоит из 20 строк кода, но вы можете легко создать некоторое удобство, неизменность и магию формы состояний, чтобы с ними было легче работать.
Mr5o1
источник