Let me know if you can help me out somehow, i'm kind of struggling to get my head around.
Starting with some Marionette application logics:
app.js
//basic setup
this.Graph = new joint.dia.Graph;
this.Paper = new joint.dia.Paper({ width: 640, height: 480, model: this.Graph });
// [...] lots of code
//adding elements
app.Elements.add(element);
So far so good. Now the tricky part. I want a collection.
JointCollectionView.js
module.exports = Marionette.CollectionView.extend({
tagName: 'div',
className: 'row',
childView: JointView,
addChild: function(child, ChildView, index){
//does that make sense?
app.Graph.addCell(child);
//should i add it to collection?
if (child.shouldBeShown()) {
return Marionette.CollectionView.prototype.addChild.call(this, child, ChildView, index);
}
},
getChildView: function(item) {
return app.Graph.getCell(item);
}
//[...]
})
Now even more tricky. How do i handle the joint-view to make it work with collections and also display html elements?
JointView.js
module.exports = joint.dia.ElementView.extend({ /* ?!?!?! */ });
//OR ?
module.exports = Marionette.ItemView.extend({
jointElementView: null, //this will be like above somewhere else...
initialize: function(options){
jointElementView = new JointElementView({ /* ... */ });
}
})
I'm no JointJS expert, but your implementation looks perfect.
You want to use the second option:
JointView.js
module.exports = Marionette.ItemView.extend({
template: _.template("");
jointElementView: null, //this will be like above somewhere else...
initialize: function(options){
this.jointElementView = new JointElementView({ /* ... */ });
}
});
since a Marionette.CollectionView expects a Marionette view (sp. a Marionette.ItemView or descendent [LayoutView/CompositeView]).
What I would add to JointView.js is a method to inject the result from this.jointElementView into the JointView.js html. So, add a property to it, like:
onRender: function () {
this.$el.append(this.jointElementView.el); // Where this.jointElementView.el is the JointJS view html
}
With the help of #seebiscuit i looked much deeper into jointjs and narrowed down how i should approach this problem (You didn't seem to be interested in the points though, so i'll answer myself)
The following files were edited:
app.js (changed, important!)
//this step is necessary to create these element before the paper is created.
//Make jointjs GLOBAL!!!!!!
joint.shapes.html = {};
joint.shapes.html.Element = require('views/Element'); //this dude im gonna use to create the elements
joint.shapes.html.ElementView = require('views/ElementView'); //this badboy will fire when i create those elements. MAGIC!
//basic setup
this.Graph = new joint.dia.Graph;
this.Paper = new joint.dia.Paper({ width: 640, height: 480, model: this.Graph });
// [...] lots of code
//adding elements
app.Elements.add(element);
JointCollectionView.js (beauti-/simplyfied)
module.exports = Marionette.CollectionView.extend({
tagName: 'div',
className: 'row',
childView: JointView,
onRender: function(){
// jointjs' paper is added here long after jointjs custom element init.
this.el.appendChild(app.Paper.el);
},
onDestroy: function(){
// removal of the paper is done here
this.el.removeChild(app.Paper.el);
},
buildChildView: function(child, JointView, childViewOptions){
// add model, jointjs' paper and graph into JointView's options
var options = _.extend({model: child}, childViewOptions);
options = _.extend({paper: app.Paper, graph: app.Graph}, options);
return new JointView(options);
}
//[...]
})
JointView.js (magic here!)
module.exports = Marionette.ItemView.extend({
tagName: 'div',
className: 'html-element',
template: "#agentTmpl",
// store here just in case
cell: null,
// [...]
initialize: function() {
// initialize joinjs element-shape here
Marionette.bindEntityEvents(this, this.model, this.modelEvents);
if(this.cell == null){
//notice `el: this.el` This will actually pass the option el to ElementView. Surprised?
//Yeah me too. From there i can do with the dom-element whatever i want
this.cell = new joint.shapes.html.Element({ el: this.el, position: { x: 80, y: 80 }, size: { width: 250 } });
}
},
onRender: function(){
// after rendering add cell to graph
this.options.graph.addCell(this.cell);
},
onDestroy: function(){
// after removal remove cell from graph
this.cell.remove();
}
});
Element.js
ElementView.js
For simplicity more or less like here: http://www.jointjs.com/tutorial/html-elements
To summarize what actually happens is: whenever a new Element is created ElementView will fire all necessary event (initialize, render & whatnot). From there you can manipulate the drawn svg elements or overlap (similar to the tutorial) with my previously created JointView's html. I basically put my JointView dom-element over the SVG which is drawn by jointjs.
There you go. Fixed!
Related
Hi Im a novice in ExtJs script, im trying to develop custom multifield, i was able to understand the node creation part, but in the scripting part im unable to catch some of the things like in listener adding scope:this, fn:this.updatehidden i tried to google out the answer but i din get any satisfactory answer. so could any one please explain me the scope:this part
and why we call superclass constructor in the initcomponent,any related resources are also welcome
Thanks in advance
Love to code
Ejst.CustomWidget = CQ.Ext.extend(CQ.form.CompositeField, {
/**
* #private
* #type CQ.Ext.form.TextField
*/
hiddenField: null,
/**
* #private
* #type CQ.Ext.form.ComboBox
*/
allowField: null,
/**
* #private
* #type CQ.Ext.form.TextField
*/
otherField: null,
constructor: function(config) {
config = config || { };
var defaults = {
"border": false,
"layout": "table",
"columns":2
};
config = CQ.Util.applyDefaults(config, defaults);
Ejst.CustomWidget.superclass.constructor.call(this, config);
},
// overriding CQ.Ext.Component#initComponent
initComponent: function() {
Ejst.CustomWidget.superclass.initComponent.call(this);
this.hiddenField = new CQ.Ext.form.Hidden({
name: this.name
});
this.add(this.hiddenField);
this.allowField = new CQ.form.Selection({
type:"select",
cls:"ejst-customwidget-1",
listeners: {
selectionchanged: {
scope:this,
fn: this.updateHidden
}
},
optionsProvider: this.optionsProvider
});
this.add(this.allowField);
this.otherField = new CQ.Ext.form.TextField({
cls:"ejst-customwidget-2",
listeners: {
change: {
**scope:this,
fn:this.updateHidden**
}
}
});
this.add(this.otherField);
},
// overriding CQ.form.CompositeField#processPath
processPath: function(path) {
console.log("CustomWidget#processPath", path);
this.allowField.processPath(path);
},
// overriding CQ.form.CompositeField#processRecord
processRecord: function(record, path) {
console.log("CustomWidget#processRecord", path, record);
this.allowField.processRecord(record, path);
},
// overriding CQ.form.CompositeField#setValue
setValue: function(value) {
var parts = value.split("/");
this.allowField.setValue(parts[0]);
this.otherField.setValue(parts[1]);
this.hiddenField.setValue(value);
},
// overriding CQ.form.CompositeField#getValue
getValue: function() {
return this.getRawValue();
},
// overriding CQ.form.CompositeField#getRawValue
getRawValue: function() {
if (!this.allowField) {
return null;
}
return this.allowField.getValue() + "/" +
this.otherField.getValue();
},
// private
updateHidden: function() {
this.hiddenField.setValue(this.getValue());
}
});
// register xtype
CQ.Ext.reg('ejstcustom', Ejst.CustomWidget);
Class hierarchy, superclass constructor:
You are calling the superclass initComponent function because you want the functionality of the derived class's hierarchy to be available.
For example, if you want to construct an elephant:
First you set some properties, like "big" and "gray" and "female".
Then you construct a mammal with these properties.
The mammal class constructor will itself set some properties, like "has a head", and then call the animal constructor, so if you don't call the mammal constructor from elephant, you don't even get an animal at all!
The animal constructor will then inspect the properties and create an animal.
Then, the mammal class will add details that the animal class didn't cover, e.g. the breast.
After the mammal constructor has finished, the elephant constructor adds the details that the mammal class doesn't cover, for example the trunk.
If you would use the standard ExtJS syntax for this (not sure whether CQ has it's own "standard syntax"), the elephant definition would look like this:
Ext.define('Elephant',{
extend:'Mammal',
initComponent:function() {
var me = this;
// set config properties. Two possible calls:
// "Ext.apply" overwrites config properties already defined by the subclass before constructor has been called
// "Ext.applyIf" only sets config properties that have NOT been set by the subclass!
// Since a MiniElephant subclass may want to set size:"small", we use applyIf here.
Ext.applyIf(me,{
size:'big',
color:'gray'
});
me.callParent(arguments); // <- call constructor of superclass
me.addTrunk(); // <- postprocessing
},
addTrunk:function() {
var trunk = Ext.create('Trunk',{
...
});
me.getHead().add(trunk);
// since addTrunk is called after the mammal constructor has been executed,
// the head is already initialized and the getHead function available!
}
});
Ext.define('Mammal',{
extend:'Animal',
initComponent:function() {
var me = this;
// Every mammal has a head, so we force the property into here using "apply"!
Ext.apply({
hasHead:true,
...
});
me.callParent(arguments); // <- construct animal
me.addBreast(); // <- add breast
},
getHead:function() {
return this.headerEl;
},
...
});
Listener scope:
A listener is a function. Every function has a so-called scope, which is the object that you will get when you access this from inside the function. As long as you don't use this inside your function, the scope doesn't matter to you.
By default, in JavaScript, the scope of a function is the object that the function is attached to, so if you have an object
var listeners = {
update:function() {
console.log(this);
}
};
and if you call the function like this:
listeners.update()
it will log the listeners object to the console; but if you do it like this:
var fn = listeners.update;
fn();
it won't! The scope of a function can be set if you call the function:
listeners.update.call(myScope, firstParameter, secondParameter, ...)
or if you apply it:
listeners.update.apply(myScope, parameterArray)
(Good to remember: Apply takes the Array!)
Since, in ExtJS, the listeners configuration is processed by an Observable mixin, which puts the functions into specially crafted sub-objects, the default scope won't make sense at all to an ExtJS programmer, so they have changed it. And for convenience, ExtJS has added a config property that can be used by the programmer to define his intended scope of the function.
So if you define a panel and add a field inside:
Ext.apply(me, {
items:[{
xtype:'textfield',
listeners:{
update:function() {
console.log(this); // <- returns the panel, because...
},
scope:me // <- we are scoping to the panel!
}
}
});
I have a Sencha class that extends Ext.draw.Component and it accepts a store of MyModel. I am trying two different methods and I am getting differing, non-satisfactory results.
The First Method
Inside the constructor of the class I read in the store and do the following:
//Inside constructor of the class
this.store = config.store; //config is passed in from the constructor
var me = this;
me.store.each(function (model) {
me.renderTo = model.get('elementToRenderTo');
me.items = [{
type: 'rect',
x: 1.6620979,
y: 52.362183,
radius: 90,
width: 448.10959,
height: 1000,
fill: model.get('color'),
stroke: 'none'
}];
if (me.items) {
Ext.apply(config, { //config is passed in from the constructor
items: me.items
});
}
me.callParent([config]);
}
When I put the last of the code where it is (inside of the store.each), I get an exception:
Uncaught TypeError: Cannot call method 'apply' of undefined
The Second Method
However, if I move the Ext.apply and callParent outside of the store.each, I don't get an expection, but only the last model gets drawn (likely because the me.items is being overwritten on each iteration of the model).
//Inside constructor of the class
this.store = config.store; //config is passed in from the constructor
var me = this;
me.store.each(function (model) {
me.renderTo = model.get('elementToRenderTo');
me.items = [{
type: 'rect',
x: 1.6620979,
y: 52.362183,
radius: 90,
width: 448.10959,
height: 1000,
fill: 'black',
stroke: 'none'
}];
}
if (me.items) {
Ext.apply(config, { //config is passed in from the constructor
items: me.items
});
}
me.callParent([config]);
Is there another way to create a custom Ext.draw.Component that uses a store? What am I missing? The second method doesn't seem right, but I can't get rid of the exception on the first.
There are a few possible issues with this code:
1
this.store = config.store;
Is this the store instance or a string config? The proper way to deal with store configs in a constructor is like so:
this.store = Ext.data.StoreManager.lookup(this.store || 'ext-empty-store');
2
Although you are using me for scope, you probably want to ensure that the scope of each is indeed that of what's outside it so:
me.store.each(function (model) { ... }, this);
3
Regardless of where you do this, you won't be pushing all of the items:
Ext.apply(config, {
items: me.items
});
Because what you do here is you keep override item with me.items.
You shouldn't apply anything to items - it's an array managed by the component itself. You should really add items to it instead:
items.push( me.items )
4
Are you assuming local store only? Because if your store is to be loaded asynchronously - you'd get nothing unless you load the items upon load.
5
What are you actually trying to do here? Have the items loaded from a store? If such is the case you shouldn't do it in the constructor.
You should really look at one of the Ext source files to see how this is to be done. Here's a simplified version of how Ext.panel.Table does it:
Ext.define('Ext.panel.Table', {
extend: 'Ext.panel.Panel',
initComponent: function() {
var me = this,
store = me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');
me.mon(store, {
load: me.onStoreLoad,
scope: me
});
},
onStoreLoad: function() {
}
});
I'm trying to use Backbone.localStorage as a backend to an app.
I wrote a Model called Era, and a corresponding EraView. Pressing enter in the EraView causes the model to be saved, and this is where I get the error:
Uncaught Error: A "url" property or function must be specified
urlError backbone.js:1509
_.extend.url backbone.js:515
_.result underscore-min.js:1060
Backbone.sync backbone.js:1410
Backbone.sync backbone.localStorage.js:188
_.extend.sync backbone.js:276
_.extend.save backbone.js:476
karass.EraView.Backbone.View.extend.close era.js:61
karass.EraView.Backbone.View.extend.updateOnEnter era.js:75
Here is the code to the EraView
var karass = karass || {};
// Era Item View
// the DOM element for an era item
karass.EraView = Backbone.View.extend({
tagName: 'li',
className: 'era',
template: _.template( $('#era-template').html() ),
// The DOM events specified to an item
events: {
'dblclick .edit-input': 'edit',
'keypress .edit-input': 'updateOnEnter',
//'blur .edit': 'close',
},
// The EraView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between an era and a EraView in this karass,
// we set a direct reference on the model for convenience.
initialize: function(){
_.bindAll(this);
this.model.on('change', this.render, this);
},
// Re-renders the era item to the current state of the model and
// updates the reference to the era's edit input within the view
render: function(){
this.$el.html( this.template(this.model.attributes));
this.$era_start = this.$('.era-start');
this.$era_end = this.$('.era-end');
this.$era_start.attr('disabled', true);
this.$era_end.attr('disabled', true);
return this;
},
// Switch this view into editing mode, displaying the input field
edit: function(){
this.$('.edit-input').removeAttr('disabled');
this.$el.addClass('editing');
this.$('.edit-input').addClass('editing');
},
// Close the editing mode, saving changes to the era
close: function(){
this.$('.edit-input').attr('disabled', true);
var start = this.$era_start.val().trim();
var end = this.$era_end.val().trim();
if(start && end){
this.model.save({from: start, until: end});
}
this.$el.removeClass('editing');
this.$('.edit-input').removeClass('editing');
this.trigger('close');
},
updateOnEnter: function(e){
if(e.which !== ENTER_KEY && (!this.$era_start.val().trim() || !this.$era_end.val().trim())){
return;
}
this.close();
}
});
And this is the code for the era model:
var karass = karass || {};
karass.Era = Backbone.Model.extend({
defaults: {
from: '',
until: ''
},
});
I thought I didn't need a url while using localStorage.
Edit: I forgot to mention that while this behavior occurs in the Era/EraView itself, it also occurs in the Name model, which extends Era. Name in turn belongs in a Names collection. I don't know if this makes a difference, but I figured I add it.
Edit 2: The Names collection looks like this:
karass.Names = History.extend({
model: karass.Name,
localStorage: new Backbone.LocalStorage('karass-names'),
});
Edit 3: I posted all the code on jsfiddle: http://jsfiddle.net/herrturtur/CRv6h/
You don't need an url while using localStorage. But you need to set the localStorage property on your model or on your collection (if you set the localStorage on a collection the models inside the collection will "inherit" this setting):
karass.Era = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("EraCollection"),
// the EraCollection should be an unique name within your app.
defaults: {
from: '',
until: ''
},
});
If you don't setup the localStorage property the plugin falls back to the default ajax sync so you get the uri missing exception.
Is it possible to dynamically add and remove regions to a layout with Marionette? My app needs to be able to push and pop regions from a layout. This is similar to how GitHub pushes and pops views when you drill down in the source code of a project. They have the slide over animation when presenting the next view and then it slides back when you're backing out. The idea is that I need to keep the previous views around. Another analogy would be how UINavigationControllers work on iOS.
Or maybe I should just define a custom layout that is able to handle adding and removing regions on the fly?
I ended up implementing a container view to fit my needs. It cleans up event refs like you'd expect in Marionette.
https://github.com/ayoung/backbone-vs-marionette/blob/marionette/public/js/views/mainContainer.js
I'm not sure but you may be getting confused with the existence of some html and the displaying of that html?
I mean you can make a CompositeView of Items and only show one of the items at a time. Then use jQuery animate or another animation library to move through the CompositeView's Items.
Yes it is possible. Here is the code I use.
The layout:
var Layout = Marionette.LayoutView.extend({
initialize: function(options) {
options = _.extend({ regionTag: 'div' }, options);
this.mergeOptions(options, ['regionTag', 'regionName']);
},
template: false,
regions: {},
append: function(view) {
var viewClass = 'dynamic-layout-' + this.regionName,
viewCount = $('.' + viewClass).length + 1,
viewId = this.regionName + '-view-' + viewCount,
$el = $('<' + this.regionTag + '/>', {
id: viewId,
class: viewClass
});
this.$el.append($el);
var region = Marionette.Region.extend({
el: '#' + viewId
});
this.regionManager.addRegion(viewId, region);
this.regionManager.get(viewId).show(view);
},
appendEmpty: function(id, className, tag) {
tag = tag || 'div';
var data = { id: id, className: className, tag: tag };
var $el = Marionette.Renderer.render('#append-layout-template', data);
this.$el.append($el);
var region = Marionette.Region.extend({
el: '#' + id
});
this.regionManager.addRegion(id, region);
},
customRemove: function(regionId) {
this.regionManager.removeRegion(regionId);
}
});
A helpful template:
<script type="text/template" id="append-layout-template">
<<%= tag %> id='<%= id %>' class='<%= className %>'></<%= tag %>>
</script>
The controller:
var view = new SomeView();
// the region name will be a part of a unique id
var layout = new Layout({ regionName: 'myRegion' });
// add a dynamic region to the layout and a view to that region
layout.append(view);
// same as above (you have to name the id and class yourself)
var regionId = 'myRegionId';
layout.appendEmpty(regionId, 'someClassName', 'span');
layout.getRegion(regionId).show(view);
// remove a region
layout.customRemove(regionId);
I'm adding a custom context menu to a TreePanel.
This was all working when I had a separate function for the context menu, but I was having problems where the context menu items would end up doubled/tripling up if I clicked on one of the options and then viewed the context menu again.
I had a look around for other contextmenu examples and came up with this one by Aaron Conran I pretty much "stole" it wholesale with a few additions, tacking the function directly into the Ext.ext.treePanel config. This gave me an error about "oe is undefined" which seemed to refer to "contextmenu: this.onContextMenu" in the tree config.
I figured it was probably something to do with the way I was defining all of this, so I decided to look at extending Ext.ext.TreePanel with my function in it as a learning exercise as much as anything.
Unfortunately, having managed to sort out extending TreePanel I'm now back to getting "oe is undefined" when the page tries to build the TreePanel. I've had a look around and I'm not really sure whats causing the problem, so any help or suggestions would be greatly appreciated.
Here is the code that is used to define/build the tree panel. I hope its not too horrible.
siteTree = Ext.extend(Ext.tree.TreePanel,{
constructor : function(config){
siteTree.superclass.constructor.call(this, config);
},
onContextMenu: function(n,e){
if (!this.contextMenu){
console.log('treeContextMenu',n,e);
if (n.parentNode.id == 'treeroot'){
var menuitems = [{text:'Add Child',id:'child'}];
} else {
var menuitems =
[{text:'Add Child',id:'child'},
{text:'Add Above',id:'above'},
{text:'Add Below',id:'below'}];
}
this.contextMenu = new Ext.menu.Menu({
id:'treeContextMenu',
defaults :{
handler : treeContextClick,
fqResourceURL : n.id
},
items : menuitems
});
}
var xy = e.getXY();
this.contextMenu.showAt(xy);
}
});
var treePanel = new siteTree({
id: 'tree-panel',
title : 'Site Tree',
region : 'center',
height : 300,
minSize: 150,
autoScroll: true,
// tree-specific configs:
rootVisible: false,
lines: false,
singleExpand: true,
useArrows: true,
dataUrl:'admin.page.getSiteTreeChildren?'+queryString,
root: {
id: 'treeroot',
nodeType: 'async',
text: 'nowt here',
draggable: false
},
listeners:{
contextmenu: this.onContextMenu
}
});
As a total aside; Is there a better way to do this in my context menu function?
if (n.parentNode.id == 'treeroot') {
Basically, if the clicked node is the top level I only want to give the user an add Child option, not add above/below.
Thanks in advance for your help
In your instantiation of your siteTree class you have:
listeners: {
contextmenu: this.onContextMenu
}
However, at the time of the instantiation this.onContextMenu is not pointing to the onContextMenu method you defined in siteTree.
One way of fixing it is to call the method from within a wrapper function:
listeners: {
contextmenu: function() {
this.onContextMenu();
}
}
Assuming you don't override the scope in the listeners config 'this' will be pointing to the siteTree instance at the time the listener is executed.
However, since you are already defining the context menu in the siteTree class, you may as well define the listener there:
constructor: function( config ) {
siteTree.superclass.constructor.call(this, config);
this.on('contextmenu', this.onContextMenu);
}
Ensuring the context menu is removed with the tree is also a good idea. This makes your siteTree definition:
var siteTree = Ext.extend(Ext.tree.TreePanel, {
constructor: function( config ) {
siteTree.superclass.constructor.call(this, config);
this.on('contextmenu', this.onContextMenu);
this.on('beforedestroy', this.onBeforeDestroy);
},
onContextMenu: function( node, event ) {
/* create and show this.contextMenu as needed */
},
onBeforeDestroy: function() {
if ( this.contextMenu ) {
this.contextMenu.destroy();
delete this.contextMenu;
}
}
});
I had this problem yesterday. The issue with the duplicate and triplicate items in the context menu is due to extjs adding multiple elements to the page with the same ID. Each time you call this.contextMenu.showAt(xy) you are adding a div with the ID 'treeContextMenu' to the page. Most browsers, IE especially, deal with this poorly. The solution is to remove the old context menu before adding the new one.
Here is an abridged version of my code:
var old = Ext.get("nodeContextMenu");
if(!Ext.isEmpty(old)) {
old.remove();
}
var menu = new Ext.menu.Menu({
id:'nodeContextMenu',
shadow:'drop',
items: [ ... ]
});
menu.showAt(e.xy);
I suggest never using hardcoded IDs. #aplumb suggests cleaning the DOM to reuse an existing ID. OK, but I suggest you cleanup the DOM when you no longer need the widgets/elements in the DOM and you should never reuse an ID.
var someId = Ext.id( null, 'myWidgetId' );
var someElement = new SuperWidget({
id: someId,
...
});
Just to add to owlness's answer
This bit here:
listeners: {
contextmenu: this.onContextMenu
}
Gets executed when the javascript file is loaded. this at that stage is most likely pointing to the window object.
A simple way to fix it is adding the listener on hide event of context menu, so you destroy him.
new Ext.menu.Menu(
{
items:[...],
listeners: { hide: function(mn){ mn.destroy(); } }
}
).show(node.ui.getAnchor());
;)