Як реалізувати прив'язку даних 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).

Що я спробував?

Я великий шанувальник вусів, тому я спробував використовувати його для створення шаблонів. Однак я зіткнувся з проблемами, намагаючись виконати прив'язку даних, оскільки вуса обробляє HTML як рядок, тому після отримання його результату я не маю посилання на те, де знаходяться об'єкти в моїй програмі перегляду. Єдине вирішення, про яке я міг подумати, - це зміна HTML-рядка (або створеного дерева DOM) з атрибутами. Я не проти використовувати інший двигун шаблонів.

В основному, у мене є сильне відчуття, що я ускладнюю проблему і є просте рішення.

Примітка. Будь ласка, не надайте відповіді, які використовують зовнішні бібліотеки, особливо ті, що мають тисячі рядків коду. Я використовував (і як!) AngularJS та KnockoutJS. Я дуже не хочу відповідей у ​​формі "використовувати frame x". Оптимально, я хотів би, щоб майбутній читач, який не знає, як використовувати багато рамок, щоб зрозуміти, як реалізувати двонаправлену прив'язку даних. Я не сподіваюсь на повну відповідь, але та, яка отримає ідею впоперек.


2
Я базував CrazyGlue на дизайні Бенджаміна Грюнбаума. Він також підтримує SELECT, прапорці та радіотеги. jQuery - залежність.
JohnSz

12
Це питання абсолютно приголомшливе. Якщо вона коли-небудь закриється за тему, що не стосується теми, або якусь іншу дурницю, я буду серйозно поставлений галочкою.
OCDev

@JohnSz дякуємо, що згадали про ваш проект CrazyGlue. Я довго шукав простий двосторонній зв'язувач даних. Схоже, ви не використовуєте Object.observe, тому підтримка вашого браузера має бути чудовою. І ви не використовуєте шаблони для вусів, щоб вони були ідеальними.
Гавін

@Benjamin Що ти закінчив?
Джонні

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

DEMO: http://jsfiddle.net/RkTMD/


5
+1 Дуже чистий підхід, дуже просто кажучи і досить простий, щоб люди могли навчитися, набагато чистіший, ніж у мене. Загальним випадком використання є використання шаблонів у коді для представлення поглядів об'єктів. Мені було цікаво, як це може тут працювати? У таких двигунах, як "Вуса" я щось роблю Mustache.render(template,object), припускаючи, що я хочу тримати об'єкт синхронізованим з шаблоном (не характерним для Вуса), як би я продовжував це робити?
Бенджамін Груенбаум

3
@BenjaminGruenbaum: Я не використовував шаблони на стороні клієнта, але думаю, що у вуса є якийсь синтаксис для ідентифікації точок вставки, і що цей синтаксис містить мітку. Тому я думаю, що "статичні" частини шаблону будуть перетворені на шматки HTML, що зберігаються в масиві, і динамічні частини будуть проходити між цими фрагментами. Тоді мітки на точках вставки використовуватимуться як властивості об'єкта. Тоді, якщо деякі inputмають оновити одну з цих точок, було б відображення від входу до цієї точки. Я побачу, чи можу я привести швидкий приклад.

1
@BenjaminGruenbaum: Хммм ... я не думав, як чітко координувати два різні елементи. Це трохи більше, ніж я думав спочатку. Мені цікаво, тому мені, можливо, доведеться попрацювати над цим трохи пізніше. :)

2
Ви побачите, що є первинний Templateконструктор, який виконує розбір, вміщує різні MyCtorоб'єкти та надає інтерфейс для оновлення кожного за його ідентифікатором. Повідомте мене, якщо у вас є питання. :) EDIT: ... скористайтеся цим посиланням натомість ... Я забув, що у мене експоненціальне збільшення вхідного значення кожні 10 секунд демонструє оновлення JS. Це обмежує його.

2
... повністю коментована версія плюс незначні вдосконалення.

36

Отже, я вирішив кинути власне рішення в горщик. Ось робоча скрипка . Зауважте, це працює лише у дуже сучасних браузерах.

Що він використовує

Ця реалізація дуже сучасна - вона вимагає (дуже) сучасного браузера та користувачів двох нових технологій:

  • MutationObservers для виявлення змін у домі (також використовуються слухачі подій)
  • Object.observeдля виявлення змін в об'єкті та сповіщення дому. Небезпека, оскільки ця відповідь була написана. Оо було обговорено та вирішено проти ТК ECMAScript, розглянемо полізасипку .

Як це працює

  • На елемент покладіть 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);

Ось робоча скрипка . Зауважте, що це рішення є досить загальним. Доступно мерехтіння спостерігача за об'єктом спостереження та мутації.


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/


2
Якби у нас були Proxyоб’єкти Гармонії :) Встановлення здається непоганою ідеєю, але хіба це не вимагатиме від нас модифікації фактичних об'єктів? Також, на стороні примітки - тут Object.createможна було б використовувати (знову ж таки, припускаючи сучасний браузер, який дозволений для другого параметра). Крім того, сеттер / геттер може використовуватися для "проектування" різного значення на об'єкт та елемент DOM :). Мені цікаво, чи маєте ви якусь думку щодо створення шаблонів, це здається справжнім викликом, особливо, щоб гарно структурувати :)
Бенджамін Груенбаум

