Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bind to a non-DOM ui element

I'm using the fantastic knockout.js to bind ViewModel properties to the DOM. Now, part of my GUI is rendered on a canvas element. I use fabric.js to draw the elements on the canvas. Since these elements are not part of the dom (they are wrappers around canvas drawing methods), I cannot use knockout to bind to them. Nevertheless, I need to keep track of their position/color/label in the ViewModel.

I would think I could create a custom binding for each of the fabric primitive types and then bind them just like a dom node. However, a custom binding expects a DOM element as its first parameter. Secondly, I cannot (easily) add a binding programmatically. I need to be able to do this since I can't write the bindings in HTML.

I'm still thinking about this but I'm kinda stuck for the moment. Any ideas?

like image 590
bertvh Avatar asked Jul 25 '14 16:07

bertvh


1 Answers

Custom bindings are implemented inside of computed observables, so that dependencies are tracked and the update functionality can be triggered again.

It sounds like for your functionality, you might want to consider using computed observables that track your objects, access dependencies, and make any necessary updates/api calls.

I haven't used fabric before until now, but here would be one take where you define a view model to represent a rectangle and create a computed to continually update the fabric object when any values change.

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

Another brief idea, if you would rather keep some of the fabric/canvas calls out of your view model (I probably would). You could create a fabric binding that takes in an array of shapes to add to the canvas. You would also pass a handler that retrieves the params to pass to it. The binding would then create the shape, add it to the canvas, and then create a computed to update the shape on changes. Something like:

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

        });
    }
};

You could place it on a canvas like:

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

At that point, the view model could be simplified quite a bit to just represent the rectangle's data. Sample here: http://jsfiddle.net/rniemeyer/G6MGm/

like image 163
RP Niemeyer Avatar answered Oct 01 '22 23:10

RP Niemeyer