Changes in Page/Dirty Page Backbone/MarioneteJS - backbone.js

I have the following question:
I have an application that I'm writing with Backbone/MarioneteJS, and I have the following issue, maybe is a simple and I'm complicating the issue.
In the application you can edit some fields and then save them, I want to be available that when the user edit some filed and he tries to navigate to another place, the application will block/inform him that he have unsaved changes.
In the past not in Backbone/MarioneteJS, we use a global variable that I checked before leaving to another place, something like(pseudocode like):
var dirtyPage = false;
// when editing we do
if (editing) {
dirtPage = true;
}
// In another part of the code before navigating to another place
if (dirtPage) {
ShowMessage("Unsaved Changes");
} else {
Navigate(AnotherPlace);
}
I know that if I do it like below it will work, but maybe there is a more elegant way to resolve the issue.

Related

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...).

In backbone marionette is there a way to tell if a view is already shown in a region?

Given something like this:
View = Backbone.Marionette.ItemView.extend({ });
myView = new View();
//region already exists
myLayout.region.show(myView)
//some time later this gets called again:
myLayout.region.show(myView)
I can see currentView in the docs but this only seems to apply at initialisation. Once a view is shown can I query the region to see the view? Either the view instance or type would be helpful. Looking in Chrome's debugger I can't see any properties/methods on the region that would help.
The motive for wanting to do this is so I don't show a static item view in a region again if it is already displayed as this can (especially if images are involved) cause a slight flickering effect on the screen.
Thanks
--Justin Wyllie
you can add a condition before calling show method:
if (myLayout.region.currentView != myView)
myLayout.region.show(myView)
so if you'll try to call show with the same View it wont be shown.
if you want to call region.show(myView) once you can check in this way:
if (_.isUndefined(myLayout.region.currentView))
myLayout.region.show(myView)
You can check the isClosed and $el attributes of the view. Something like
if (myView.isClosed || _.isUndefined(myView.$el)) {
myLayout.region.show(myView);
}
This is the same way the region checks to see if the view is closed or not:
show: function(view) {
this.ensureEl();
var isViewClosed = view.isClosed || _.isUndefined(view.$el);
...
I'm going out on a limb here and assuming that the OP's question is based on app behavior when navigating to different parts of the app via an anchor tag in the navigation or something similar.
This is how I found the question and I thought briefly that the answers would save my day. Although both answers so far are correct they do not quite solve the problem I was having. I wanted to display a persistent navigation bar. However, I did not want it to display on the login page. I was hopeful that detecting if a Region was already shown or not I'd be able to properly let the display logic take care of this.
As it turns out we were both on the right track to implement Regions as this provides granular control, but even after implementing the above I found that my nav bar would still "flicker" and essentially completely reload itself.
The answer is actually a bit ridiculous. Somehow in all the Backbone tutorials and research I've been doing the last two weeks I never came across the need to implement a javascript interface to interrupt normal link behavior. Whenever a navigation item was clicked the entire app was reloading. The routing was functioning so the content was correct, but the flicker was maddening.
I added the following to my app.js file right after the Backbone.history.start({pushState: true}); code:
// Holy crap this is SOOO important!
$(document).on("click", "a[href^='/']", function(event) {
if (!event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
event.preventDefault();
var url = $(event.currentTarget).attr("href").replace(/^\//, "");
Backbone.history.navigate(url, { trigger: true });
}
});
Check out this article for some explanation about the keyPress detection stuff. http://dev.tenfarms.com/posts/proper-link-handling
Boom! After adding this stuff in my app no longer completely reloads!
Disclaimer: I am very new to Backbone and the fact that the above was such a revelation for me makes me think that I may be doing something wrong elsewhere and this behavior should already exist in Backbone. If I've made a giant error here please comment and help me correct it.

Best way to save/update a resource with ($watch or save button, etc..)

Currently I run into the following problem,
we are working on a form heavy application and wanted a good user experience so we tried to stop have a save / update button everywhere.
Currently we try to $watch every Form Change, but this won't work correctly since it updates the scope when the model gets updated which causing problems on Decimal / Money Values.
What would you prefer? Still the messy save button or maybe something like Gmail did?
What are good methods to do this without save buttons.
/* EDITED */
Currently we use this method to update our form.
It first copys the scope in an object and checks if it is the same than the object that is set after the date got pulled.
$scope.$watch('task', function(scope) {
console.log($scope.updateForm);
scopeObject = angular.copy(scope);
if(scope !== undefined) {
if(!(_.isEqual(scopeObject, mainObject))){
//scope_copy.request_date = $filter('date')(new Date(scope.request_date), 'fullDate');
console.log('update');
scope.$update({pk: $routeParams.taskId}, function() {
scope.request_date = $filter('date')(scope.request_date);
mainObject = angular.copy(scope);
});
mainObject = angular.copy(scope);
}
}
}, true);
currently i think this code is somehow messy since it can't update decimal numbers.
but currently i don't have a better solution. (i don't want to use a Button to submit the form, it should be done interactivly).

Angular: Push, then delete an item

Use case: A user creates a new task, which has to be sent upstream through an API. When that API returns success, I add it to the scope for display. I have that working fine with:
$http.post('some_url', newtask).success(function(data) {
$scope.tasks.push(data);
});
(newtask is a simple object defined earlier, not shown here).
The problem is, the API is quite slow (though reliable), whereas I want this to feel like a real-time app. So I'd like to push the new task to the $scope immediately, then replace it with the "real" one when the API returns success. So I prepend the above with:
$scope.tasks.push(newtask); // Add to UI immediately (provisionally)
What happens now is that the new task is added immediately, then a second copy of the task is added when the API returns. So what I'd like to do is remove the first copy as soon as the second copy is added.
I can't seem to find a find a way to do that. Or is my approach all wrong? (I admit, the approach does feel like a bit of a hack, so I'm open to suggestions).
In the success callback function, I suggest you compare the new task (coming back from the API) to the last task in your tasks array. If they match, you don't have to do anything. (If they don't match, you'll need to remove the last item and throw up some kind of error indication.)
$http.post('some_url', newtask).success(function(data) {
if($scope.tasks[$scope.tasks.length-1] === data) {
// do nothing, we already added it before
} else {
... error handling here
}
});
If the user can't add more than one task at a time (before the API returns), and you really want to remove then add, just use normal JavaScript operations on the array to remove the last item, then add the API value:
$http.post('some_url', newtask).success(function(data) {
$scope.tasks.pop();
$scope.tasks.push(data);
});
Angular should notice the model change and automatically update the view for you.
//Push the new task immediately
$scope.tasks.push(newtask);
//When API return check for error if error just pop out the added task
$http.post('some_url', newtask).success(function(data) {
if($scope.tasks[$scope.tasks.length-1] !== data) {
$scope.tasks.pop();
}
});

