Cancel route using Sammy.js without affecting history - javascript

I want to intercept all route changes with Sammy to first check if there is a pending action. I have done this using the sammy.before API and I return false to cancel the route. This keeps the user on the 'page' but it still changes the hash in the browsers address bar and adds the route to the browsers' history. If I cancel the route, I dont want it in the address bar nor history, but instead I expect the address to stay the same.
Currently, to get around this I can either call window.history.back (yuk) to go back to the original spot in the history or sammy.redirect. Both of which are less than ideal.
Is there a way to make sammy truly cancel the route so it stays on the current route/page, leaves the address bar as is, and does not add to the history?
If not, is there another routing library that will do this?
sammy.before(/.*/, function () {
// Can cancel the route if this returns false
var response = routeMediator.canLeave();
if (!isRedirecting && !response.val) {
isRedirecting = true;
// Keep hash url the same in address bar
window.history.back();
//this.redirect('#/SpecificPreviousPage');
}
else {
isRedirecting = false;
}
return response.val;
});

In case someone else hits this, here is where I ended up. I decided to use the context.setLocation feature of sammy to handle resetting the route.
sammy.before(/.*/, function () {
// Can cancel the route if this returns false
var
context = this,
response = routeMediator.canLeave();
if (!isRedirecting && !response.val) {
isRedirecting = true;
toastr.warning(response.message); // toastr displays the message
// Keep hash url the same in address bar
context.app.setLocation(currentHash);
}
else {
isRedirecting = false;
currentHash = context.app.getLocation();
}
return response.val;
});

When using the code provided within the question and answer you have to notice that the route you cancelled will also be blocked for all future calls, routeMediator.canLeave will not be evaluated again. Calling a route twice and cancelling it depending on current state is not possible with this.

I could produce the same results as John Papa did when he used SammyJS on the SPA/Knockout course.
I used Crossroads JS as the router, which relies on Hasher JS to listen to URL changes "emitted" by the browser.
Code sample is:
hasher.changed.add(function(hash, oldHash) {
if (pageViewModel.isDirty()){
console.log('trying to leave from ' + oldHash + ' to ' + hash);
hasher.changed.active = false;
hasher.setHash(oldHash);
hasher.changed.active = true;
alert('cannot leave. dirty.');
}
else {
crossroads.parse(hash);
console.log('hash changed from ' + oldHash + ' to ' + hash);
}
});

After revisiting an older project and having a similar situation, I wanted to share another approach, just in case someone else is directed here.
What was needed was essentially a modern "auth guard" pattern for intercepting pages and redirecting based on credentials.
What worked well was using Sammy.around(callback) as defined here:
Sammy.js docs: Sammy.Application around(callback)
Then, simply do the following...
(function ($) {
var app = Sammy("body");
app.around(checkLoggedIn);
function canAccess(hash) {
/* access logic goes here */
return true;
}
// Authentication Guard
function authGuard(callback) {
var context = this;
var currentHash = app.getLocation();
if (!canAccess(currentHash)) {
// redirect
context.redirect("#/login");
}
else {
// execute the route path
callback();
}
};
})(jQuery);

Related

Can't change Vue Instance data values

