backbone.history.naviguate trigger router staying on the same page - backbone.js

My problem is pretty simple.
I start on /app, when I click on some button I trigger /report with
Backbone.history.navigate('report',{trigger: true, replace: true});
Then I'm on /report page. No problem.
I have the same button on /report and I would like to trigger router route "report" when i click on it. It seems it's not working because I'm already on the asked page.
Any idea how I should proceed ?
thanks a lot

Backbone.history.navigate('report',{trigger: true, replace: true});
You should not be using this to render/reload your application views.This is not recommended.
Rather you should have a controller object (a js object) which extends Backbone.Events
Define a callback for the for the route "/report" as follows:
var MyAppBus = _.extend({},Backbone.Events);
MyAppBus.on("report",function(payLoad){
// 1.Load the data
// 2.Create an instance of the view passing the loaded data.
// (In your case, you could also pass an instance of the view in the payload
// and update the data alone in the view instance.)
// 3.Render the view.
// 4.Call the router navigate function with trigger set to false to
// update the url on a need basis.
});
Now, in the event/router callback you can do something like this:
MyAppBus.trigger("report",payLoad);
Where payLoad in the event callback could be:
var payLoad = {};
payLoad.view = viewInstance;

Related

AngularJS $location replace() replacing last history entry also

On my AngularJS application I need to save only state changes in the browser history. That's why when I'm changing parameters of $location, I'm using replace() method.
For example, When I'm accessing /page1, it is save in the history. 'centre' parameter is added automatically with replace() so it doesn't add a new history entry:
$location.search('centre', hash).replace();
Every time I move a map, 'centre' changes.
When I go to /page2, the new entry in the history is created. When I move map, 'centre' changes.
The thing is that when I press BACK button, I'm going to /page1 but I need to keep 'centre' the same as it was before, but it changes to what was saved together with history entry of /page1.
How would I fix this issue?
I tried to add:
$window.history.replaceState({}, '', $location.absUrl());
After I do replace(), but didn't work.
I found calling the search & replace inside a 0ms timeout ensures it happens in a separate digest cycle from the main state change and prevents the previous state being replaced.
Something like:
$timeout(function() {
$location.search('centre', hash).replace();
}, 0);
This is how I solved this.
First, we need to save search result as previous and current:
$rootScope.$on('$locationChangeStart', function (event, toURL, fromURL) {
if ($rootScope.search.current) {
$rootScope.search.previous = $rootScope.search.current;
}
$rootScope.search.current = $location.search();
});
Then we need to always change the actual location we are in at the moment.
$rootScope.$on('$locationChangeSuccess', function (event, toURL, fromURL) {
$rootScope.actualLocation = $location.path();
});
And if back of forward button was pressed, center must be changed in the URL (using previous search results) without pushing a new history entry.
$rootScope.$watch(function () { return $location.path() }, function (newLocation, oldLocation) {
if ($rootScope.actualLocation === newLocation) {
$location.search('center', $rootScope.search.previous.center).replace();
}
});
I had the same problem and could only get it working by using the native history api, like so:
window.history.replaceState(hash, null, $location.absUrl());

Backbone go to same route won't refresh the page

