วิธีการใช้การเชื่อมโยงข้อมูล DOM ใน JavaScript


244

โปรดปฏิบัติต่อคำถามนี้เพื่อการศึกษาอย่างเคร่งครัด ฉันยังคงสนใจที่จะรับฟังคำตอบและแนวคิดใหม่ ๆ

TL; DR

ฉันจะใช้การเชื่อมโยงข้อมูลแบบสองทิศทางกับ 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 ฉันไม่ต้องการคำตอบในรูปแบบ 'use framework x' อย่างดีที่สุดฉันต้องการผู้อ่านในอนาคตที่ไม่ทราบวิธีการใช้กรอบงานจำนวนมากเพื่อเข้าใจวิธีการใช้ข้อมูลผูกมัดตัวเองแบบสองทิศทาง ฉันไม่ได้คาดหวังคำตอบที่สมบูรณ์แต่เป็นคำตอบที่ได้รับความคิด


2
ฉันใช้CrazyGlueตามการออกแบบของ Benjamin Gruenbaum นอกจากนี้ยังรองรับ SELECT, ช่องทำเครื่องหมายและแท็กวิทยุ jQuery เป็นการพึ่งพา
JohnSz

12
คำถามนี้ยอดเยี่ยมมาก ถ้ามันถูกปิดเพราะไม่ได้หัวข้อหรือเรื่องไร้สาระอื่น ๆ ฉันจะถูกติ๊กอย่างจริงจัง
OCDev

@ JohnSz ขอบคุณที่พูดถึงโครงการ CrazyGlue ของคุณ ฉันค้นหาเครื่องผูกข้อมูลแบบ 2 ทางมาเป็นเวลานานแล้ว ดูเหมือนว่าคุณไม่ได้ใช้ Object.observe ดังนั้นการสนับสนุนเบราว์เซอร์ของคุณจึงยอดเยี่ยม และคุณไม่ได้ใช้ templating หนวดจึงสมบูรณ์แบบ
Gavin

@Benjamin คุณทำอะไรลงไปบ้าง?
johnny

@ จอห์นนี่ในความคิดของฉันวิธีที่ถูกต้องคือการสร้าง DOM ใน JS (เช่น React) และไม่ใช่ในทางกลับกัน ฉันคิดว่าในที่สุดนั่นคือสิ่งที่เราจะทำ
Benjamin Gruenbaum

คำตอบ:


106
  • การผูกกับวัตถุจะทำงานอย่างไร
  • การฟังการเปลี่ยนแปลงในแบบฟอร์มอาจทำงานอย่างไร

นามธรรมที่ปรับปรุงวัตถุทั้งสอง

ฉันคิดว่ามีเทคนิคอื่น ๆ แต่ในที่สุดฉันก็มีวัตถุที่เก็บการอ้างอิงถึงองค์ประกอบ DOM ที่เกี่ยวข้องและมีอินเทอร์เฟซที่ประสานการปรับปรุงข้อมูลของตัวเองและองค์ประกอบที่เกี่ยวข้อง

.addEventListener()มีอินเตอร์เฟซที่ดีมากสำหรับเรื่องนี้ คุณสามารถให้วัตถุที่ใช้eventListenerอินเทอร์เฟซได้และมันจะเรียกใช้ตัวจัดการกับวัตถุนั้นเป็นthisค่า

สิ่งนี้ช่วยให้คุณเข้าถึงองค์ประกอบและข้อมูลที่เกี่ยวข้องโดยอัตโนมัติ

การกำหนดวัตถุของคุณ

การถ่ายทอดทางพันธุกรรมของ Prototypal เป็นวิธีที่ดีในการนำมาใช้แม้ว่าจะไม่จำเป็นต้องใช้แน่นอน ก่อนอื่นคุณต้องสร้างตัวสร้างที่ได้รับองค์ประกอบของคุณและข้อมูลเริ่มต้นบางอย่าง

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)สมมติว่าฉันต้องการเก็บวัตถุที่ซิงค์กับแม่แบบ (ไม่ใช่เฉพาะหนวด) ฉันจะไปเกี่ยวกับเรื่องนั้นได้อย่างไร
Benjamin Gruenbaum

