I'm working on a medium-complex app using backbone.js to handle wordpress data, and i can't figure out how to get the force working in a backbone layout.
basically, i'm trying to instantiate a force layout within a backbone boilerplate layout, like this:
myLayout = Backbone.Layout.extend({
initialize: function() {
var f = this; // i.e. the layout instance
f.force = d3.layout.force()
.nodes(myModels)
.on("tick", f.tick)
.gravity(0)
.friction(0.9)
.start();
console.log(f.force);
},
tick: function() {
// stuff to do when the force ticks
}
});
The problem is that the force is being defined with all blank functions, like gravity: function(x) { //lots of null things here }. i'm pretty sure it's a namespacing issue, but nothing i try works - i've tried doing $(window).force, var force, $this.force...
in my example tick is the only namespaced function, but i've tried doing that with all the others too (gravity, friction, etc.) to no avail (even though they should just be chaining onto the force object).
anyone have any ideas? i can't really post a .jsfiddle because the app is too complicated, so sorry in advance about that. The current version is up here
edit: here's how d3 can access the models successfully:
this works:
myLayout.nodes = myLayout.d3_wrapper.selectAll(".node")
.data(myModels)
.enter().append("g").attr("class", "node")
.attr("x",10)
.attr("y",10);
myLayout.nodes.append("clipPath")
.attr("id", function(d) { return d.get("slug"); })
as does this:
myLayout.nodes.append("clipPath")
.attr("id", function(d) { return d.attributes.slug });
edit: in the interest of clarity, here's the non-nicknamed code:
setforce: function() { // this gets called from the layout's initialize fn
console.log("setting force");
var f = this; // the layout
f.force = d3.layout.force()
.nodes(Cartofolio.elders.models) // Cartofolio is the module, elders is a Backbone Collection
.gravity(0)
.friction(0.9)
.start();
console.log(f.force);
}
I would try using toJSON() on your collection before passing it to d3:
myLayout = Backbone.Layout.extend({
initialize: function() {
var f = this; // i.e. the layout instance
f.force = d3.layout.force()
.nodes(myModels.toJSON())
.on("tick", f.tick)
.gravity(0)
.friction(0.9)
.start();
console.log(f.force);
},
tick: function() {
// stuff to do when the force ticks
}
});
Related
In my Backbone app, I have the following
playlistView = new PlaylistView({ model: Playlist });
Playlist.getNewSongs(function() {
playlistView.initialize();
}, genre, numSongs);
Playlist.getNewSongs() is called back when some ajax request is finished. I want to re-initialize the view then. However, I believe the way I'm doing it leads to this problem of a view listening to a same event twice. Is calling initialize() like this acceptable? If not, what should I do instead?
Update:
I wrote this chrome extension in Backbone to learn Backbone, and it's in a design hell at the moment. I am in the middle of refactoring the entire codebase. The snippet below is my PlaylistView initialize() code block.
var PlaylistView = Backbone.View.extend({
el: '#expanded-container',
initialize: function() {
var playlistModel = this.model;
var bg = chrome.extension.getBackgroundPage();
if (!bg.player) {
console.log("aborting playlistView initialize because player isn't ready");
return;
}
this.listenTo(playlistModel.get('songs'), 'add', function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').prepend(songView.render().el);
});
this.$('#song-search-form-group').empty();
// Empty the current playlist and populate with newly loaded songs
this.$('.playlist-songs').empty();
var songs = playlistModel.get('songs').models;
// Add a search form
var userLocale = chrome.i18n.getMessage("##ui_locale");
var inputEl = '<input class="form-control flat" id="song-search-form" type="search" placeholder="John Lennon Imagine">' +
'<span class="search-heart-icon fa fa-heart"></span>'+
'<span class="search-input-icon fui-search"></span>';
}
this.$('#song-search-form-group').append(inputEl);
var form = this.$('input');
$(form).keypress(function (e) {
if (e.charCode == 13) {
var query = form.val();
playlistModel.lookUpAndAddSingleSong(query);
}
});
// Fetch song models from bg.Songs's localStorage
// Pass in reset option to prevent fetch() from calling "add" event
// for every Song stored in localStorage
if (playlistModel.get('musicChart').source == "myself") {
playlistModel.get('songs').fetch({ reset: true });
songs = playlistModel.get('songs').models;
}
// Create and render a song view for each song model in the collection
_.each(songs, function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').append(songView.render().el);
}, this);
// Highlight the currently played song
var currentSong = playlistModel.get('currentSong');
if (currentSong)
var currentVideoId = currentSong.get('videoId');
else {
var firstSong = playlistModel.get('songs').at(0);
if (!firstSong) {
// FIXME: this should be done via triggering event and by Popup model
$('.music-info').text(chrome.i18n.getMessage("try_different_chart"));
$('.music-info').fadeOut(2000);
//console.log("something wrong with the chart");
return;
}
var currentVideoId = firstSong.get('videoId');
}
_.find($('.list-group-item'), function (item) {
if (item.id == currentVideoId)
return $(item).addClass('active');
});
},
It is not wrong but probably not a good practice. You did not post the code in your initialize but maybe you have too much logic here.
If you are simply initializing the view again so that the new data is rendered, you should use event listener as such:
myView = Backbone. View.extend ({
initialize : function() {
// We bind the render method to the change event of the model.
//When the data of the model of the view changes, the method will be called.
this.model.bind( "change" , this.render, this);
// Other init code that you only need once goes here ...
this.template = _.template (templateLoader. get( 'config'));
},
// In the render method we update the view to represent the current model
render : function(eventName) {
$ (this.el ).html(this .template ((this.model .toJSON())));
return this;
}
});
If the logic in your initiialize is something totally else, please include it. Maybe there is a beter place for it.
I am working on Marionette.behavior.I was trying to pass the behaviors hash dynamically at the time of view initialization but it is not getting assigned to the behaviors object of view.because behaviors are getting initialized at the time of view construction.
so we achieved the solution in the following way but is it the right way to achieve it?
is there any other way to achieve? and
why behaviors are not allowed to pass dynamically?
Here's the code:
var Behaviour = new Marionette.Application();
Behaviour.addRegions({
mainRegion:"#main-region"
});
var Person = Backbone.Model.extend({
defaults:{
firstName:"NA",
lastName:"NA",
phoneNumber:"NA",
presentAddr:"NA",
permanantAddr:"NA"
}
});
var buttonView=Marionette.ItemView.extend({
template:"#buttontemplate",
constructor:function(options){
this.behaviors = options.behaviors;
Marionette.ItemView.apply(this, arguments);
},
events:{
"click .display":"displayDetail"
},
displayDetail:function(){
this.triggerMethod("DisplayPersonDetails");
},
//behaviors:{Behavior1:{ },Behavior2:{ }}
})
var PersonDetailsView = Marionette.ItemView.extend({
template:"#static-template",
ui: {
"Change": ".change"
},
events:{
"click #ui.Change":"changeBehavior"
},
changeBehavior:function(){
},
});
var Behavior1 = Marionette.Behavior.extend({
onDisplayPersonDetails:function(){
var person=new Person({firstName:"abhijeet",lastName:"avhad",phoneNumber:"9604074690",permanantAddr:"sangamner",presentAddr:""})
var myView = new PersonDetailsView({model:person});
Behaviour.mainRegion.show(myView);
}
});
var Behavior2 = Marionette.Behavior.extend({
onDisplayPersonDetails:function(){
var person =new Person({firstName:"abhijeet",lastName:"avhad",phoneNumber:"9604074690",permanantAddr:"",presentAddr:"shivajinagar"})
var myView =new PersonDetailsView({model:person});
Behaviour.mainRegion.show(myView);
}
});
Behaviour.on("initialize:after", function(){
console.log(" started!");
Marionette.Behaviors.behaviorsLookup = function() {
return window.Behaviors;
};
window.Behaviors = {};
window.Behaviors.Behavior1 = Behavior1;
window.Behaviors.Behavior2 = Behavior2;
var buttonview=new buttonView({behaviors:{Behavior1:{ },Behavior2:{}}});
Behaviour.mainRegion.show(buttonview);
});
Behaviour.start();
The other way of achieving that is in your definition declare a function that returns the behaviors supplied at initialization, like this:
var buttonView=Marionette.ItemView.extend({
...
behaviors: function () {
return this.options.behaviors;
},
...
This is because the Marionette applies the behaviors in the constructor:
if (_.isObject(this.behaviors)) {
new Marionette.Behaviors(this);
}
You may try to do the same in your initialize method, but I'm not sure if it will work correctly if you already had some behaviors assigned beforehand.
After hacking through the source, I've come up with the following. It breaks encapsulation, which leads me to believe that there is probably a better way. Nonetheless, until I find it, this is going straight into production.
// Define Behavior.
var Behavior1 = { /* Behavior definition */ }
// Create View like normal.
var view = new ItemView({
behaviors: {
behavior1: { behaviorClass: Behavior1 }
}
});
// Here's the ugly part.
view.undelegateEvents();
view._behaviors = Marionette.Behaviors(subview);
view.delegateEvents();
After you do that, your Behaviors should all work.
Behavior can be passed directly with behaviorClass property within declaration of behaviors:
As seen in the marionette.behaviors docs, for example we have Tooltip behavior, which we want to pass directly and not from global list.
define(['marionette', 'lib/tooltip'], function(Marionette, Tooltip) {
var View = Marionette.ItemView.extend({
behaviors: {
Tooltip: {
behaviorClass: Tooltip, // <-- passing the behavior directly here
message: "hello world"
}
}
});
});
I am using same el for more than 1 view like below. I'm not facing any problem till now. Is this good approach or should i do any changes?
<div id="app">
<div id="app-header"></div>
<div id="app-container"></div>
<div id="app-footer">
</div>
App View:
{
el: "#app",
v1: new View1(),
v2: new View2(),
render: function () {
if (cond1) {
this.v1.render();
} else if (cond2) {
this.v2.render();
}}
}
View 1:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
View 2:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
By reading your question, I do not really see what advantages you could possibly have using this approach rather than having the different div elements being the root el for your views 1, 2, 3 and using
this.$el.html(template)
in the render method.
Your approach could work for a small application, but I think it will become really hard to maintain as the application grows.
EDIT
I still do not really get your point, you could only initialize everything only once in both cases.
Here is a working Fiddle.
By the way I am changing the content by listening to the click event but this is to simplify the example. It should be done by the router.
I do use a mixin to handle such situation, I call it stated view. For a view with all other options I will send a parameter called 'state', render will in-turn call renderState first time and there after every time I set a 'state' renderState will update the view state. Here is my mixin code looks like.
var setupStateEvents = function (context) {
var stateConfigs = context.getOption('states');
if (!stateConfigs) {
return;
}
var state;
var statedView;
var cleanUpState = function () {
if (statedView) {
statedView.remove();
}
};
var renderState = function (StateView) {
statedView = util.createView({
View: StateView,
model: context.model,
parentEl: context.$('.state-view'),
parentView:context
});
};
context.setState = function (toState) {
if (typeof toState === 'string') {
if (state === toState) {
return;
}
state = toState;
var StateView = stateConfigs[toState];
if (StateView) {
cleanUpState();
renderState(StateView);
} else {
throw new Error('Invalid State');
}
} else {
throw new Error('state should be a string');
}
};
context.getState = function () {
return state;
};
context.removeReferences(function(){
stateConfigs = null;
state=null;
statedView=null;
context=null;
})
};
full code can be seen here
https://github.com/ravihamsa/baseapp/blob/master/js/base/view.js
hope this helps
Backbone Rule:
When you create an instance of a view, it'll bind all events to el if
it was assigned, else view creates and assigns an empty div as el for that view and bind
all events to that view.
In my case, if i assign #app-container to view 1 and view 2 as el and when i initialize both views like below in App View, all events bind to the same container (i.e #app-container)
this.v1 = new App.View1();
this.v2 = new App.View2();
Will it lead to any memory leaks / Zombies?
No way. No way. Because ultimately you are having only one instance for each view. So this won't cause any memory leaks.
Where does it become problematic?
When your app grows, it is very common to use same id for a tag in both views. For example, you may have button with an id btn-save in both view's template. So when you bind btn-save in both views and when you click button in any one the view, it will trigger both views save method.
See this jsFiddle. This'll explain this case.
Can i use same el for both view?
It is up to you. If you avoid binding events based on same id or class name in both views, you won't have any problem. But you can avoid using same id but it's so complex to avoid same class names in both views.
So for me, it looks #Daniel Perez answer is more promising. So i'm going to use his approach.
I'm creating an ajax upload component which consists of a progress bar for each backbone view, this is how my view template looks like.
<script id="view-template-dropped-file" type="text/html">
<a><%=name %></a><span><%=fileSize%></span>
<div class="ui-progress-bar">
<div class="ui-progress"></div>
</div>
</script>
When I drop files on my drop area I create a view for each file like this
for (i = 0; i < files.length; i++) {
var view = new DroppedFileView({
model: new DroppedFile({
name: files[i].name,
fileSize: files[i].size
})
});
var $li = view.render().$el;
$('#droparea ul').append($li);
});
The drop area with some files added showing a progress bar for each file. http://cl.ly/Lf4v
Now when I press upload I need to show the progress for each file individually.
What I tried to do was to bind to an event in my DroppedFileView like this
initialize: function() {
var app = myapp.app;
app.bind('showProgress', this._progress, this);
}
and the _progress function
_progress: function(percentComplete) {
this.$el.find('.ui-progress').animateProgress((percentComplete * 100), function () { }, 2000);
}
and this is how I trigger the event from the drop area view
xhr: function () {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
app.trigger('showProgress', percentComplete);
}
}, false);
return xhr;
}
of course this will not work because I listen to the same showProgress event in all views which will cause all progress bars to show the same progress.
So, is it possible to bind an event to a specified view so the progress can be updated individually or is events not a good approach?
You might want to consider making the DroppedFile model emit the progress events. So simply instead of triggering the event on app, trigger it on the model instance which is being uploaded.
Your sample code doesn't mention which class holds the xhr method, but it would make sense to define it on the model itself. In which case the event triggering is trivial:
xhr: function () {
var model = this;
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
model.trigger('showProgress', percentComplete);
}
}, false);
return xhr;
}
And in view constructor:
initialize: function() {
this.model.bind('showProgress', this._progress, this);
}
Edit based on comments:
Even if your view structure is a bit more complicated than I assumed above, in my opinion using the DroppedFile model as event emitter is the way to go. If one DroppedFileView represents DroppedFile, it should reflect the state of the model it makes sense.
Just keep track of the models in DropzoneView, just like (or instead of how) you do now with the files in the DropzoneView.files. Whether you want to have the actual AJAX request to be the responsibility of the view or refactor it to the individual models doesn't really matter.
I have several views that have common code I'd like to abstract into a custom Backbone.View class. Is there any best practices for doing this?
is a good pattern to do something like this? :
// Base Grid view
var GridView = Backbone.View.extend({
initialize : function(){
//common view init code ..
//do the plug in overrides
if (options.addHandler)
this.addHandler = options.addHandler;
if (options.events)
//?? extend default events or override?
this.events = $.extend(this.events, options.events);
},
addHandler : function() {
//defaulthandler this code can be overridden
});
});
// in another object create some views from the GridView base
....
var overrides = { events:"xxx yyy", el: ulElement addHandler: myAddFunction }
var UserList = GridView.extend(overrides)
var userList = new UserList(users, options);
....
var coursesOverrides : {addHandler: ...}
var coursesOptions: {el: courseElement, ...}
var CourseList = GridView.extend(coursesOverrides)
var courseList= new CourseList (courses, coursesOptions)
// along the same lines maybe there's an abstraction for toolbar views
var ClassToolbarView = ToolbarBase.extend(toolOverrides)
var classtoolbar = new ClassToolbarView(actions, toolbaropts)
Any pointers to good examples of extending a View for refactoring common view code is appreciated.
First, I don't see the options being passed in your initializer(), so that's a bug.
Secondly, the .extend() method is inherited:
var GridView = Backbone.View.extend({ ... })
var GridViewWithNewFunctionsAndEvents = GridView.extend({ ... })
And you can replace or extend GridView's functionality, and call new GridViewWithNewFunctionsAndEvents() and get the extra functionality in a new object you need, just like you extend the Backbone stock View class.
If you need to extend the initializer, you can do this to call the initializer on the superclass:
var GridViewWithNewFunctionsAndEvents = GridView.extend({
initializer: function(options) {
GridView.prototype.initializer.call(this, options);
/* Your stuff goes here */
}
});