2014-07-25 94 views
8

我使用的奇妙knockout.js結合視圖模型屬性到DOM。現在,我的一部分GUI被渲染在一個畫布元素上。我使用fabric.js在畫布上繪製元素。由於這些元素不是dom的一部分(它們是圍繞canvas繪製方法的包裝),所以我不能用knockout來綁定它們。不過,我需要跟蹤ViewModel中的位置/顏色/標籤。綁定到非DOM UI元素

我想我可以創建自定義的每個結構基本類型的綁定,然後將它們綁定就像一個DOM節點。但是,自定義綁定期望將DOM元素作爲其第一個參數。其次,我不能(很容易)以編程方式添加綁定。我需要能夠做到這一點,因爲我不能在HTML中編寫綁定。

我還在想這一點,但我有點卡住的一瞬間。有任何想法嗎?

+0

你介意分享一個提供雙向數據綁定的源代碼的小提琴或其他演示嗎?我可以在下面的答案中看到小提琴中的一種方式。另外,您是否遇到過面料和淘汰賽的其他問題? –

+1

我們沒有繼續使用面料。我們開始使用基於svg(而不是canvas)的jointjs.com,這允許我們將html嵌入到圖表中並綁定到這些圖表。我不推薦這個確切的庫本身,但我猜想類似的東西會是一個愚蠢的選擇。這段時間以後快速創建小提琴有點難。 – bertvh

+0

感謝您的分享。我正在用JointJS和knockoutjs創建一個PoC。你能分享一下嗎?我正在研究在jointjs元素中嵌入html。 –

回答

4

自定義綁定計算可觀的內部實現,讓依賴性跟蹤和update功能可以再次被觸發。

這聽起來像你的功能,你可能要考慮使用計算觀測跟蹤你的對象,訪問的依賴,並進行必要的更新/ API調用。

我以前沒有使用過的織物到現在爲止,但這裏是一個拿你定義一個視圖模型來表示一個矩形,並創建一個計算時,任何值的變化不斷更新面料對象。

// create a wrapper around native canvas element (with id="c") 
var canvas = new fabric.Canvas('c'); 

var RectangleViewModel = function (canvas) { 
    this.left = ko.observable(100); 
    this.top = ko.observable(100); 
    this.fill = ko.observable("red"); 
    this.width = ko.observable(100); 
    this.height = ko.observable(100); 

    this.rect = new fabric.Rect(this.getParams()); 
    canvas.add(this.rect); 

    this.rectTracker = ko.computed(function() { 
    this.rect.set(this.getParams()); 

    canvas.renderAll();  
    }, this); 

}; 

RectangleViewModel.prototype.getParams = function() { 
    return { 
     left: +this.left(), 
     top: +this.top(), 
     fill: this.fill(), 
     width: +this.width(), 
     height: +this.height() 
    }; 
}; 

var vm = new RectangleViewModel(canvas); 

ko.applyBindings(vm); 

另一個簡單的想法,如果你寧願保留你的視圖模型(我可能會)的一些結構/畫布調用。您可以創建一個fabric綁定,該綁定需要將一組形狀添加到畫布中。你也可以傳遞一個處理器來檢索傳遞給它的參數。綁定然後會創建該形狀,將其添加到畫布,然後創建一個計算更新形狀的變化。喜歡的東西:

ko.bindingHandlers.fabric = { 
    init: function (element, valueAccessor) { 
     var shapes = valueAccessor(), 
      canvas = new fabric.Canvas(element); 

     ko.utils.arrayForEach(shapes, function (shape) { 
      //create the new shape and initialize it 
      var newShape = new fabric[shape.type](shape.params()); 
      canvas.add(newShape); 

      //track changes to the shape (dependencies accessed in the params() function 
      ko.computed(function() { 
       newShape.set(this.params()); 
       canvas.renderAll(); 
      }, shape, { 
       disposeWhenNodeIsRemoved: element 
      }); 

     }); 
    } 
}; 

你可以把它像一個畫布上:

<canvas data-bind="fabric: [ { type: 'Rect', params: rect.getParams }, { type: 'Rect', params: rect2.getParams } ]"></canvas> 

此時,視圖模型可以簡化了不少,只是表示矩形的數據。這裏的示例:http://jsfiddle.net/rniemeyer/G6MGm/

+0

自定義綁定似乎是一種很好的方法。我需要進行雙向綁定(需要在拖動時跟蹤形狀),但對於布料事件來說這似乎很容易。我還注意到,在你的jsFiddle中,當我改變文本框中的位置後,繪製形狀的「點擊目標」似乎不正確,這意味着當我在形狀上單擊/拖動時,什麼都不會發生。當我點擊形狀旁邊時,我可以拖動它。也許結構需要刷新/重繪呼叫或其他東西。 – bertvh

+0

@bertvh - 是的 - 我甚至沒有意識到你可以點擊並拖動/調整形狀。在那小提琴之前我沒有用過布料。您肯定必須在'init'中訂閱結構事件,並根據事件提供的數據更新觀察值。 –

+0

@Niemeyer雖然不是一個完整的解決方案,但我接受了你的答案,因爲它確實讓我走上了正確的Trak。謝啦! – bertvh

0

我在我的愛好項目http://simonlikesmaps.appspot.com中有類似的問題...我需要將一個路點數組綁定到地圖,我不能直接通過KO來完成,因爲路標隱藏在OpenLayers內部SVG圖層。所以我使用這種模式下的訂閱功能:

首先,你正在顯示的東西,我的航點,你的元素的視圖模型;大致 -