3
@BenjaminGruenbaum: ฉันไม่ได้ใช้แม่แบบฝั่งไคลเอ็นต์ แต่ฉันคิดว่า Mustache มีไวยากรณ์บางอย่างสำหรับการระบุจุดแทรกและไวยากรณ์นั้นมีป้ายกำกับ ดังนั้นฉันจึงคิดว่าส่วน "คงที่" ของเทมเพลตจะแสดงผลเป็นชิ้นส่วนของ HTML ที่จัดเก็บในอาเรย์และส่วนแบบไดนามิกจะอยู่ระหว่างชิ้นส่วนเหล่านั้น จากนั้นจะใช้ป้ายกำกับของจุดแทรกเป็นคุณสมบัติของวัตถุ จากนั้นหากบางคนinputต้องอัปเดตจุดใดจุดหนึ่งก็จะมีการจับคู่จากอินพุตไปยังจุดนั้น ฉันจะดูว่าฉันสามารถมากับตัวอย่างที่รวดเร็ว

1
@BenjaminGruenbaum: อืม ... ฉันไม่ได้คิดเกี่ยวกับวิธีการประสานงานองค์ประกอบที่แตกต่างกันสองอย่างให้สะอาด นี่เป็นสิ่งที่เกี่ยวข้องมากกว่าที่ฉันคิดไว้เล็กน้อย แม้ว่าฉันอยากรู้อยากเห็นดังนั้นฉันอาจต้องทำงานนี้ในภายหลัง :)

2
คุณจะเห็นว่ามีคอนTemplateสตรัคเตอร์หลักที่ทำการแยกวิเคราะห์ถือMyCtorวัตถุที่แตกต่างกันและมีอินเทอร์เฟซสำหรับอัปเดตแต่ละรายการด้วยตัวระบุ แจ้งให้เราทราบหากคุณมีคำถาม :) แก้ไข: ... ใช้ลิงค์นี้แทน ... ฉันลืมไปว่าฉันมีการเพิ่มค่าชี้แจงในทุก ๆ 10 วินาทีเพื่อแสดงการอัพเดท JS สิ่งนี้ จำกัด ไว้

2
... เวอร์ชันที่ได้รับการวิจารณ์อย่างเต็มที่รวมถึงการปรับปรุงเล็กน้อย

36

ดังนั้นฉันจึงตัดสินใจโยนสารละลายของตัวเองลงในหม้อ นี่คือไวโอลินที่ใช้งานได้ โปรดทราบว่านี่จะทำงานบนเบราว์เซอร์ที่ทันสมัยมากเท่านั้น

มันใช้อะไร

การใช้งานนี้ทันสมัยมาก - มันต้องใช้เบราว์เซอร์ที่ทันสมัย

  • MutationObserversเพื่อตรวจจับการเปลี่ยนแปลงใน dom (ใช้ฟังเหตุการณ์ด้วย)
  • Object.observeเพื่อตรวจจับการเปลี่ยนแปลงในวัตถุและแจ้งให้ทราบโดม อันตรายเนื่องจากคำตอบนี้ได้รับการเขียน Oo ได้รับการกล่าวถึงและตัดสินใจโดย ECMAScript TC พิจารณา polyfill

มันทำงานอย่างไร

  • บนองค์ประกอบใส่การ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 และการกลายพันธุ์ของผู้สังเกตการณ์ shimming สามารถใช้ได้


1
ฉันเพิ่งจะเขียนเรื่องนี้ (es5) เพื่อความสนุกสนานถ้าใคร ๆ เห็นว่ามันมีประโยชน์ - ทำให้ตัวคุณเองjsfiddle.net/P9rMm
Benjamin Gruenbaum

