【问题标题】:Bind to a non-DOM ui element绑定到非 DOM ui 元素
【发布时间】:2014-07-25 16:10:05
【问题描述】:

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

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

我还在考虑这个,但我现在有点卡住了。有什么想法吗?

【问题讨论】:

  • 您介意与具有双向数据绑定的源共享小提琴或其他演示吗?我可以在下面的答案中看到小提琴中的单向绑定。另外,您在面料和脱色方面遇到过任何其他问题吗?
  • 我们没有继续使用织物。我们开始使用基于 svg(而不是画布)的 jointjs.com,允许我们将 html 嵌入到图形中并绑定到这些图形。我不推荐这个确切的库本身,但我猜类似的东西会是一个愚蠢的选择。这么久了,要快速制作小提琴有点困难。
  • 感谢分享。我正在使用 JointJS 和 knockoutjs 创建一个 PoC。你可以为此分享任何小提琴吗?我正在研究在jointjs元素中嵌入html。

标签: knockout.js fabricjs


【解决方案1】:

自定义绑定在计算后的 observables 中实现,以便跟踪依赖关系并且可以再次触发 update 功能。

听起来,对于您的功能,您可能需要考虑使用计算的 observables 来跟踪您的对象、访问依赖项并进行任何必要的更新/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>

此时,视图模型可以被简化很多,只代表矩形的数据。示例:http://jsfiddle.net/rniemeyer/G6MGm/

【讨论】:

  • 自定义绑定似乎是个不错的方法。我需要进行双向绑定(需要在拖动形状时跟踪形状),但这对于织物事件来说似乎很容易。我还在您的 jsFiddle 中注意到,在我更改文本框中的位置后,绘制形状的“点击目标”似乎不正确,这意味着当我点击/拖动形状时,什么也没有发生。当我单击形状旁边时,我可以拖动它。也许织物需要刷新/重绘调用或其他东西。
  • @bertvh - 是的 - 我什至没有意识到您可以单击并拖动/调整形状大小。在那个小提琴之前我没有用过织物。您肯定必须订阅 init 中的结构事件,并根据事件提供的数据更新可观察对象。
  • @Niemeyer 虽然不是一个完整的解决方案,但我接受了您的回答,因为它确实让我走上了正确的道路。谢谢大佬!
【解决方案2】:

我在我的爱好项目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))
   }
}

听起来您将对此进行排序;目的是最终得到一个包含在 observableArray 中的元素列表。然后您可以订阅它并使用 Fabric 工作;下一段有很多伪代码!

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 调用 subscribe 函数立即反映在您的画布中。新的元素列表和画布上已有的元素之间的“同步”非常粗糙:简单地销毁所有元素并重新渲染整个列表。这对我的航点来说没问题,但对你来说可能太贵了。

【讨论】:

  • 是的.. 这确实适用于简单的情况。但是,并没有真正的约束。我的画布上的项目可由用户拖动,我需要将位置同步到我的视图模型。我不能用这个解决方案做到这一点。
【解决方案3】:

我有兴趣遇到这个问题,因为我也一直在尝试将 fabric.js 对象与淘汰模型集成 - 我想我确信没有一个明显的答案。这是显示我最新想法的示例: 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 并手动订阅它以更新结构对象
  • 我设置了一个结构事件处理程序,用于在对象选择/修改时更新 ko.observable。除了当前选定的对象之外,我不需要绑定任何东西。
  • 我不得不使用延迟更新来避免循环更新(fabric updates ko,然后通知fabric...)

我对淘汰赛很陌生,所以欢迎任何 cmets/建议。

【讨论】:

    猜你喜欢
    • 2018-08-30
    • 1970-01-01
    • 2019-06-16
    • 2012-12-12
    • 1970-01-01
    • 2013-01-23
    • 2014-05-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多