I have a very simple tag which will go the current route, since I need to refresh the page to clear some messages.
<span>Click here to go get your password back</span>
<-- I am at /resetpassword route now -->
But this won't work, since I believe is Backbone route detects it is the same route, therefore just returns.
I found this question is related to mine, but there is a difference that I am not using functions, just tag.
(CMD + R) works
So is there any way I can refresh the same route page in tag?
Thanks!
In the view I would do this.
In the view
events:
"click span": "resetPage"
resetPage: ->
#trigger "reset:page"
Then in the controller
#listenTo currentView, "reset:page", (args) ->
model = args.model
App.vent.trigger "reset:page", model
Then in your router you listen for that message and call your API route again, essentially refreshing the page, but if you have something you would need to re-fetch you could just pass that along with each message and avoid having the memory/unresponsive feeling that a navigate({trigger: true}) would cause :).
#SampleApp.module "PostsApp", (PostsApp, App, Backbone, Marionette, $, _) ->
class PostsApp.Router extends Marionette.AppRouter
appRoutes:
"" : "list"
":id" : "show"
API =
list: ->
new PostsApp.List.Controller
show: (id, post) ->
new PostsApp.Show.Controller
id: id
post: post
App.vent.on "posts:list:clicked", ->
App.navigate "/"
API.list()
App.vent.on "reset:page", (post) ->
App.navigate "/" + post.id
API.show post.id, post
App.addInitializer ->
new PostsApp.Router
controller: API
To avoid re-fetching the post you'd do this in the initialize function in your controller:
#SampleApp.module "PostsApp.Show", (Show, App, Backbone, Marionette, $, _) ->
class Show.Controller extends App.Controllers.Application
initialize: (options) ->
{ post, id } = options
post or= App.request "post:entity", id
App.execute "when:fetched", post, =>
#layout = #getLayoutView()
#listenTo #layout, "show", =>
#panelRegion post
#postRegion post
#bannerRegion post
#show #layout
That will keep your app very responsive - note this is assuming you don't need to reset your models/collections. If you need to just reset the model/collection data the navigate({trigger: true}) route might be the way to go. Unless you want to keep track of say items you added and in the event do some manual resetting and then re-render the view.
I suppose you also could implement a "resetModel" function in your model that will re-fetch it and trigger a custom event which your view could listen to as a modelEvents and then re-render itself.

Collection renders fine when loaded from remote Url, but not when loaded from localStorage (initialize vs. onRender)