1
โปรดทราบว่าเมื่อobj.nameมีตัวตั้งค่ามันจะไม่สามารถสังเกตได้จากภายนอก แต่ต้องถ่ายทอดว่ามันมีการเปลี่ยนแปลงจากภายในตัวตั้งค่า - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - kinda โยนประแจในงาน สำหรับ Oo () ถ้าคุณต้องการความซับซ้อนมากขึ้นพฤติกรรมการพึ่งพาซึ่งกันและกันโดยใช้ setters นอกจากนี้เมื่อobj.nameไม่สามารถกำหนดค่าได้ให้นิยามใหม่ว่า setter (พร้อมด้วยเทคนิคต่าง ๆ เพื่อเพิ่มการแจ้งเตือน) ไม่ได้รับอนุญาต - ดังนั้นข้อมูลทั่วไปที่มี Oo () จะถูกตัดทิ้งโดยสิ้นเชิงในกรณีเฉพาะนั้น
Nolo

8
Object.observe ถูกลบออกจากเบราว์เซอร์ทั้งหมด: caniuse.com/#feat=object-observe
JvdBerg

1
พร็อกซีสามารถใช้แทน Object.observe หรือgithub.com/anywhichway/proxy-observeหรือgist.github.com/ebidel/1b553d571f924da2da06หรือ polyfills รุ่นเก่าเช่นกันบน github @JvdBerg
jimmont

29

ฉันต้องการที่จะเพิ่มคำบุพบทของฉัน ฉันแนะนำวิธีการที่แตกต่างกันเล็กน้อยที่จะช่วยให้คุณสามารถกำหนดค่าใหม่ให้กับวัตถุของคุณโดยไม่ต้องใช้วิธีการ จะต้องมีการตั้งข้อสังเกตแม้ว่าจะไม่ได้รับการสนับสนุนโดยเฉพาะเบราว์เซอร์รุ่นเก่าและ IE9 ยังคงต้องใช้ส่วนต่อประสานที่แตกต่างกัน

สิ่งที่สะดุดตาที่สุดคือแนวทางของฉันไม่ได้ใช้ประโยชน์จากเหตุการณ์

Getters และ Setters

ข้อเสนอของฉันใช้ประโยชน์จากคุณลักษณะที่ค่อนข้างใหม่ของผู้ได้รับและผู้ตั้งค่าโดยเฉพาะผู้ตั้งค่าเท่านั้น โดยทั่วไป mutators ช่วยให้เราสามารถ "ปรับแต่ง" พฤติกรรมของวิธีการที่คุณสมบัติบางอย่างได้รับการกำหนดค่าและดึง

การใช้งานอย่างหนึ่งที่ฉันจะใช้ที่นี่คือวิธีการObject.defineProperty มันทำงานได้ใน FireFox, GoogleChrome และ - ฉันคิดว่า - IE9 ยังไม่ได้ทดสอบเบราว์เซอร์อื่น แต่เนื่องจากนี่เป็นเพียงทฤษฎี ...

อย่างไรก็ตามมันยอมรับพารามิเตอร์สามตัว พารามิเตอร์แรกเป็นวัตถุที่คุณต้องการกำหนดคุณสมบัติใหม่สำหรับสตริงที่สองคล้ายกับชื่อของคุณสมบัติใหม่และสุดท้าย "วัตถุ descriptor" ให้ข้อมูลเกี่ยวกับพฤติกรรมของคุณสมบัติใหม่

