calling Backbone.history.navigate(...) leads to recursion? - backbone.js

I am using bootstrap, backbone, marionette in my app with require js support. This is a moderately large application with many views and sub views. As it consists of tabbed display, I am using bootstrap tabs.
In my layout view, I am handling the tab shown event and trying to do tab pane specific rendering as below...
var AppLayoutView = Backbone.Marionette.LayoutView.extend({
el : "body",
regions: {
headerRegion: "#ecp_header",
bodyRegion: "#ecp_body",
contentRegion: "#home"
},
events: {
'shown.bs.tab ul.nav-tabs a': 'onTabShown'
},
...
onTabShown: function(e) {
var self = this;
console.log("App layout view: 'onTabShown' executing...");
var tabId = $(e.currentTarget).attr('id');
if (tabId === 'home-tab') {
/** Show the dashboard (home) view */
require(['user_dashboard_layout'],
function(UserDashboardLayoutView) {
// update the URL in addressbar, so it can be available in history stack
Backbone.history.navigate('dashboard');
var dbLytView = new UserDashboardLayoutView();
dbLytView.render();
//self.bodyRegion.show(dbLytView);
//self.contentRegion.attachView(dbLytView);
});
} else if (tabId == "scheduling-tab") { ... }
All of this was working decently, until I added the line the history navigation line as shown in the above code.
Backbone.history.navigate('dashboard');
I also added app_controller to take care of routing. But after this, I observe a strange behavior. I see that the above fn "onTabShown" gets invoked multiple times, as seen in the browser console window (screenshot attached), whenever I perform login/logout on my app.
BTW, in my SPA (single page app), when user logs in, I show dashboard (if the user is logged in), or show welcome page (if not logged in).
If the offending line (history.navigate(...)) is present, I can see that tabShown is invoked multiple times, that is, for each login/logout it gets accumulated (some sort of strange recursion, or stack is not unwound).
But, if I comment out the history.navigate line, it doesn't perform page refresh after logout.
My basic question is...
"does the backbone.history.navigate(...) fn play any role in actual page navigation/refresh, apart from just updating the history stack?
From the documentation, it appeared that we need to call bb.h.navigate(...) just to keep the url in addressbar in sync with our current state of app. However, I am experiencing this strange behavior?
Since the app is a bit fairly complex, I may not have provided all relevant details for soliciting a proper answer to this qn.
May someone point me in right direction...?

Related

How to reload the ionic view?

I have created a sidemenu based app, in that after login I am displaying a number of tasks. If I click on the task it will redirect to the task details page, in that page I can update the tasks.
So after updating a task I need to go back to the previous task list page. I am using $ionicHistory.goBack(); to go back.
My problem is after come back, I need to refresh the task list i.e. updated task should not be there in the task list. How can I refresh/reload the task list?
If you bind your task to a tasks array, which will be used in the task list page, it should be automatically updated.
But the question is about not displaying, newly added tasks (still my previous suggestion should work) if not, performance reasons ionic views are cached, So when you come back to the previous view it doesn't go through the normal loading cycle. But you 2 options
1 - disable the caching by using <ion-view cache-view="false" view-title="My Title!"> in your ion-view, but this is not a very elegant solution. read more
2 - use ionRefresher (my preferred). read more here
https://github.com/angular-ui/ui-router/issues/582
according to #hpawe01 "If you are using the current ionicframework (ionic: v1.0.0-beta.14, angularjs: v1.3.6, angular-ui-router: v0.2.13), the problem with the not-reloading-controller could be caused by the new caching-system of ionic:
Note that because we are caching these views, we aren’t destroying scopes. Instead, scopes are being disconnected from the watch cycle. Because scopes are not being destroyed and recreated,controllers are not loading again on a subsequent viewing.
There are several ways to disable caching. To disable it only for a single state, just add cache: false to the state definition.
This fixed the problem for me (after hours of reading, trying, frustration).
For all others not using ionicframework and still facing this problem: good luck!"
Hope this helps.
You can also listen to ionic events such as $ionicView.enter on $scope and trigger the code that refreshes the list if you haven't bound your list as #sameera207 suggested.
EG:
// List.controller.js
angular.module('app')
.controller('ListController', ['$scope', function($scope) {
// See http://ionicframework.com/docs/api/directive/ionView/ for full events list
$scope.$on('$ionicView.enter', function() {
_someCodeThatFetchesTasks()
.then(function(tasks) {
$scope.tasks = tasks;
});
});
});
Bear in mind that it's not the most proper way (if proper at all) and if you do this you certainly have a design flaw. Instead you should share the same data array via a factory or a service for example.
For your task you can also use ion-nav-view.
It is well documented. And if you are using now Ionic 2 beta you can use some of the view lifecyle hooks like onPageWillLeave() or onPageWillEnter(). I just faced the same problem and defined a refresh() function, but the user had to click on a button to actually update the view. But then I found:
https://webcake.co/page-lifecycle-hooks-in-ionic-2/
You just have to import the Page and NavController module and also define it in the constructor. The you can use for example onPageWillEnter(), which will always invoke when you go again to a view:
onPageWillEnter() {
// Do whatever you want here the following code is just to show you an example. I needed it to refresh the sqlite database
this.storage.query("SELECT * FROM archivedInserates").then((data) = > {
this.archivedInserates =[];
if (data.res.rows.length > 0) {
for (var i = 0; i < data.res.rows.length; i++) {
this.archivedInserates.push({userName:data.res.rows.item(i).userName, email:
data.res.rows.item(i).email});
}
}
},(error) =>{
console.log("ERROR -> " + JSON.stringify(error.err));
});
}
With ionic beta 8 the lifecylcle events changed their names. Check out the official ionic blog for the full list of the lifecycle events.
if you are building data driven app then make sure use $ionicConfigProvider.views.maxCache(0);in your app.config so that each review can refresh for more details read this http://ionicframework.com/docs/api/provider/$ionicConfigProvider/

Angular UI Router - Parentless Child States

I think that my objective won't work with AngularUI Router - but I'm going to put it out here in case someone can prove me wrong or has an alternative solution or a workaround that solves the same problem.
The Objective
I want to display modal windows which change the url but can be opened from anywhere in my application - regardless as to which parent state is currently active. Specifically, I want the url changed so that, when the browser/device back button is pushed, the modal is closed (i.e. the app will return to whichever parent state they were using). Such a modal could be opened by the user at any time while using the app (an example would be a help window accessible from the app's main menu bar).
What I really don't want to do is copy and paste the modal state as a child of every possible parent state (i.e. register the help state as a child for each of user profile/search results/home/etc...). If there were just one such modal in the app then doing so may be an acceptable approach - but when you start introducing several globally accessible modal child states into an app then multiple child state registration starts to become a real problem.
To illustrate more clearly, here's a user story:
The user is viewing some search results (they've infinitely scrolled through several pages worth of results).
There is an action they want to perform but they're not sure how to achieve it so they click the help icon in the app's header.
A modal dialog opens which is layered above the search results they were viewing.
They search through the help and figure out what they need to do.
They press the device's back button.
The modal dialog closes, revealing the previous state they had been viewing without any loss of context.
The user performs their task and is extremely happy with themselves - and not pissed off at the app developers due to a stupid user experience design.
In the above story, the only way I can think to cause the back event to close the modal is to tie the modal to AngularUI Router's state transitions. The user would go from the search results state (url: /search-results) to the help state (url: /search-results?help) - however, in another case they may go from a user profile state (url: /profile/123) to the help state (url: /profile/123?help). The key here being, that help wasn't registered directly as a child of both the search results and profile states, but somehow independently as a type of orphaned state which could be potentially applied to any parent.
Alternative Objective
This is not my preferred solution. If it's possible to cause the browser/device back button to close a modal without changing the url then I can make these modals work independently of AngularUI Router - but I don't like this as an approach, it means having an inconsistent development approach for different types of views (and who knows, maybe in the future we'll decide that one of these modal windows should be a first-class state in its own right and this would require a change from one approach to the other - which is undesirable). I also think this is an unreliable approach as handling the back event is no trivial matter, in my experience.
This actually would be useful for many situations (for example, a user could click back to close a sub-menu or context-menu), I just don't think it's a technically viable solution - but feel free to prove me wrong. ;-)
Notes
I am aware that it is possible to open modal child states - in-fact, I've implemented this where child states are explicitly tied to a specific parent state.
This is for an app which specifically targets mobile as its main use-case. This means the back button is a fundamentally important consideration - it's normal behaviour for a mobile user to use the back button to close or cancel a dialog and I categorically do not want to have to train my app's users to click close when they're already used to using the back button.
Sorry, I have no code attempts to present - I have no idea how to get this to work or even where to start - and none of my research has shed any light on the problem (maybe I'm searching with the wrong terms?).
Thanks in advance for any assistance provided!
Edit
1. Updated the user story explanation to include concrete url/state examples for greater clarity.
Well, for anyone who has a similar need, I found a simple solution which basically goes outside of the whole routing mechanism of UI Router.
Firstly, I believe it should be possible to use the deferIntercept feature in the upcoming 0.3 release, as detailed in this SO answer. However, my solution takes a different approach. Instead of using a query parameter to identify these orphaned views (i.e. ?help), I'm using url fragment identifiers (i.e. #help). This works because the routing mechanism seems to ignore anything after the hash symbol.
I did come across a couple of gotchas before I managed to get this fully working - specifically, when dealing with non-html5 mode in the $location service. As I understand it, it's technically illegal to include a hash symbol in a fragment identifier (i.e. a url cannot contain two # symbols), so it comes with some risk, but from my testing it seems that browsers don't complain too much).
My solution involves having a hashRouter service which manages the jobs of serialising and deserialising your query data to and from the fragment identifier, and monitoring $locationChangeSuccess events to hand external changes in the url (i.e. when the browser or device's back and forward buttons are pressed).
Here's a simplified version of my service:
hashRouter.$inject = [
'$rootScope',
'$location'
];
function hashRouter($rootScope, $location) {
var service = this,
hashString = $location.hash(),
hash = fromHashString(hashString);
$rootScope.$on('$locationChangeSuccess', function (e, newUrl) {
var newHashString = getHashSection(newUrl);
if (newHashString != hashString) {
var newHash = fromHashString(newHashString);
service.hash(newHash.name, newHash.params);
}
});
service.hash = function (name, params) {
var oldHash = hash,
oldHashString = hashString;
hash = { name: name || '', params: params || {} };
hashString = toHashString(hash);
if (hashString !== oldHashString) {
var oldHashExists = oldHashString.length > 0;
if (oldHashExists) {
$rootScope.$broadcast('hashRouteRemoved', oldHash);
}
if (hashString.length > 0) {
$rootScope.$broadcast('hashRouteAdded', hash);
}
$location.hash(hashString);
if (oldHashExists) {
$location.replace();
}
}
};
return service;
function toHashString(data) {
var newHashString = '';
var name = data.name;
if (!!name) {
newHashString += encodeURIComponent(name);
}
var params = data.params;
if (!!params) {
var paramList = [];
for (var prop in params) {
var key = encodeURIComponent(prop),
value = params.hasOwnProperty(prop) ? encodeURIComponent(params[prop].toString()) : '';
paramList.push(key + '=' + value);
}
if (paramList.length > 0) {
newHashString += ':' + paramList.join('&');
}
}
return newHashString;
}
function fromHashString(urlHash) {
var parsedHash = {
name: '',
params: {}
};
if (!!urlHash && urlHash.length > 0) {
if (urlHash.indexOf(':') !== -1) {
var hashSegments = urlHash.split(':');
parsedHash.name = decodeURIComponent(hashSegments[0]);
var querySegments = hashSegments[1].split('&');
for (var i = 0; i < querySegments.length; i++) {
var pair = querySegments[i].split('=');
parsedHash.params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]) || null;
}
} else {
parsedHash.name = decodeURIComponent(urlHash);
}
}
return parsedHash;
}
function getHashSection(url) {
if (url.indexOf('#') === -1 || (url.indexOf('#!') !== -1 && url.indexOf('#') === url.lastIndexOf('#'))) {
return '';
}
var urlSegments = url.split('#');
return urlSegments[urlSegments.length - 1];
}
}
angular.module('myApp').service('hashRouter', hashRouter);
There are a couple of things to note about the service:
I rolled-my-own serialisation/deserialisation functions and they're anything but complete, so use at your own risk - or replace with something more suitable.
This depends upon using the bang part of the hash-bang (#! as opposed to #) when not in html5 mode.
If you do mess around with the serialisation/deserialisation functionality, be very careful: I found my self in a few infinite-loop scenarios which basically crashed my browser. So make sure you test thoroughly!
You still need to invoke the service whenever you open/close a dialog/menu/etc which uses the service and listen to the hashRouteAdded and hashRouteRemoved events as appropriate.
I've built this system to support only one view at a time - if you need multiple views then you'll need to customise the code somewhat (although I guess it could support nested views easily enough).
Hopefully, if anyone else needs to do the same as I've done here this can save them some time :-)
Could you use a single state as the parent for everything in the app? I do the same thing in my angular app.
$stateProvider
//root route
.state('app', {
url: '/',
templateUrl: '/scripts/app/app/views/app.html',
controller: 'appController',
resolve: {
//resolve any app wide data here
}
});
Then you can do your modal as a child of this state. That way you can always transition back to this route to get back to your app's default state (when your modal closes).
Another benefit of doing things this way is you can use the view for this route as a layout to put any markup that doesn't change from page to page (header, sidebar, etc...).

Backbone.js change url without reloading the page

I have a site that has a user page. On that page, there are several links that let you explore the user's profile. I'd like to make it so that, when one of those links is clicked on, the url changes, but the top third of the page containing the user's banner doesn't reload.
I'm using Backbone.js
I have a feeling that I'm in one of those situation where I have such a poor understanding of the problem I'm dealing with that I'm asking the wrong question, so please let me know if that appears to be the case
My mistake was assuming that there was a special, built-in way of doing this in backbone. There isn't.
Simply running the following line of code
window.history.pushState('object or string', 'Title', '/new-url');
will cause your browser's URL to change without reloading the page. You can open up the javascript console in your browser right now and try it with this page. This article explains how it works in more detail (as noted in this SO post).
Now I've just bound the following event to the document object (I'm running a single page site):
bindEvents: () ->
$(document).on('click', 'a', #pushstateClick)
pushstateClick: (e) ->
href = e.target.href || $(e.target).parents('a')[0].href
if MyApp.isOutsideLink(href) == false
if e.metaKey
#don't do anything if the user is holding down ctrl or cmd;
#let the link open up in a new tab
else
e.preventDefault()
window.history.pushState('', '', href);
Backbone.history.checkUrl()
See this post for more info.
Note that you CAN pass the option pushstate: true to your call to Backbone.history.start(), but this merely makes it so that navigating directly to a certain page (e.g. example.com/exampleuser/followers) will trigger a backbone route rather than simply leading to nowhere.
Routers are your friend in this situation. Basically, create a router that has several different routes. Your routes will call different views. These views will just affect the portions of the page that you define. I'm not sure if this video will help, but it may give you some idea of how routers interact with the page: http://www.youtube.com/watch?v=T4iPnh-qago
Here's a rudimentary example:
myapp.Router = Backbone.Router.extend({
routes: {
'link1': 'dosomething1',
'link2': 'dosomething2',
'link3': 'dosomething3'
},
dosomething1: function() {
new myapp.MyView();
},
dosomething2: function() {
new myapp.MyView2();
},
dosomething3: function() {
new myapp.MyView3();
}
});
Then your url will look like this: www.mydomain.com/#link1.
Also, because <a href=''></a> tags will automatically call a page refresh, make sure you are calling .preventDefault(); on them if you don't want the page to refresh.

Whats the Advantage of Marionette AppRouter+Controller over Backbone.Router?

From my understanding, the differences is the callback functions to events on an AppRouter should exist in the Controller, instead of the same Router object. Also there is a one-to-one relationship between such AppRouter & Controllers, all my code from Router now moves to Controller, I don't see too much point of that? So why use them? I must be missing something?
The way I see it is to separate concerns:
the controller actually does the work (assembling the data, instanciating the view, displaying them in regions, etc.), and can update the URL to reflect the application's state (e.g. displayed content)
the router simply triggers the controller action based on the URL that has been entered in the address bar
So basically, if you're on your app's starting page, it should work fine without needing any routers: your actions (e.g. clicking on a menu entry) simply fire the various controller actions.
Then, you add on a router saying "if this URL is called, execute this controller action". And within your controller you update the displayed URL with navigate("my_url_goes_here"). Notice you do NOT pass trigger: true.
For more info, check out Derick's blog post http://lostechies.com/derickbailey/2011/08/28/dont-execute-a-backbone-js-route-handler-from-your-code/ (paragraph "The “AHA!” Moment Regarding Router.Navigate’s Second Argument")
I've also covered the topic in more length in the free preview of my book on Marionette. See pages 32-46 here: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf
I made some override for the router. And currently use it in this way (like Chaplin):
https://gist.github.com/vermilion1/5525972
appRoutes : {
// route : controller#method
'search' : 'search#search'
'*any' : 'common#notFound'
},
initialize : function () {
this.common = new Common();
this.search = new Search();
}

Backbone Router For Modals

I have a very basic Backbone JS application that has modals. Currently, my router presents the modals as follows:
class App.Routers.Router extends Backbone.Router
routes:
"modal" : "modal"
modal: ->
view = new App.Views.Modal.New()
$('#shared').html(view.el)
view.render()
view.show()
return
class App.Views.Sessions.New extends Backbone.View
template: Handlebars.templates["backbone/templates/modals"]
initialize: (options) ->
super(options)
render: ->
$(#el).html(#template())
$('.modal', #el).modal()
$('.modal', #el).on 'hidden', #cleanup
return #
show: ->
$('.modal', #el).modal('show')
hide: ->
$('.modal', #el).modal('hide')
cleanup: ->
# ?
This works fine, however I am unclear of how to handle the window history and a user selecting the back button (i.e. how to I teardown the modal on clicking back). Does anyone have any ideas on the best approach? Thanks.
You've stumbled on an interesting problem with single page apps (SPAs). Yes it can get a bit tricky but simple software engineering principles/designs can help here. I've dealt with clean up in the following way:
Have a separate class/object responsible for managing view transitions between various 'parts of the application'. For example in my app I have something like this:
var App = {};
//when showing a specific app:
App.showView = function(appToShow){
if(this.currentApp)
currentApp.close();
this.currentApp = appToShow;
//render appToShow;
}
appToShow is a 'class' that has methods create/render and close. So that the app is responsible for it's clean-up.
Now sometimes I have modal or "sub apps" within the main application. I've used a variant of the above and added a close method to the App object. But the basic idea is to add these 'sub apps' as a property of the main app:
//when creating modal:
App.addModal = function(modal){
this.currentApp.modal = modal;
}
Now when transitioning by clicking the back button or to a different part of the app - you must call upon the App-manager to handle the clean-ups and transitions. It's basically like saying:
App.closeModal = function(modal){
if(this.currentApp.modal)
this.currentApp.modal.close();
}
Depending on how your routers are organized, you should be able to decide whether to close the entire app altogether or just the modal/sub-apps. Basically your "App Manager" is a separate object responsible for only handling transitions between the views and need not be a Backbone.View - a separate object would work just fine. You could even have it listen for events using Backbone's events by creating and event aggregator. Derick Bailey has written an excellent blog post detailing the same: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/

Resources