I'm using a layout's 'initialize' method to instantiate a collection, fetch() data from a url on that collection, and then instantiate a new collectionView passing through the collection with it's fetched data. The collectionView later gets 'shown' in a region.
It works perfectly.
I wanted to switch over to using localStorage to retrieve the data, instead of from a remote url. So I saved the data retrieved from remote url into localStorage. I then updated the collection with the 'localStorage: new Backbone.LocalStorage('entries')'. Now it successfully retrieves the data from localStorage. But. None of the regions in the layout get rendered (i've tried calling this.render() in various places with no luck).
If I replace the 'initialize' method to 'onRender' in the layout which is responsible for instantiating everything, then it renders perfectly (regardless of whether it came from remote url or localStorage).
Why would it be that if i use the layout's 'initialize' method to fetch data in a collection from localStorage, then nothing gets rendered; whereas it does get rendered if that data is fetched from remote Url? why does both work when using the 'onRender' method?
Some code for reference:
var EntriesCollection = Backbone.Collection.extend({
model: EntryModel,
url: '/api/entries',
localStorage: new Backbone.LocalStorage('entries')
});
return EntriesLayout = Marionette.Layout.extend({
...
// Below, if I use onRender everything works, if I change to 'initialize' then
// it works only when data is loaded via remote url, not localStorage. (the collection
// is successfully populated with data in both cases.)
onRender: function() {
...
this.entries_collection = new EntriesCollection();
var self = this;
this.entries_collection.fetch().done(function() {
/*
self.entries_collection.localStorage = new Backbone.LocalStorage('entries');
self.entries_collection.each(function(model) {
model.save();
});
*/
self.entries_list = new EntriesList({collection: self.entries_collection});
self.collectionSynced();
});
},
collectionSynced: function() {
this.columnRight.show(this.voting_intro);
this.collectionList.show(this.entries_list);
this.listenTo(this.voting_intro, 'start:clicked', function() {
this.displayEntry(0);
});
},
...
I'm using Marionette's Layout, CollectionView, ItemView's etc, not sure how critical that is to the question.

Difference between $(this.el) and this.$el in Backbone

I'm developing a single-page web application using Backbone and Laravel. I've set my router to use pushState and configured Laravel to send all other requests to the main view of the backbone application, where backbone takes care of the routing.
My problem/question is as follows:
I have a route called 'dashboard', this route is the main application view and is shown after login. It uses a collection called Clients.
dashboard:function(uri){
dashboardCallback = function(data){
if(data.check){
console.log('generate dashboard');
//get clients collection
clientsCollection = new Dash.Collections.Clients();
clientsCollection.fetch().then(function(clients){
//genenerate dashboard view
new Dash.Views.Dashboard({collection:clientsCollection}).renderDashboard();
});
}
else{
router.navigate('/', {trigger:true, replace:true});
}
}
Dash.Utilities.user.isLoggedIn(dashboardCallback);
},
The Dash.Views.Dashboard view takes care of all the views in the application, when calling the renderDashboard(); method, it starts rendering all client views. This is where it gets interesting.
The code for rendering all the client views is as follows:
renderClients:function(){
console.log('Rendering all clients', this.collection);
clientsView = new Dash.Views.Clients({collection:this.collection}).render();
$(this.el).html(clientsView.el);
}
with the above code, it works in all cases. With that i mean when I log in first and the application routes me to the dashboard view all the clients gets rendered and appended to the DOM, the same thing happens when I access /dashboard immediately (afther the application checks if i'm logged in).
But, when I use the following code it doesn't load the client views when I first log in. It does load the client views when i access /dashboard directly.
renderClients:function(){
console.log('Rendering all clients', this.collection);
clientsView = new Dash.Views.Clients({collection:this.collection}).render();
this.$el.html(clientsView.el);
}
It took me a while to figure out that the fix of the problem was that I had to replace this.$el with $(this.el), but I alway's thought it didn't matter because they are essentially the same, or am I wrong in this assumption?
Can someone explain to me this weird behaviour?
As requested, here is my global Dashboard view
Dash.Views.Dashboard = Backbone.View.extend({
tagName:'div',
id:'main',
className:'dashboard',
initialize: function(){
console.log('Initializing Global Dashboard View');
//make sure the main element is only added once.
if(!$('.dashboard').length){
$('body').append(this.el);
}
else{
this.el = $('.dashboard');
}
},
renderDashboard: function(){
console.log('Render all Dashboard components');
this.renderNavBar();
this.renderClients();
},
renderNavBar: function(){
var navBarView = new Dash.Views.NavBar().render();
$(this.el).before(navBarView.el);
},
renderLogin: function(){
var logInView = new Dash.Views.Login().render();
$(this.el).html(logInView.el);
},
renderWhoops:function(error){
console.log('Render Whoops from Global Dashboard');
var whoopsModel = new Dash.Models.Whoops(error);
$(this.el).html(new Dash.Views.Whoops({model:whoopsModel}).render().el)
},
renderClients:function(){
console.log('Rendering all clients', this.collection);
clientsView = new Dash.Views.Clients({collection:this.collection}).render();
$(this.el).html(clientsView.el);
}
});
I'd guess that your problem is right here:
if(!$('.dashboard').length){
$('body').append(this.el);
}
else{
this.el = $('.dashboard'); // <----- Broken
}
If there is no .dashboard then you directly assign to this.el and that's a mistake as it won't update this.$el. The result is that this.el and this.$el reference different things and nothing works. You should use setElement to change a view's el:
setElement view.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use setElement, which will also create the cached $el reference and move the view's delegated events from the old element to the new one.
So you should be saying this:
if(!$('.dashboard').length){
$('body').append(this.el);
}
else{
this.setElement($('.dashboard')); // <----- Use setElement
}

Setting URL in Backbone.Router for QUnit testing

I have a Backbone.Router based myRouter, that calls certain set() methods based on the URL:
var MyRouter = Backbone.Router.extend({
routes: {
"doSomething/:value": "doSetterOnModel",
},
doSetterOnModel: function(value) {
// does some set(value) calls on my Model
},
});
I would like to test now if with the model gets updated (set()) correctly using a QUnit test:
test( "update Model via URL change", function() {
var value = "newValue";
var url = "http://localhost/#/doSomething/" + value;
//does not work as it moves page away from QUnit's tests.html page
window.location = url;
// does also not work because it moves away from QUnit's tests.html page
myRouter.navigate(url);
deepEqual( myModel.get(key), value, "Value on Model was not updated by URL change");
});
How can I set the URL in Qunit in order to test if my Router performs the right action for a certain URL?
When I want to test my routers I usually bypass the document navigation altogether and simply call Backbone.history.loadUrl, which is responsible for matching URL fragments to a router methods:
Backbone.history.loadUrl("#/doSomething");
The additional benefit here is that you don't need to call Backbone.history.start in your test code. Note also the use or a relative url. Specifying the http://localhost part is not necessary.
Tiny demo here.

Resources