สองอธิบายที่น่าสนใจโดยเฉพาะอย่างยิ่งมีและget setตัวอย่างจะมีลักษณะดังนี้ โปรดทราบว่าการใช้สองสิ่งนี้ห้ามมิให้ใช้ descriptor อีก 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วัตถุที่เป็นHarmony เท่านั้น:) ผู้สร้างดูเหมือนจะเป็นความคิดที่ดี แต่นั่นไม่ต้องการให้เราแก้ไขวัตถุจริงเหรอ? นอกจากนี้ในหมายเหตุข้าง - Object.createสามารถใช้ที่นี่ (อีกครั้งสมมติว่าเบราว์เซอร์สมัยใหม่ที่อนุญาตให้ใช้พารามิเตอร์ที่สอง) นอกจากนี้ setter / getter ยังสามารถใช้เพื่อ 'project' ค่าที่แตกต่างจาก object และอิลิเมนต์ DOM :) ฉันสงสัยว่าถ้าคุณมีข้อมูลเชิงลึกใด ๆ บน templating เกินไปที่ดูเหมือนจะเป็นความท้าทายที่แท้จริงที่นี่โดยเฉพาะอย่างยิ่งกับโครงสร้างอย่าง :)
เบนจามิน Gruenbaum

ฉันก็ไม่ได้ทำงานอะไรมากนักกับเครื่องมือสร้างเทมเพลตฝั่งไคลเอ็นต์เช่นเดียวกัน :( แต่สิ่งที่คุณหมายถึงโดยปรับเปลี่ยนวัตถุที่เกิดขึ้นจริงหรือไม่และฉันต้องการที่จะเข้าใจความคิดของวิธีการที่คุณได้เข้าใจว่าตัวตั้งค่า / getter สามารถใช้ในการ ... . the getters / setters ที่นี่จะใช้เพื่ออะไร แต่การเปลี่ยนเส้นทางอินพุตทั้งหมดไปยังและการดึงข้อมูลจากวัตถุไปยังองค์ประกอบ DOM นั้นโดยทั่วไปเป็นProxyเช่นที่คุณพูดไว้;) ฉันเข้าใจความท้าทายที่จะทำให้คุณสมบัติที่แตกต่างกันสองแบบตรงกัน วิธีการของฉันกำจัดหนึ่งในทั้งสอง
Kiruse

A Proxyจะขจัดความจำเป็นในการใช้ getters / setters คุณสามารถผูกองค์ประกอบโดยไม่ทราบว่ามีคุณสมบัติใดบ้าง สิ่งที่ฉันหมายถึงคือผู้ได้รับสามารถเปลี่ยนแปลงได้มากกว่า bindTo.value พวกเขาสามารถมีตรรกะ (และอาจเป็นแม่แบบ) คำถามคือวิธีที่จะรักษาความผูกพันแบบสองทิศทางนี้กับแม่แบบในใจ? ให้บอกว่าฉันทำแผนที่วัตถุของฉันไปยังแบบฟอร์มฉันต้องการรักษาทั้งองค์ประกอบและแบบฟอร์มที่ซิงค์และฉันสงสัยว่าฉันจะทำอย่างไรกับสิ่งนั้น คุณสามารถตรวจสอบวิธีการทำงานใน knockout learn.knockoutjs.com/#/?tutorial=introตัวอย่าง
Benjamin Gruenbaum

@BenjaminGruenbaum Gotcha ฉันจะให้มันดู
Kiruse

@BenjaminGruenbaum ฉันเห็นสิ่งที่คุณพยายามที่จะเข้าใจ การตั้งค่าทั้งหมดนี้โดยคำนึงถึงแม่แบบกลายเป็นเรื่องยากขึ้นเล็กน้อย ฉันจะทำงานกับสคริปต์นี้สักครู่ (และรีบูตมันอย่างต่อเนื่อง) แต่ตอนนี้ฉันหยุดพัก จริงๆแล้วฉันไม่ค่อยมีเวลาสำหรับเรื่องนี้
Kiruse

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 do 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 หรือ
Mohsen Shakiba

สำหรับตอนนี้มันต้องการ shim หรือ polyfill Object.observeเนื่องจากการสนับสนุนมีอยู่เฉพาะในโครเมี่ยมเท่านั้น caniuse.com/#feat=object-observe
madcampos

