Bind to a non-DOM ui element

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

I would think that I could create a custom binding for each of the primitives of type fabric, and then bind them the same way as a dom node. However, user binding expects the DOM element to become the first parameter. Secondly, I cannot (easily) add a binding programmatically. I need to be able to do this since I cannot write bindings in HTML.

I still think about it, but now I'm a bit stuck. Any ideas?

+8
fabricjs
source share
3 answers

Custom bindings are implemented inside the calculated observable data, so dependencies are tracked, and the update function can be run again.

It looks like for your functionality you might need to use calculated observable data that track your objects, depend on dependencies and make any necessary updates / api calls.

I still haven’t used fabric so far, but here you would take where you define the presentation model to represent the rectangle and create a computed one to constantly 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 save some of the tech / canvas from your view model (I probably will). You can create a fabric binding that takes an array of shapes to add to the canvas. You will also pass a handler that retrieves the parameters to go to it. After that, the binding will create the form, add it to the canvas, and then create the form calculated to update the form upon 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 can place it on a canvas like:

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

At this point, the presentation model can be simplified quite a bit to just represent the data of the rectangle. Example here: http://jsfiddle.net/rniemeyer/G6MGm/

+4
source share

I have a similar problem in my hobby project, http://simonlikesmaps.appspot.com ... I need to map an array of waypoints to the map and I cannot do this directly through KO, because waypoints are hidden inside SVL OpenLayers layers. Therefore, I use the subscription function in this template:

Firstly, a presentation model for the things you show, for my waypoints, for you elements; roughly -

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

Secondly, a view model for storing a list of these things:

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

Then some logic to load your elements from your web service into a view model, something like:

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

It looks like you will be sorting this bit; the goal is to end up with a list of elements in the observable array. Then you can subscribe to it and work with you. there is a lot of pseudo-code in the next bit!

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

This is not a very complicated process, but it means that whenever your data changes and your list of items changes, these changes are immediately reflected in your canvas from KO calling the subscription function. The “synchronization” between the new list of elements and what is already on the canvas is rather crude: just destroy everything and redeploy the entire list. This is normal for me with my waypoints, but may be too expensive for you.

0
source share

I was interested to meet this question as I am also trying to integrate fabric.js objects with a knockout model. I guess I'm sure there is not a single obvious answer. Here is an example showing my last thinking: https://jsfiddle.net/whippet71/aky9af6t/ It has two-way data binding between DOM objects (text fields) and the currently selected canvas object.

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

A few notes:

  • I create ko.observable and do a manual subscription to it in order to update fabric objects.
  • I configured an event handler that updates ko.observable when selecting / modifying an object. I did not need to be attached to anything other than the currently selected object.
  • I had to use delayed updates to avoid circular updates (updates for mother ko, which then notify the fabric ...)

I am very new to knockout, so any comments / suggestions would be welcome.

0
source share

All Articles