I'm building a custon social login page for my web application, and I'm stuck with a bug I can't find why it's hapenning .
Basically, I want to call a function called "connectFb" and then if all the Facebook API calls are successful, I would like to change a bunch of data in my vue instance in order to render other elements . (those are rendred conditionally via v-if)
Here's the part of my code responsible for this :
app = new Vue({
el : "#social-auth",
data: {
showTwitter : false,
showFb: true,
showPages: false,
fb_state: "unconnected",
continue_auth: false,
pages_fb: []
},
methods : {
connectFb: function() {
FB.login(function(response) {
if (response.authResponse) {
alert('You are logged in & cookie set!');
fb_token = response.authResponse.accessToken
FB.api('/me/accounts','get',{access_token: fb_token},function(pages){
if(pages["error"] !== undefined){
console.log('EROR')
}
else{
console.log("Got a list of pages");
console.log(pages);
this.pages_fb = pages.data;
this.showFb = false;
this.showPages = true;
this.continue_auth = true;
}
})
} else {
alert('User cancelled login or did not fully authorize.');
}
},{scope: 'public_profile,manage_pages'});
return false;
}
How The Code Works :
Basically, after the user is logged in to fb, it will get a list of his pages, this is not the problem, the problem is in the success callback after it (the callback related to the function fetching pages) . using the debugger I could see that the variable pages contains all the data I need and pages.data return an array of those pages info .
After this I'm trying to attribute it to my instance variable called pages_fb . when this code run pages_fb is always empty even though pages.data is not .
The problem is not only with pages_fb but also with all my instance variable that should change in the callback they are the same after the callback run .
I'm getting mad at this problem, so please help me understand what's wrong .
Extremely common mistake. this defined in your FB.login callback is not the Vue. Use an arrow function, closure, or bind to make it correct.
FB.api('/me/accounts','get',{access_token: fb_token}, pages => {
...
})
See How to access the correct this inside a callback?
When you use this. in a callback it isn't pointing to your Vue instance anymore. You can user => functions to bind this the way you want. Try this:
FB.api('/me/accounts','get',{access_token: fb_token},(pages) => {
if(pages["error"] !== undefined){
console.log('EROR')
}
else{
console.log("Got a list of pages");
console.log(pages);
this.pages_fb = pages.data;
this.showFb = false;
this.showPages = true;
this.continue_auth = true;
}
})

AngularJS - autocomplete with $http requests

I have an application in AngularJS and I want to implement an autocomplete input for a certain list of programs.
My problem is that I have lots of programs in my database and I don't want to load them all when the page loads. Instead I load pages and have a button that loads the next page when clicked.
scope.loadPrograms = function() {
Programs.getPage($scope.page)
.success(function(data) {
$scope.allprograms.push.apply($scope.allprograms, data.campaigns);
$scope.page++;
if(data.pagination.pages < $scope.page) {
$scope.page = -1;
}
})
.error(function(data){
alert('There has been an error. Please try again later!');
});
}
and the button
<md-button ng-click="loadPrograms()" ng-show="page != -1">Load more data</md-button>
So this approach makes me do a request everytime I write/delete a letter in the autocomplete input, given the fact that I don't have all the program loaded on $scope. Is it ok to make so many request? Is there another approach?
Thanks.
EDIT
Ok so now I put a delay on the autocomplete, but the method doesn't work anymore.
// Search for programs
scope.querySearch = function(query) {
if (typeof pauseMonitor !== 'undefined') {
$timeout.cancel(pauseMonitor);
}
pauseMonitor = $timeout(function() {
var results = query ? scope.allprograms.filter(createFilterFor(query)) : [];
return results;
}, 250);
};
// Create filter function for a query string
function createFilterFor(query) {
var lowercaseQuery = angular.lowercase(query);
return function filterFn(programs) {
return (programs.name.toLowerCase().indexOf(lowercaseQuery) != -1);
};
};
It enters in the createFilterFor method, finds a good match but doesn't show it anymore.
If you need to retrieve a set of words for the purpose of auto completion from a large database, one simple trick is to use $timeout with some time threshold which can detect the pauses of the user typing.
The idea is to prevent a request being generated for every key. You look for a pause in the user typing pattern and make your request there for the letters typed. This is a simple implementation of this idea in your key handler.
function processInput(input) {
if (typeof pauseMonitor !== 'undefined') {
$timeout.cancel(pauseMonitor);
pauseMonitor = $timeout(function() {
//make your request here
}, 250);
}
Take a look at ng-model-options
you can set a debounce time and some other interesting things.
ng-model-options="{ debounce: '1000' }"
Above line means the input value will be updated in the model after 1 sec

Windows 8 Javascript app activation/launch deferral

So currently in a windows 8 WinJS app I'm coding, I am trying to get the loading of an xml file to take place in the app startup sequence, while the splash screen is still showing, as this xmldoc element is needed for when the home page loads, and loading of the home page will fail without it.
This is my initiation sequence in default.js:
(function () {
"use strict";
var activation = Windows.ApplicationModel.Activation;
var app = WinJS.Application;
var nav = WinJS.Navigation;
var sched = WinJS.Utilities.Scheduler;
var ui = WinJS.UI;
app.addEventListener("activated", function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
// TODO: This application has been newly launched. Initialize
// your application here.
console.log("Newly Launched!");
var localSettings = Windows.Storage.ApplicationData.current.localSettings;
WinJS.Namespace.define("MyGlobals", { localSettings: localSettings });
// APP RUN TYPE CHECK AND SEQUENCE (FIRST RUN / NOT FIRST RUN):
if (MyGlobals.localSettings.values['firstRunCompleted']) {
console.log("NOT FIRST RUN!");
// CACHE VERSION CHECK. IF APP HAS BEEN UPDATED, INITIATE NEWLY ADDED CACHE VALUES HERE:
} else {
console.log("FIRST RUN!")
MyGlobals.localSettings.values['firstRunCompleted'] = true;
};
//loadXML(); have tried many things with this. doesn't work.
} else {
// TODO: This application has been reactivated from suspension.
// Restore application state here.
var currentVolume = app.sessionState.currentVolume;
if (currentVolume) {
console.log("RESTORE FROM SUSPENSION");
console.log(currentVolume);
};
}
nav.history = app.sessionState.history || {};
nav.history.current.initialPlaceholder = true;
// Optimize the load of the application and while the splash screen is shown, execute high priority scheduled work.
ui.disableAnimations();
var p = ui.processAll().then(function () {
return nav.navigate(nav.location || Application.navigator.home, nav.state);
}).then(function () {
return sched.requestDrain(sched.Priority.aboveNormal + 1);
}).then(function () {
ui.enableAnimations();
});
args.setPromise(p);
args.setPromise(WinJS.UI.processAll().then(function completed() {
loadSavedColour();
// Populate Settings pane and tie commands to Settings flyouts.
WinJS.Application.onsettings = function (e) {
e.detail.applicationcommands = {
"helpDiv": { href: "html/Help.html", title: WinJS.Resources.getString("settings_help").value },
"aboutDiv": { href: "html/About.html", title: WinJS.Resources.getString("settings_about").value },
"settingsDiv": { href: "html/Settings.html", title: WinJS.Resources.getString("settings_settings").value },
};
WinJS.UI.SettingsFlyout.populateSettings(e);
}
As you can see where I have the commented line of "loadXML()", that is where I need the loadXML() function to take place.
Here is my loadXML() function:
function loadXML() {
Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync("foldername").then(function (externalDtdFolder) {
externalDtdFolder.getFileAsync(MyGlobals.localSettings.values['currentBook']).done(function (file) {
Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file).then(function (doc) {
WinJS.Namespace.define("MyGlobals", {
xmlDoc: doc,
});
})
})
});
};
(loadXML is a working function and works in other scenarios)
However, the issue is that before the loadXML function finishes, the app splash screen goes away, and the next home.html home page loads, which starts the accompanying home.js, which has a function that requires the MyGlobals.xmlDoc object that loadXML should have made. This immediately crashes the app, as MyGlobals.xmlDoc is undefined/null.
I used to have this app working by running loadXML in home.js for the home.html page directly, but in that scenario the XML document is reloaded every time navigation is made to the page, wasting time and resources. As such, I'm trying to move the xmldocument loading into the app startup/initialization.
Thanks a lot!
loadXML has async functionality and you need to handle that.
You shouldn't expect that the loadFromFileAsync (or any of the other async functions) have completed before it returns to the caller. If your code doesn't wait, you'll find that the MyGlobals.xmlDoc value won't be set when you need it.
I've renamed it below to be more accurate as to its behavior. The big change is that it returns a Promise that can be used by the caller to properly wait for the Xml doc to be loaded. This Promise could be used with other Promises to wait on multiple conditions if you'd like (or in the case, other async work).
function loadXMLAsync() {
return new WinJS.Promise(function (complete, error, progress) {
var localSettings = MyGlobals.localSettings.values;
var installedLocation = Windows.ApplicationModel.Package.current.installedLocation;
installedLocation.getFolderAsync("foldername").then(function (externalDtdFolder) {
externalDtdFolder.getFileAsync(values['currentBook']).done(function (file) {
Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file).then(function (doc) {
complete(doc);
});
});
});
});
};
Then, in use:
loadXmlAsync().then(function(doc) {
WinJS.Namespace.define("MyGlobals", {
xmlDoc: doc,
});
// and any other code that should wait until this has completed
});
The code above does not handle errors.
I believe what you need to do is to extend the splash screen to give your app more time to initialize UI. For your scenario, it's loading the xml.
I will suggest you read How to extend the splash screen (HTML). The main idea is to display an extended splash screen(you can use the same image as the default one) in the activated event, then call your loadXML.
In addition to other comments, what you really need to do is include the promise from loadXml with what you pass to args.setPromise. As you know, setPromise is a way to tell the app loader to wait until that promise is fulfilled before removing the splash screen. However, in your code you're calling setPromise multiple times. What you should be doing is joining all the promises you care about (animations, loadXml, and setting loading) with WinJS.Promise.join, so that you get a single promise that's then waiting on all the other three, and when that one is fulfilled, then remove the splash screen.
Alan's suggestion for an extended splash screen is helpful if that whole loading process ends up taking too long, as doing an extended splash gives you total control over what's happening and when the transition happens to your main page.

Fast way to redirect tab when new page is loading

I'm trying to redirect a tab to a new page when the URL matches my pattern before it's done loading. The method I came up with does the redirection after a good part of the page is done loading yet.
var tabs = require("sdk/tabs");
var tab_utils = require("sdk/tabs/utils");
function logShow(tab) {
console.log(tab.url + " is loaded; " + pattern.test(tab.url));
if (pattern.test(tab.url)) {
var lowLevelTab = viewFor(tab);
console.log(tab_utils.setTabURL (lowLevelTab, newURL(tab.url)));
// also simply replacing this bit with
// tab.url = "foo" doesn't speed things up
}
}
tabs.on('load', logShow);
Is there a good way of calling setTabURL (...) earlier?
I finally found the best way to do it:
function listener(event) {
var channel = event.subject.QueryInterface(Ci.nsIHttpChannel);
var url = event.subject.URI.spec;
// Here you should evaluate the url and decide if make a redirect or not.
if (pattern.test(url)) {
// If you want to redirect to another url,
// you have to abort current request, see: [1] (links below)
channel.cancel(Cr.NS_BINDING_ABORTED);
// Set the current gbrowser object (since
// the user may have several windows/tabs)
var goodies = loadContextGoodies(channel);
var domWin = goodies.aDOMWindow; // method suggested by
var gBrowser = goodies.gBrowser; // Noitidart [2] (links below)
var browser = goodies.browser; // for reference see comment below
var htmlWindow = goodies.contentWindow;
// and load the fixed URI
browser.loadURI(newUrl(url));
} else {
// do nothing, let Firefox keep going on the normal flow
}
}
exports.main = function() {
events.on("http-on-modify-request", listener);
}
credit where credit is due: answer by matagus (on question asked by Andrew)
[1]: Link: Intercepting Page Loads
[2]: Noitidart: 'from topics: How can I change the User Agent in just one tab of Firefox? and Is it possible to know the target DOMWindow for an HTTPRequest?'
Never used sdk/tabs before, but you could load your content hidden.
Once your page has loaded your logShow function will run.
Then build into this function some "reveal body" functionality.

change url without redirecting using javascript [duplicate]

This question already has answers here:
How do I modify the URL without reloading the page?
(20 answers)
Closed 9 years ago.
I would like to know how to change the url without redirecting like on this website http://dekho.com.pk/ads-in-lahore
when we click on tabs the url changes but the page dosent reload completely. There are other questions on stackoverflow indicating that it is not possible but i would like to know how the above mentioned website have implemented it.
Thanks
use pushState:
window.history.pushState(data, title, url);
LE: since modern browsers changed behaviour, use replaceState instead:
window.history.replaceState(data, title, url);
If you want to know exactly what they using, it's Backbone.js (see lines 4574 and 4981). It's all mixed up in there with the jQuery source, but these are the relevant lines of the annotated Backbone.Router source documentation page:
The support checks:
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
The route function:
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment) {
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
Choosing between hash and push states:
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
Backbone.$(window).bind('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window).bind('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}​
More on what they're up to:
// If we've started off with a route from a `pushState`-enabled browser,
// but we're currently in a browser that doesn't support it...
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
this.fragment = this.getFragment(null, true);
this.location.replace(this.root + this.location.search + '#' + this.fragment);
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead...
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, this.root + this.fragment);
}
if (!this.options.silent) return this.loadUrl();
And the coup 'd grace:
// If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
}
You should read the annotated Backbone.js link I provided at the top. Very informative.

Categories