9
Object.observe ตายแล้ว แค่คิดว่าฉันทราบว่าที่นี่
Benjamin Gruenbaum

@BenjaminGruenbaum เป็นอะไรที่ถูกต้องที่จะใช้ตอนนี้ตั้งแต่นี้ตายไปแล้ว?
johnny

1
@ จอห์นนี่ถ้าฉันไม่ผิดมันจะเป็นกับดักพร็อกซี่เพราะพวกเขาอนุญาตให้มีการควบคุมที่ละเอียดยิ่งขึ้นของสิ่งที่ฉันสามารถทำกับวัตถุ แต่ฉันต้องตรวจสอบว่า
madcampos

7

เมื่อวานนี้ฉันเริ่มเขียนวิธีผูกข้อมูลของตัวเอง

มันตลกมากที่ได้เล่นกับมัน

ฉันคิดว่ามันสวยงามและมีประโยชน์มาก อย่างน้อยในการทดสอบของฉันโดยใช้ Firefox และโครเมี่ยม 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;
}

ตอนนี้เมื่อใดก็ตามที่เราต้องการผูกคุณสมบัติของแบบจำลองกับส่วนหนึ่งของ UI เราก็ต้องตั้งค่าคุณสมบัติข้อมูลที่เหมาะสมในองค์ประกอบ HTML ที่สอดคล้องกัน:

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

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

แม้ว่าลิงก์นี้อาจตอบคำถามได้ดีกว่าหากรวมส่วนสำคัญของคำตอบไว้ที่นี่และให้ลิงก์สำหรับการอ้างอิง คำตอบสำหรับลิงค์เท่านั้นอาจไม่ถูกต้องหากหน้าเว็บที่เชื่อมโยงมีการเปลี่ยนแปลง
Sam Hanley

@sphanley ตั้งข้อสังเกตว่าฉันอาจจะอัปเดตเมื่อฉันมีเวลามากขึ้นเพราะมันเป็นรหัสที่ค่อนข้างยาวสำหรับการโพสต์คำตอบ
Nikos M.

@sphanley โค้ดตัวอย่างที่สร้างซ้ำตามคำตอบจากลิงก์อ้างอิง (แม้ว่าฉันจะใช้ตัวหนานี้จะสร้างเนื้อหาซ้ำกันเกือบตลอดเวลา)
Nikos M.

1
แน่นอนว่ามันจะสร้างเนื้อหาที่ซ้ำกัน แต่นั่นคือจุดเชื่อมโยงบล็อกมักจะมีเวลาและโดยการทำซ้ำเนื้อหาที่เกี่ยวข้องที่นี่จะช่วยให้มั่นใจว่าจะสามารถใช้งานได้และเป็นประโยชน์ต่อผู้อ่านในอนาคต คำตอบดูดีมากตอนนี้!
Sam Hanley

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

ฉันได้ผ่านตัวอย่างจาวาสคริปต์พื้นฐานโดยใช้ตัวจัดการเหตุการณ์ 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

วิธีการผูกตัวแปรอย่างง่ายเข้ากับอินพุต (การผูกสองทาง) คือการเข้าถึงองค์ประกอบอินพุตใน getter และ setter โดยตรง

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

มันง่ายมากที่จะผูกข้อมูลสองทางในวานิลลาจาวาสคริปต์ ....

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

<div id="name">

</div>


2
แน่นอนนี้จะทำงานกับเหตุการณ์ onkeyup เท่านั้น? เช่นถ้าคุณทำคำขออาแจ็กซ์และจากนั้นเปลี่ยน innerHTML ผ่านทางจาวาสคริปต์แล้วนี้จะไม่ทำงาน
ซัคสมิ ธ

1