How to stop Silverlight browser history recording NavigationState/bookmark changes?

We have a Silverlight Prism project with complex state information stored in the browser bookmark. This allows us to share bookmarks/links which will restore the application to the exact same visual state.
We do not however want trivial bookmark changes (i.e. non-navigation changes) to result in a browser history entry. Otherwise the browser back/forward buttons would also make simple changes (things like simple list selections, tab selections etc).
Q: Is there a way to still change the browser bookmark URL, but exclude it from the browser history, or (failing that) is it possible to remove entries from the browser history?
Our visual states are prioritised, so we know which ones actually should affect navigation and which are just for decoration. This can be determined either before the URL is changed or after, so your answer can use either situation. We can add a specific marker to the bookmark, indicating it should not be archived, if that would also help your solution.
Thanks
I created one answer myself, but would prefer not to rely on JavaScript features. I also need to check that window.location.replace is supported by all browsers.
I now call this method with a flag, to say if I want the bookmarking ignored in the browser history:
public void NavigateToBookmark(string bookMarkUrl, bool replaceUrl)
{
if (replaceUrl)
{
HtmlPage.Window.Invoke("NavReplace", bookMarkUrl);
}
else
{
HtmlPage.Window.NavigateToBookmark(bookMarkUrl);
}
}
then add this JavaScript in the Silverlight hosting page:
function NavReplace(url) {
var newurl = parent.location.href;
var index = newurl.indexOf("#");
if (index > 0) {
newurl = newurl.substr(0, newurl.indexOf("#"));
}
window.location.replace(newurl + "#" + url);
}
Any takers for a better way to do this?

Resources