ElementViewModel = function(data) { 
    this.position = ko.observable(data.position) 
    this.label = ko.observable(data.label) 
    this.color = ko.observable(data.color) 
} 

其次,視圖模型來保存這些東西的清單:

ElementListViewModel = function() { 
    this.elements = ko.observableArray() 
} 

然後一些邏輯從您的Web服務的元素裝入視圖模型,大致爲:

var element_list = new ELementListViewModel() 
ajax_success_function(ajax_result) { 
    for (element in ajax_result) { 
     element_list.elements.push(new ElementViewModel(element)) 
    } 
} 

這聽起來像你會有這種排序;目的是最後列出一系列可觀察陣列中的元素。然後,您可以訂閱它併爲您製作布料作品;在下一個位中有很多僞代碼!

ElementListViewModel = function() { 
    this.elements = ko.observableArray() 
    this.elements.subscribe(function(new_elements) { 
     // Remove everything from Fabric 
     Fabric.removeEverything() // <!-- pseudo code alert! 
     for (element in new_elements) { 
      Fabric.addThing(convertToFabric(element)) 
     } 
    }, this); 
} 

這不是超級複雜的,但並不意味着只要您的數據發生變化,你的元素列表的變化,這些變化立即被KO體現在畫布調用訂閱功能。新元素列表和畫布上已有內容之間的「同步」是非常簡單的:簡單地摧毀所有內容並重新渲染整個列表。我的航點對我來說確實很好,但對您而言可能太貴。

+0

是的..這確實適用於簡單的情況。但是,這並沒有真正的約束力。我的畫布上的項目可以由用戶拖動,我需要將位置同步到我的視圖模型。我不能用這個解決方案做到這一點。 – bertvh

0

我有興趣想知道這個問題,因爲我也一直試圖將fabric.js對象與knockout模型集成 - 我想我很放心,沒有一個明顯的答案。下面是顯示我最新思想的示例: https://jsfiddle.net/whippet71/aky9af6t/ 它在DOM對象(文本框)和當前選定的畫布對象之間具有雙向數據綁定。

HTML:

<div> 
    <canvas id="mycanvas" width="600" height="400"></canvas> 
</div> 

<div class="form form-inline"> 
    <div class="form-group">X: <input data-bind="textInput: x" class="form-control"></div> 
    <div class="form-group">Y: <input data-bind="textInput: y" class="form-control"></div> 
</div> 

的Javascript:

var jsonFromServer = [{"type":"rectangle","left":10,"top":100,"width":50,"height":50},{"type":"rectangle","left":85,"top":100,"width":50,"height":50},{"type":"circle","left":25,"top":250,"radius":50}]; 
var selectedObject; 

var canvas = new fabric.Canvas('mycanvas'); 

for (var i=0; i<jsonFromServer.length; i++) { 
    var thisShape = jsonFromServer[i]; 
    if (thisShape.type == 'rectangle') { 
     var rect = new fabric.Rect({ 
      width: thisShape.width, 
      height: thisShape.height, 
      left: thisShape.left, 
      top: thisShape.top, 
      fill: 'blue' 
     }); 
     canvas.add(rect); 
    } else if (thisShape.type == 'circle') { 
     var circle = new fabric.Circle({ 
      radius: thisShape.radius, 
      left: thisShape.left, 
      top: thisShape.top, 
      fill: 'green' 
     }); 
     canvas.add(circle); 
    } 
} 
// Set first object as selected by default 
selectedObject = canvas.getObjects()[0]; 
canvas.setActiveObject(selectedObject); 


// A view model to represent the currently selected canvas object 
function ShapeViewModel(initX, initY) { 
    var self = this; 

    self.x = ko.observable(initX); 
    self.y = ko.observable(initY); 
    // Create a computed observable which we subsribe to, to notice change in x or y position 
    // Use deferred updates to avoid cyclic notifications 
    self.position = ko.computed(function() { 
     return { x: self.x(), y: self.y() }; 
    }).extend({ deferred: true }); 
} 

var vm = new ShapeViewModel(selectedObject.left, selectedObject.top); 
ko.applyBindings(vm); 

// Function to update the knockout observable 
function updateObservable(x, y) { 
    vm.x(x); 
    vm.y(y); 
} 

// Fabric event handler to detect when user moves an object on the canvas 
var myHandler = function (evt) { 
    selectedObject = evt.target; 
    updateObservable(selectedObject.get('left'), selectedObject.get('top')); 
} 
// Bind the event handler to the canvas 
// This does mean it will be triggered by ANY object on the canvas 
canvas.on({ 'object:selected': myHandler, 'object:modified': myHandler }); 

// Make a manual subscription to the computed observable so that we can 
// update the canvas if the user types in new co-ordinates 
vm.position.subscribe(function (newPos) { 
    console.log("new x=" + newPos.x + " new y=" + newPos.y); 
    selectedObject.setLeft(+newPos.x); 
    selectedObject.setTop(+newPos.y); 
    selectedObject.setCoords(); 
    canvas.renderAll(); 
    // Update server... 
}); 

有幾件事情需要注意:

  • 我創建了一個ko.observable並進行手動訂閱它爲目的更新織物對象
  • 我設置了織物事件h更新關於對象選擇/修改的ko.observable。我沒有任何需要綁定到除當前選定對象之外的其他任何東西。
  • 我不得不使用延遲更新,以避免循環更新(布更新KO,然後通知布...)

我很新的淘汰賽,因此任何的意見/建議將受到歡迎。