มางานปาร์ตี้สายโดยเฉพาะอย่างยิ่งเมื่อฉันเขียน libs ที่เกี่ยวข้อง 2 เดือน / ปีที่ผ่านมาฉันจะพูดถึงพวกเขาในภายหลัง แต่ก็ยังมีความเกี่ยวข้องกับฉัน เพื่อทำให้สปอยเลอร์สั้นจริงๆเทคโนโลยีที่ฉันเลือกคือ:

  • Proxy สำหรับการสังเกตของแบบจำลอง
  • MutationObserver สำหรับการติดตามการเปลี่ยนแปลงของ DOM (สำหรับเหตุผลที่มีผลผูกพันไม่ใช่การเปลี่ยนแปลงค่า)
  • การเปลี่ยนแปลงในมูลค่า (มุมมองต่อการไหลของรุ่น) ได้รับการจัดการผ่านทางปกติaddEventListenerไส

IMHO นอกเหนือจาก OP เป็นสิ่งสำคัญที่การดำเนินการผูกข้อมูลจะ:

  • จัดการกรณีวงจรชีวิตของแอพที่แตกต่างกัน (HTML เป็นอันดับแรกจากนั้นเลือก JS, JS ก่อนจากนั้นจึงเปลี่ยนแอตทริบิวต์แบบไดนามิก ฯลฯ )
  • อนุญาตให้รวมรูปแบบลึกเพื่อให้หนึ่งอาจผูก user.address.block
  • อาร์เรย์เป็นแบบจำลองควรได้รับการสนับสนุนอย่างถูกต้อง ( shift, spliceและเหมือนกัน)
  • จัดการ ShadowDOM
  • ความพยายามที่จะง่ายต่อการเปลี่ยนเทคโนโลยีเท่าที่จะทำได้ดังนั้นการสร้างเทมเพลตย่อยเป็นวิธีที่ไม่เป็นมิตรกับการเปลี่ยนแปลงในอนาคต

เมื่อพิจารณาจากทั้งหมดแล้วในความคิดของฉันทำให้มันเป็นไปไม่ได้ที่จะโยนสาย JS หลายสิบบรรทัด ฉันพยายามทำมันเป็นลวดลายมากกว่าlib - ไม่ได้ผลสำหรับฉัน

ถัดไปการObject.observeลบออกและจากการสังเกตแบบจำลองเป็นส่วนที่สำคัญ - ส่วนนี้ทั้งหมดต้องแยกออกจากความกังวลกับ lib อื่น ทีนี้มาถึงจุดที่หลักการของวิธีการที่ฉันนำปัญหานี้ - ตรงตาม OP ถาม:

รุ่น (ส่วน JS)

การสังเกตแบบจำลองของฉันคือพร็อกซีนี่เป็นวิธีเดียวที่ทำให้ฉันใช้งานได้ จุดเด่นอย่างครบถ้วนobserverควรเป็นห้องสมุดของตัวเองดังนั้นฉันจึงพัฒนาobject-observerห้องสมุดเพื่อจุดประสงค์เดียว

โมเดล / s ควรลงทะเบียนผ่าน API เฉพาะบางจุดซึ่งเป็นจุดที่ POJO เปลี่ยนเป็นObservables ไม่เห็นทางลัดใด ๆ ที่นี่ องค์ประกอบ DOM ซึ่งถือว่าเป็นมุมมองที่ถูกผูกไว้ (ดูด้านล่าง) ได้รับการอัปเดตด้วยค่าของโมเดล / s ในตอนแรกและจากนั้นเมื่อการเปลี่ยนแปลงข้อมูลแต่ละครั้ง

จำนวนการดู (ส่วน HTML)

IMHO วิธีที่ชัดเจนที่สุดในการแสดงการเชื่อมโยงคือผ่านแอตทริบิวต์ หลายคนทำสิ่งนี้มาก่อนและหลายคนจะทำตามดังนั้นจึงไม่มีข่าวที่นี่นี่เป็นเพียงวิธีที่เหมาะสมในการทำเช่นนั้น ในกรณีของฉันฉันได้ไปกับไวยากรณ์ต่อไปนี้: <span data-tie="modelKey:path.to.data => targerProperty"></span>แต่สิ่งนี้มีความสำคัญน้อยกว่า อะไรคือสิ่งสำคัญที่ฉันไม่มีไวยากรณ์การเขียนสคริปต์ที่ซับซ้อนใน HTML - นี้ไม่ถูกต้องอีกครั้ง IMHO