Так само, як і мій передмовник, я теж не дуже працюю з двигунами-шаблонами на стороні клієнта, вибачте. :( Але що ви маєте на увазі під зміною фактичних об’єктів ? І я хотів би зрозуміти ваші думки про те, як ви зрозуміли, що сеттер / геттер може бути використаний для .... Геттери / сетери тут не використовуються ні для чого але перенаправлення всіх вхідних даних і вилучень з об'єкта на елемент DOM, в основному як a Proxy, як ви сказали.;) Я зрозумів, що завдання полягає в тому, щоб зберегти два різних властивості синхронізовано. Мій метод усуває одну з обох.
Кірусе

A Proxyвиключить необхідність використання геттерів / сеттерів, ви можете зв'язати елементи, не знаючи, якими властивостями вони володіють. Що я мав на увазі, це те, що користувачі можуть змінити більше, ніж bindTo.значення, вони можуть містити логіку (а може бути, навіть шаблон). Питання полягає в тому, як підтримувати такий тип двонаправленого прив’язки за допомогою шаблону на увазі? Скажімо, я співставляю об’єкт із формою, я б хотів підтримувати і елемент, і форму синхронізованою, і мені цікаво, як би я продовжував подібне. Ви можете дізнатися, як це працює у нокауті 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);

Я сподіваюся, що мені допомогли.


Хіба не існує проблеми порівняння з використанням .observer?
Мохсен Шакіба

наразі йому потрібна обшивка або поліфіл, Object.observeоскільки підтримка зараз представлена ​​лише в хромі. caniuse.com/#feat=object-observe
madcampos

9
Об'єкт.спостереження мертвий. Просто подумав, що тут зазначу.
Бенджамін Грюнбаум

@BenjaminGruenbaum Що правильно використовувати зараз, оскільки це мертве?
johnny

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, то мінімальною реалізацією моделі користувача заради цього експерименту може бути наступне:

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, відтворив зразок коду у відповідь із посилань (хоча я thinbk це створює дублікартний контент більшу частину часу, все одно)
Nikos M.

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.


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"

скрипка: Ось


2

Я переглянув декілька основних прикладів Javascript, використовуючи обробники подій onkeypress та onchange для створення прив'язного перегляду до наших js та js для перегляду

Ось приклад 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>

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


Нахабніший спосіб зробити вищезазначене без геттера / сетера:

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.

1

Дуже просто двостороння прив'язка даних у ванільному JavaScript ....

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

<div id="name">

</div>


2
напевно, це спрацювало б лише з подією onkeyup? тобто якщо ви зробили запит на ajax, а потім змінили внутрішнійHTML через JavaScript, то це не буде працювати
Зак Сміт,

1

Пізно до вечірки, тим більше, що я написав 2 лійки, пов'язані місяці / роки тому, я згадаю їх пізніше, але все ще виглядає для мене актуальною. Щоб зробити це дійсно коротким спойлером, на мій вибір:

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

IMHO, крім ОП, важливо, щоб реалізація обов'язкових даних:

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

Враховуючи все це, на мою думку, неможливо просто кинути кілька десятків ліній JS. Я намагався зробити це як шаблон , а не ліб - для мене не вийшло.

Далі, маючи Object.observe вилучення та, враховуючи, що спостереження за моделлю є найважливішою частиною, - ця вся частина ОБОВ'ЯЗКОВО розділена на іншу область. Тепер до суті принципів, як я сприйняв цю проблему - саме так, як запитав ОП:

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

Моє враження про моделювання спостереження - це проксі , це єдиний розумний спосіб змусити його працювати, IMHO. Повністю представлений observerзаслуговує на те, що це власна бібліотека, тому я розвинувсяobject-observer бібліотеку лише для цієї мети.

Моделі (и) повинні бути зареєстровані за допомогою спеціального API, саме в цей момент перетворюються POJO Observable s, тут не видно жодного ярлика. Елементи DOM, які вважаються пов'язаними поданнями (див. Нижче), спочатку оновлюються значеннями моделей / с, а потім після кожної зміни даних.

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

IMHO, найчистіший спосіб виразити зв'язок, здійснюється через атрибути. Багато хто робив це раніше, і багато хто буде робити після, тому ніяких новин тут немає, це просто правильний спосіб зробити це. У моєму випадку я перейшов із таким синтаксисом:, <span data-tie="modelKey:path.to.data => targerProperty"></span>але це менш важливо. Що таке важливо для мене, що не складний синтаксис сценаріїв в HTML - це не так, знову ж , ІМХО.

Усі елементи, призначені для зв'язаних поглядів, збираються спочатку. Мені здається неминучим з боку продуктивності керувати деяким внутрішнім відображенням між моделями та видами, здається, правильний випадок, коли пам'ять + деяке управління повинні бути принесені в жертву для економії пошуку та оновлень.

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

Перелік особливостей може йти далі, але, на мою думку, це основні принципи, які зробили б прив'язку даних реалізованими з хорошим балансом повноти функцій з одного та розумною простотою з іншого боку.

Таким чином, крім object-observerзгаданого вище, я написав справді також data-tierбібліотеку, яка реалізує прив'язку даних уздовж вищезгаданих концепцій.


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 рядків коду, але ви можете легко створити деяку зручність, незмінність і магію форми штату, щоб полегшити роботу.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.