2014-07-25 23 views
8

Sto utilizzando il fantastico knockout.js per associare le proprietà ViewModel al DOM. Ora, parte della mia GUI è resa su un elemento canvas. Io uso fabric.js per disegnare gli elementi sulla tela. Dato che questi elementi non fanno parte del dom (sono involucri attorno ai metodi di disegno su tela), non posso usare il knockout per legarli. Tuttavia, ho bisogno di tenere traccia della loro posizione/colore/etichetta nel ViewModel.Collegamento a un elemento ui non DOM

Penso che potrei creare un binding personalizzato per ciascuno dei tipi di tessuto primitivo e quindi legarli come un nodo dom. Tuttavia, un'associazione personalizzata prevede un elemento DOM come primo parametro. In secondo luogo, non posso (facilmente) aggiungere un'associazione a livello di codice. Devo essere in grado di farlo poiché non riesco a scrivere i binding in HTML.

Sto ancora pensando a questo, ma sono momentaneamente bloccato per il momento. Qualche idea?

+0

Ti è dispiaciuto condividere un violino o un'altra demo con la sorgente che ha un collegamento dati bidirezionale? Riesco a vedere l'unidirezionale del violino nella risposta qui sotto. Inoltre, hai affrontato altri problemi con fabric & knockout? –

+1

Non abbiamo continuato con il tessuto. Abbiamo iniziato a utilizzare jointjs.com, che è basato su svg (anziché su tela), che ci consente di incorporare html all'interno dei grafici e collegarli a quelli. Non sto raccomandando questa libreria esatta di per sé, ma qualcosa di simile sarebbe una scelta sbagliata, immagino. È un po 'difficile creare rapidamente un violino dopo tutto questo tempo. – bertvh

+0

grazie per aver condiviso. Sto creando un PoC con JointJS e knockoutjs. Puoi condividere qualsiasi violino per questo? Sto cercando di incorporare html all'interno degli elementi di jointjs. –

risposta

4

Le associazioni personalizzate sono implementate all'interno di osservabili calcolati, in modo che le dipendenze siano tracciate e la funzionalità update possa essere nuovamente attivata.

Per la tua funzionalità, potresti prendere in considerazione l'utilizzo di osservabili calcolati che tracciano gli oggetti, accedono alle dipendenze e fanno tutti gli aggiornamenti/le chiamate api necessarie.

Fino ad ora non ho usato tessuto, ma qui ci sarebbe una ripresa in cui si definisce un modello di vista per rappresentare un rettangolo e si crea un calcolo per aggiornare continuamente l'oggetto tessuto quando cambiano i valori.

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

Un'altra idea breve, se si preferisce mantenere un po 'del tessuto/tela chiamate dal vostro modello di vista (probabilmente lo farei). È possibile creare un binding fabric che includa una matrice di forme da aggiungere al canvas. Dovresti anche passare un gestore che recupera i parametri da passare ad esso. L'associazione quindi creerebbe la forma, la aggiungerà alla tela e quindi creerà un calcolo per aggiornare la forma sulle modifiche. Qualcosa di simile:

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

     }); 
    } 
}; 

Si potrebbe metterlo su una tela come:

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

A quel punto, il modello di vista potrebbe essere semplificato un po 'per rappresentare solo i dati del rettangolo. Esempio qui: http://jsfiddle.net/rniemeyer/G6MGm/

+0

L'associazione personalizzata sembra un buon approccio. Avrei bisogno di rendere l'associazione bidirezionale (è necessario tenere traccia delle forme quando vengono trascinate), ma ciò sembra abbastanza facile con gli eventi fabric. Ho anche notato nel jsFiddle che, dopo aver cambiato la posizione nelle caselle di testo, i "clic target" delle forme disegnate sembrano non essere corretti, il che significa che quando faccio clic/trascina sulle forme, non succede nulla. Quando clicco vicino alla forma, posso trascinarlo. Forse il tessuto ha bisogno di una chiamata di aggiornamento/ridisegno o qualcosa del genere. – bertvh

+0

@bertvh - sì- In realtà non mi ero nemmeno reso conto che potevi fare clic e trascinare/ridimensionare le forme. Non avevo usato tessuto prima di quel violino. Dovresti assolutamente iscriverti agli eventi fabric in 'init' e aggiornare gli osservabili in base ai dati forniti dall'evento. –

+0

@Niemeyer Pur non essendo una soluzione completa, ho accettato la tua risposta dal momento che mi ha davvero preso il giusto trak. Grazie uomo! – bertvh

0

Ho un problema simile nel mio progetto hobby, http://simonlikesmaps.appspot.com ... ho bisogno di impegnare una serie di waypoint su una mappa e non posso farlo direttamente tramite KO perché i waypoint sono nascosti all'interno strati OpenLayers SVG . Quindi uso una funzione di sottoscrizione all'interno di questo modello:

In primo luogo, un modello di visualizzazione per le cose che si stanno visualizzando, per me i waypoint, per voi elementi; grosso modo -

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

In secondo luogo, un modello al fine di tenere un elenco di queste cose:

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

Poi una certa logica per caricare gli elementi dal vostro webservice nel modello vista, più o meno:

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

Sembra che questo bit verrà ordinato; l'obiettivo è finire con un elenco di elementi contenuti in un array osservabile. Puoi quindi iscriverti ad esso e farti lavorare su Fabric; c'è un sacco di pseudo-codice in questo prossimo 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); 
} 

Questo non è super-sofisticata, ma vuol dire che ogni volta che le modifiche di dati e la vostra lista di elementi cambiano, tali modifiche sono riflessi immediatamente a tua tela per KO chiamando la funzione Sottoscrivi. La "sincronizzazione" tra la nuova lista di elementi e ciò che è già presente sulla tela è abbastanza cruda: basta distruggere tutto e ri-renderizzare l'intera lista. Questo è OK per me con i miei waypoint, ma potrebbe essere troppo costoso per te.

+0

Sì ... funzionerebbe davvero per casi semplici. Tuttavia, non vi è alcun vincolo reale in corso. Gli oggetti sulla mia tela sono trascinabili dall'utente e ho bisogno che la posizione sia sincronizzata con i miei modellini di vista. Non posso farlo con questa soluzione. – bertvh

0

Ero interessato a trovare questa domanda poiché ho anche cercato di integrare gli oggetti fabric.js con il modello a eliminazione diretta - suppongo di essere rassicurato dal fatto che non esiste una sola risposta ovvia. Ecco un esempio che mostra il mio ultimo pensiero: https://jsfiddle.net/whippet71/aky9af6t/ Ha un collegamento dati bidirezionale tra oggetti DOM (caselle di testo) e l'oggetto canvas attualmente selezionato.

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

Un paio di cose da notare:

  • creo un ko.observable e fare un abbonamento manuale per lo scopo di aggiornamento degli oggetti fabric
  • Ho impostato un evento fabric h andler che aggiorna il ko.observable sulla selezione/modifica dell'oggetto. Non avevo bisogno di legare a qualcosa di diverso dall'oggetto attualmente selezionato.
  • ho dovuto usare gli aggiornamenti anticipate per evitare aggiornamenti ciclici (aggiornamenti tessuto ko, che poi informa il tessuto ...)

Sono molto nuovo a eliminazione diretta in modo qualsiasi commento/suggerimento sarebbe il benvenuto.

Problemi correlati