องค์ประกอบทั้งหมดที่กำหนดให้เป็นมุมมองที่ถูกผูกไว้จะถูกรวบรวมในตอนแรก ดูเหมือนว่าฉันจะหลีกเลี่ยงไม่ได้จากด้านประสิทธิภาพในการจัดการการแมปภายในระหว่างโมเดลและมุมมองดูเหมือนเป็นกรณีที่หน่วยความจำ + การจัดการบางส่วนควรเสียสละเพื่อบันทึกการค้นหาและการอัพเดตรันไทม์

มุมมองจะอัปเดตในตอนแรกจากโมเดลหากมีและตามการเปลี่ยนแปลงในภายหลังตามที่เราได้กล่าวไว้ ยิ่งไปกว่านั้น DOM ทั้งหมดควรได้รับการปฏิบัติMutationObserverเพื่อตอบสนอง (ผูก / ไม่ผูก) ในองค์ประกอบที่เพิ่ม / ลบ / เปลี่ยนแปลงแบบไดนามิก นอกจากนี้ทั้งหมดนี้ควรทำซ้ำใน ShadowDOM (เปิดหนึ่งหลักสูตร) ​​เพื่อไม่ให้เกิดหลุมดำที่ไม่ถูกผูกไว้

รายการของข้อมูลเฉพาะอาจไปได้อีกแน่นอน แต่สิ่งเหล่านั้นอยู่ในความคิดของฉันซึ่งเป็นหลักการสำคัญที่จะทำให้การเชื่อมโยงข้อมูลถูกนำไปใช้พร้อมกับความสมดุลของคุณสมบัติที่สมบูรณ์จากหนึ่งและความเรียบง่ายที่มีสติจากอีกด้านหนึ่ง

และนอกเหนือจากที่object-observerกล่าวมาข้างต้นฉันได้เขียนdata-tierห้องสมุดด้วยเช่นกันซึ่งใช้ข้อมูลที่เชื่อมโยงกับแนวคิดที่กล่าวถึงข้างต้น


0

สิ่งต่าง ๆ มีการเปลี่ยนแปลงอย่างมากในช่วง 7 ปีที่ผ่านมาเรามีองค์ประกอบเว็บดั้งเดิมในเบราว์เซอร์ส่วนใหญ่ในขณะนี้ IMO หลักของปัญหาคือการแชร์สถานะระหว่างองค์ประกอบเมื่อคุณรู้ว่ามันสำคัญที่จะอัปเดต UI เมื่อสถานะเปลี่ยนแปลงและในทางกลับกัน

ในการแชร์ข้อมูลระหว่างองค์ประกอบต่างๆคุณสามารถสร้างคลาส 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 (คัดค้าน)
  • ไม่มีพร็อกซี (ซึ่งมีตะขอ แต่ไม่มีกลไกการสื่อสารอยู่แล้ว)
  • ไม่มีการพึ่งพา (นอกเหนือจาก polyfill ขึ้นอยู่กับเบราว์เซอร์เป้าหมายของคุณ)
  • มันรวมศูนย์และโมดูลาร์ที่สมเหตุสมผล ... อธิบายสถานะเป็น html และการมีผู้ฟังทุกที่จะทำให้ยุ่งเหยิงเร็วมาก
  • มันยืดออกได้ การใช้งานขั้นพื้นฐานนี้คือโค้ด 20 บรรทัด แต่คุณสามารถสร้างความสะดวกสบายความผันแปรไม่ได้และเวทย์มนตร์รูปทรงเพื่อให้ง่ายต่อการทำงาน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.