Knockout.js 绑定到非domui元素

Knockout.js 绑定到非domui元素,knockout.js,fabricjs,Knockout.js,Fabricjs,我正在使用奇妙的knockout.js将ViewModel属性绑定到DOM。现在,部分GUI呈现在画布元素上。我使用fabric.js在画布上绘制元素。因为这些元素不是dom的一部分(它们是画布绘制方法的包装器),所以我不能使用knockout绑定到它们。但是,我需要跟踪它们在ViewModel中的位置/颜色/标签 我认为我可以为每种结构基本类型创建自定义绑定,然后像绑定dom节点一样绑定它们。但是,自定义绑定需要一个DOM元素作为其第一个参数。其次,我不能(轻松地)以编程方式添加绑定。我需要能

我正在使用奇妙的knockout.js将ViewModel属性绑定到DOM。现在,部分GUI呈现在画布元素上。我使用fabric.js在画布上绘制元素。因为这些元素不是dom的一部分(它们是画布绘制方法的包装器),所以我不能使用knockout绑定到它们。但是,我需要跟踪它们在ViewModel中的位置/颜色/标签

我认为我可以为每种结构基本类型创建自定义绑定,然后像绑定dom节点一样绑定它们。但是,自定义绑定需要一个DOM元素作为其第一个参数。其次,我不能(轻松地)以编程方式添加绑定。我需要能够做到这一点,因为我不能用HTML编写绑定


我还在想这个,但我现在有点困了。有什么想法吗?

自定义绑定是在计算的可观察对象内部实现的,这样可以跟踪依赖关系,并且可以再次触发
更新功能

<> P>这听起来像你的功能,你可能想考虑使用计算的跟踪,跟踪你的对象,访问依赖关系,并作出任何必要的更新/ API调用。 直到现在,我还没有使用fabric,但这里有一个例子,您可以定义一个视图模型来表示一个矩形,并创建一个计算模型,以便在任何值更改时不断更新fabric对象

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


在这一点上,视图模型可以简化很多,只表示矩形的数据。这里的示例:

我的爱好项目中有一个类似的问题。。。我需要将一组航路点绑定到地图上,但我无法通过KO直接绑定,因为航路点隐藏在OpenLayers SVG层中。所以我在这个模式中使用了一个subscribe函数:

首先,一个视图模型,用于显示您正在显示的内容,对于我来说是航路点,对于您来说是元素;粗略地-

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))
   }
}
听起来你会把这一点分类;其目的是最终得到一个包含在observableArray中的元素列表。然后你就可以订阅它,做你的织物工作;接下来的代码中有很多伪代码

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);
}
ElementListViewModel=函数(){
this.elements=ko.observableArray()
this.elements.subscribe(函数(新元素){
//把所有的东西从织物上移开

Fabric.removeEverything()//我很有兴趣遇到这个问题,因为我也一直在尝试将Fabric.js对象与淘汰模型集成在一起-我想我确信没有一个明显的答案。下面是显示我最新想法的示例: 它在DOM对象(文本框)和当前选定的画布对象之间具有双向数据绑定

HTML:


X:
Y:
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...
});
var jsonFromServer=[{“类型”:“矩形”,“左”:10,“顶部”:100,“宽度”:50,“高度”:50},{“类型”:“矩形”,“左”:85,“顶部”:100,“宽度”:50,“高度”:50},{“类型”:“圆形”,“左”:25,“顶部”:250,“半径”:50};
var选择对象;
var canvas=newfabric.canvas('mycanvas');

对于(var i=0;iYeah..这确实适用于简单的情况。但是,没有真正的绑定。画布上的项目可由用户拖动,我需要将位置同步到我的viewmodels。我无法使用此解决方案。自定义绑定似乎是一种很好的方法。我需要双向绑定(拖动形状时需要跟踪形状)但是对于fabric事件来说,这似乎很容易。我还注意到,在您的JSFIDLE中,在我更改文本框中的位置后,绘制的形状的“单击目标”似乎不正确,这意味着当我单击/拖动形状时,什么也没有发生。当我单击形状旁边时,我可以拖动它。可能fabric需要一个刷新/重画调用方法。@bertvh-是的-我甚至没有意识到你可以点击并拖动/调整形状大小。在那之前我没有使用fabric。你肯定必须在
init
中订阅fabric事件,并根据事件提供的数据更新观察值。@Niemeyer虽然不是一个完整的解决方案,但我接受了你的答案因为它真的让我走上了正确的道路。谢谢你!你介意与具有双向数据绑定的源共享一个fiddle或其他演示吗?我可以在下面的答案中看到fiddle中的单向绑定。另外,你在fabric和knockout方面是否遇到了任何其他问题?我们没有继续使用fabric。我们开始使用基于svg的jointjs.com(代替画布),允许我们将html嵌入到图形中并绑定到这些图形。我并不推荐这个确切的库本身,但我想类似的东西将是一个愚蠢的选择。经过这么长时间后,要快速创建一个提琴有点困难。感谢分享。我正在创建一个带有JointJS和knockoutjs的PoC。你能为此共享任何提琴吗?我正在寻找在jointjs元素中嵌入html。
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...
});