I'm building a Backbone app where a Flask/Python server sends latitude and longitude data to the front end through a websocket. The each location will then be plotted in real-time using D3. I want to have a start and stop button which will open and close the websocket. I can't figure out how to get the websocket to close. I have checked that the stop button is in fact triggering locMap.model.stream.socket.close();. I'm very new to Backbone and am also unsure where the jQuery I wrote for starting and stopping the app is supposed to go. Here is my code (with some of the irrelevant D3 code removed):
// Initialize the websocket and wrap it in a Backbone Event
LocationStream = function () {
this.addr = "ws://" + document.domain + ":5000/websocket";
_.extend(this, Backbone.Events);
var self = this;
self.socket = new WebSocket(this.addr);
console.log("Using a standard websocket");
self.socket.onopen = function(e) {
self.trigger('open',e);
console.log('socket opened');
};
self.socket.onerror = function (e) {
self.trigger('error', e);
};
self.socket.onmessage = function (e) {
self.trigger('message', e);
self.trigger('data', e.data);
self.trigger('point', JSON.parse(e.data))
};
self.socket.onclose = function (e) {
self.trigger('close', e);
console.log("socket closed");
};
self.socket.onerror = function (e) {
self.trigger('error', e);
};
};
// Models
// Create a Backbone model to keep track of streaming location data
LocationSequence = Backbone.Model.extend({
initialize: function (opts) {
this.stream = opts.stream;
var self = this;
this.stream.on('point', function (pt) {
var points = self.get('points');
var updated = (new Date()).getTime();
var coordinates = projection([pt.long, pt.lat]);
points.push([updated, coordinates]);
self.set({points: points, last_update: updated});
});
},
defaults: {
points: []
}
});
// Views
// Add a #locations 'g' element to the SVG containing the map of the US
// location points will be appended to this element
var LocationMap = Backbone.View.extend({
initialize: function () {
d3_map.append("svg:g")
.attr("id", "locations");
this.model.on('change', this.render, this);
},
render: function () {
var locations = d3_map.select("#locations").selectAll('circle')
.data(this.model.get('points'),
function (d) { return d[0]; });
locations.enter()
.append("svg:circle")
.attr("id", "circle")
.attr("cy", function(d) { return d[1][1]; })
.attr("cx", function(d) { return d[1][0]; })
.attr("r", 4.5);
},
});
$("#start").click(function () {
var locStream = new LocationStream();
var locations = new LocationSequence({stream: locStream});
var locMap = new LocationMap({model: locations});
console.log("start triggered");
$("#stop").click(function () {
locMap.model.stream.socket.close();
});
});
Am I being dense or would this not do it:
locStream.socket.close();
Sorry, I just saw this and was wondering if you ever got an answer. I don't have the ranking to even comment, so I had to make this "answer."
Let us know if you solved it!
Related
This is one of question from a Backbone newbie.
So, I was trying to use this.model.bind and this.model.on('change', this.render), but it could not work for my model. I checked my console & my model function getStoreByName. This is returning an array, but render function is rendering before the fetch array, that's why I want to bind model to view when model change.
Here is how far I have gotten so far.
This is Backbone view:
var storeTemplate = Backbone.View.extend({
initialize: function () {
console.log('view inti is running');
this.template = template;
this.model = new storeModel();
this.model.getStoreByName();
this.stores = this.model.get('stores');
this.model.on('change', this.render);
console.log(this.stores);
console.log('ready to call this.render()');
this.render();
console.log('end to call this.render()');
console.log('view init end');
},
render: function () {
console.log('render is start');
this.logger.log(Logger.LOG_LEVELS.TRACE, "Entering render.");
console.log(this.model.stores);
this.$el.html(_.template(this.template, { stores: this.model.get('stores') }));
return this;
}
});
return storesTemplate;
});
and this is my Backbone Model
var store= Backbone.Model.extend({
initialize: function () {
console.log('init models is running');
this.stores = [];
this.set({ 'stores': this.stores});
console.log(this.get('stores'));
this.service = Service;
},
getStoreByName: function () {
console.log('getting store');
stores = [];
this.service.getStoreByName(function (xml) {
$(xml).find("element").each(
function () {
var store = {
"storeID": $(this).find("ID").text(),
"storeType": $(this).find("Type").text(),
"storeName": $(this).find("Name").text(),
};
if (xml !== null) {
stores.push(store);
}
else {
this.model.set({ stores: [] });
}
}
);
that.set('stores', store)
},
);
},
})
return store;
});
Try this.listenTo(this.model,'change:stores', this.render);
If that doesn't work use promises. Update model like this:
getStoreByName: function() {
var deferred = $.Deferred();
var stores = [];
return this.service.getStoreByName(function(xml) {
if (xml === null) {
return Deferred.resolve([]);
}
$(xml).find("element").each(function() {
var store = {
"storeID": $(this).find("ID").text(),
"storeType": $(this).find("Type").text(),
"storeName": $(this).find("Name").text(),
};
stores.push(store);
});
return Deferred.resolve(stores);
});
this.model.set('stores', stores);
return deferred.promise();
}
and in the view you can do this.model.getStoreByName().then(this.render);
I'm trying to get it so that when my backbone collection is saving if it hits an error I can do something with it. However when the form is saved the Render All Tasks button's click event is always triggered. How can I get it to stop iterating the collection if an error is found? Or otherwise how can I get it to call the fail function?
var EditTaskView = AddTaskView.extend({
template: _.template($("#individualTaskEditView").html()),
events: {
"submit": function (e) {
e.preventDefault();
if (this.model.isValid(true) && this.ScheduleView.isValid() && this.ProviderView.isValid()) {
$.when(this.model.save(), this.FiltersView.saveAll())
.done(function () {
$("#RenderAllTasks").trigger("click");
})
.fail(function (xhr, status, errorMessage) {
var message = JSON.parse(xhr.responseText).Message;
RenderError(message, "#EditTaskDetailsTabError");
});
}
}
}
};
var FieldCollectionAddView = Backbone.View.extend({
tagName: "div",
render: function () {
// iterate the collection
this.collection.each(function (field) {
// render the view and append to the collection
var view = new FieldAddView({ model: field });
var rendered = view.render();
this.$el.append(rendered.el);
}, this);
return this;
},
isValid: function () {
var valid = true;
_.each(this.collection.models, function (model) {
if (!model.isValid(true)) {
valid = false;
}
});
return valid;
},
saveAll: function () {
var errorsFound = false;
_.each(this.collection.models, function (model) {
model.save(null, {
error: function (error) {
//TODO: do soemthing with the error
var message = JSON.parse(el.responseText).Message;
RenderError(message, "#ProviderDetailsTabError");
}
}, { wait: true });
});
return errorsFound;
}
});
var ProviderAddView = Backbone.View.extend({
tagName: "div",
template: _.template($("#providerAddTemplate").html()),
render: function () {
// append the template to the element
this.$el.append(this.template);
//render provider types
this.ProviderTypes = RenderProviderTypes(this.model.attributes.ProviderTypes);
var providerTypesDiv = _.template($("#ProviderTypesTemplate").html());
$("#ProviderTypesDiv", this.$el).html(providerTypesDiv);
$("#ProviderTypesSelectDiv", this.$el).html(this.ProviderTypes.render().el);
$("#ProviderTypes", this.$el).val(this.model.attributes.ProviderType);
// render field collection
var collection = new FieldCollection(this.model.attributes.ProviderFieldList);
var fieldsView = new FieldCollectionAddView({
collection: collection
});
this.FieldsAddView = fieldsView;
// append the fields to the element
$("#fieldsDiv", this.$el).append(fieldsView.render().el);
this.stickit();
return this;
},
events: {
"submit #NewProviderForm": function (e) {
e.preventDefault();
if (this.FieldsAddView.isValid()) {
var fieldsView = this.FieldsAddView;
this.model.save(null, {}, { wait: true })
.success(function (result) {
var filters = new FilterCollection();
$.when(fieldsView.saveAll(),
filters.fetch({
data: $.param({
taskId: result.attributes.TaskId
})
}))
.done(function() {
if (!$("#FiltersForm").html()) {
var view = new FilterCollectionView({ collection: filters });
assign(view, "#FilterDetails");
$("#FiltersForm").append(buttonsTemplate);
$("#FilterDetailsTab").parent("li").removeClass("disabled");
$("#FilterDetailsTab").attr("data-toggle", "tab");
}
$("#FilterDetailsTab").tab("show");
});
})
.error(function (xhr, el, other) {
var message = JSON.parse(el.responseText).Message;
RenderError(message, "#ProviderDetailsTabError");
});
}
}
},
isValid: function () {
return this.model.isValid(true) && this.FieldsAddView.isValid();
},
save: function () {
this.model.save();
this.FieldsAddView.saveAll();
}
});
Ok, so I found how this has to be done. In short the code had to be changed so that it was syncing the entire collection at once using Backbone.sync instead of iterating over it and saving each model individually. Also when you're ready to save the collection you use the $.when function so it completes the whole sync before deciding what to do. Below is the relevant code showing the changes that were made.
var FieldCollection = Backbone.Collection.extend({
url: "/api/Field",
model: FieldModel,
syncAll: function () {
return Backbone.sync("create", this)
.error(function (xhr, el, other) {
var message = JSON.parse(xhr.responseText).Message;
var tab = "";
var activeTab = $("#Tabs li.active a").attr("id");
if (activeTab === "TaskListTab") {
tab = "#EditTaskDetailsTabError";
}
else if (activeTab === "NewTaskTab") {
tab = "#ProviderDetailsTabError";
}
RenderError(message, tab);
});
}
});
var FieldCollectionAddView = Backbone.View.extend({
saveAll: function () {
return this.collection.syncAll();
}
});
var ProviderAddView = Backbone.View.extend({
events: {
"submit #NewProviderForm": function (e) {
e.preventDefault();
if (this.FieldsAddView.isValid()) {
var fieldsView = this.FieldsAddView;
$.when(fieldsView.saveAll())
.done(function() {
// success
});
}
}
}
});
In the router I do this
function test() {
self.topbarView = new TopbarView();
self.topbarView.render();
GhostviewHunter.addView(self.topbarView);
}
function clean() {
console.log(GhostviewHunter.currentViews.length);
GhostviewHunter.clean();
}
setInterval(test, 1000);
setInterval(clean, 1000);
ghostviewhunter should clean/remove the views:
define('ghostviewHunter', [], function() {
var GhostviewHunter = function() {};
GhostviewHunter.prototype.currentViews = [];
GhostviewHunter.prototype.addView = function(view) {
this.currentViews.push(view);
}
GhostviewHunter.prototype.clean = function() {
_.each(this.currentViews, function(view) {
view.remove();
});
this.currentViews.length = 0;
}
GhostviewHunter.__instance = null;
GhostviewHunter.getInstance = function() {
if( GhostviewHunter.__instance == null ) {
GhostviewHunter.__instance = new GhostviewHunter();
}
return GhostviewHunter.__instance;
}
return GhostviewHunter.getInstance();
})
TopView is fetching a model, the model is updated every 1seconde with setInterval function.
I thought that remove(); would be enough be the memory leak is very quick when I monitor the app.
Any idea ?
EDIT:
TOPBARVIEW
define('topbarView', [
'backbone',
'parameterManager',
'text!views/topbarView/topbarTemplate.html',
'drupalidModel',
'weatherModel',
'refreshTime',
'dateParser'
], function(Backbone, ParameterManager, TopbarTemplate, DrupalidModel, WeatherModel, RefreshTime, DateParser) {
var TopbarView = Backbone.View.extend({
el: '#topbar',
template: _.template(TopbarTemplate),
events: {},
initialize: function() {
var self = this;
_.bindAll(this, 'render', 'startDateRefresh');
this.dateParser = new DateParser();
self.startDateRefresh();
setInterval(self.startDateRefresh, RefreshTime.date);
this.initWeatherModel();
},
render: function() {
var self = this;
var data = {
picto_url : ParameterManager.get('WEATHER_RESOURCE_URL') + ParameterManager.get('WEATHER_PICTO_CODE') + ".png",
date: self.date
}
this.$el.html(this.template({data: data}));
},
initWeatherModel: function() {
var self = this;
var weather_url = ParameterManager.get('WEATHER_URL');
if(weather_url === null) {
this.drupalidModel = new DrupalidModel();
this.drupalidModel.fetch({
success: function(model, response) {
var center_id_num = model.get('center_id_num');
ParameterManager.set('DRUPAL_CENTER_ID_NUM', center_id_num);
ParameterManager.constructWeatherUrl();
self.model = new WeatherModel();
self.listenTo(self.model,'change', self.render);
self.startModelRefresh();
},
error: function() {
console.log("Failed to fetch center id!");
}
})
} else {
this.model = new WeatherModel();
self.listenTo(self.model,'change', self.render);
this.startModelRefresh();
};
},
startModelRefresh: function() {
var self = this;
this.modelRefresh = function() {
self.model.fetch();
}.bind(this);
self.modelRefresh();
setInterval(self.modelRefresh, RefreshTime.weather);
},
stopModelRefresh: function() {
var self = this;
clearInterval( self.modelRefresh );
},
startDateRefresh: function() {
var self = this;
this.date = this.dateParser.classicDate();
this.render();
}
});
return TopbarView;
})
As fbynite suggested, your code which is supposed to clear the interval(s) is not correct, you should pass the interval id to clearInterval.
apart from that, you're not calling stopModelRefresh() at all. You should make sure all external references are properly removed before removing the view. For example I've added a destroy method that clears the interval before removing the view:
var TopbarView = Backbone.View.extend({
el: '#topbar',
template: _.template(TopbarTemplate),
events: {},
initialize: function() {
},
render: function() {
},
modelRefresh: function() {
this.model.fetch();
},
startModelRefresh: function() {
this.modelRefresh();
this.intervalId = setInterval(_.bind(this.modelRefresh,this), RefreshTime.weather);
},
stopModelRefresh: function() {
clearInterval(this.intervalId);
},
destroy: function() {
this.stopModelRefresh();
this.remove();
}
});
Now your GhostviewHunter should call it instead of directly calling remove:
GhostviewHunter.prototype.clean = function() {
_.each(this.currentViews, function(view) {
view.destroy();
});
this.currentViews.length = 0;
}
or you can even override the remove method itself to something like:
remove: function(){
this.stopThisInterval();
this.stopThatInterval();
this.cleanUpSomethingElse();
Backbone.View.prototype.remove.call(this);
}
and have the ghost thingy call remove itself.
Note that you have other interval calling startDateRefresh which you're not even attempting to clear... You should clear all such similarly.
And as a side note, I strongly suggest to stop spamming self = this where it is totally unnecessary for eg:
stopModelRefresh: function() {
var self = this;
clearInterval( self.modelRefresh );
// Why..? Nothing here changes the context?
},
and I also suggest recursively calling modelRefresh once the current fetch succeeds/fails rather than calling it from an interval where you have no guarantee that the previous fetch is complete
I have a set of data within a users profile which holds the UID of all the posts they have liked. I am using a data snapshot to show these images within the users profile, but I would like to load it lazy or use ionic infinite scroll to load more items as the user scrolls.
I tried by making two duplicate snap shots. First one intialises the feed upon the page loading, and then I wanted the second one to load by using ionic infinite scroller.
But it doesn't work.
Does anyone know of a better way to do this?
service.js
getWardrobeFeed: function(){
var defered = $q.defer();
var user = User.getLoggedInUser();
var wardrobeKeyRef = new Firebase(FIREBASE_URL + '/userProfile/' + user.uid + '/wardrobe');
wardrobeKeyRef.orderByKey().limitToLast(2).on('child_added', function (snap) {
var imageId = snap.key();
userImageRef.child(imageId).on('value', function (snap) {
$timeout(function () {
if (snap.val() === null) {
delete userWardrobeFeed[imageId];
} else {
userWardrobeFeed[imageId] = snap.val();
}
});
});
});
wardrobeKeyRef.orderByKey().limitToLast(2).on('child_removed', function (snap) {
var imageId = snap.key();
$timeout(function () {
delete userWardrobeFeed[imageId];
});
});
defered.resolve(userWardrobeFeed);
return defered.promise;
},
getWardrobeItems: function(){
var defered = $q.defer();
var user = User.getLoggedInUser();
var wardrobeKeyRef = new Firebase(FIREBASE_URL + '/userProfile/' + user.uid + '/wardrobe');
wardrobeKeyRef.orderByKey().limitToFirst(2).on('child_added', function (snap) {
var imageId = snap.key();
userImageRef.child(imageId).on('value', function (snap) {
$timeout(function () {
if (snap.val() === null) {
delete userWardrobe[imageId];
} else {
userWardrobe[imageId] = snap.val();
}
});
});
});
wardrobeKeyRef.orderByKey().on('child_removed', function (snap) {
var imageId = snap.key();
$timeout(function () {
delete userWardrobe[imageId];
});
});
defered.resolve(userWardrobe);
return defered.promise;
},
controller.js
$scope.userWardrobe = [];
Service.getWardrobeFeed().then(function(items){
$scope.userWardrobe = items;
});
$scope.loadMore = function() {
Service.getWardrobeItems().then(function(items){
$scope.userWardrobe = $scope.userWardrobe.concat(items);
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
Already a couple of hours struggle trying to solve this...
Although the model gets fetched correctly and I can verify it as the view gets informed of the model's 'change' event, it just does not render.
At startup, the default model data ('Test Project'), is correctly displayed in the view, but after the model is refreshed, the view is not refreshed.
I tried to show a new view in the layout after model refresh but it did not change much...
Any idea or opinion about this ?
App.Project = function () {
var Project = {};
var ProjectModel = Backbone.Model.extend({
defaults:{
id: 0,
name: "Test Project",
intro: "",
desc: ""
},
initialize: function () {
// Flag fetch status to avoid multiple simultaneous calls
this.loading = false;
var self = this;
App.vent.on("project:display", function (_id) { self.fetchProject(_id); });
},
fetchProject: function (_id) {
if (this.loading)
return true;
this.loading = true;
var self = this;
var id = _id;
this.url = 'data.project_'+id+'.json';
this.fetch({
success: function (_data) {
self.loading = false;
},
error: function () {
self.loading = false;
}
});
}
});
Project.Details = new ProjectModel();
var Layout = Backbone.Marionette.Layout.extend({
template: "#project-layout",
regions: { details: "#project_details" }
});
Project.initializeLayout = function () {
Project.layout = new Layout();
App.content.show(App.Project.layout);
};
App.addInitializer(function () {
App.Project.initializeLayout();
});
Project.display = function () {
App.Project.Views.showDetails(Project.Details);
App.vent.trigger("project:display", 1);
}
return Project;
}();
App.Project.Views = function () {
var Views = {};
var DetailView = Backbone.Marionette.ItemView.extend({
template: "#project-details-template",
tagName: "div",
initialize: function () {
//this.listenTo(this.model, "change", this.render, this);
},
modelEvents: {
'change': "modelChanged"
},
modelChanged: function() {
console.log(this.model);
this.render();
}
});
Views.showDetails = function (_project) {
var projectView = new DetailView({model: _project});
App.Project.layout.details.show(projectView);
};
return Views;
}();
App.ProjectRouting = function () {
var ProjectRouting = {};
ProjectRouting.Router = Backbone.Marionette.AppRouter.extend({
initialize: function (_options) {
this.route('project/', "displayProject", _options.controller.display);
}
});
App.vent.on("project:display", function (_id) {
App.navigate("project/");
});
App.addInitializer(function (_options) {
ProjectRouting.router = new ProjectRouting.Router({
controller: App.Project
});
});
return ProjectRouting;
}();