How to listen for URL changes in a web-extension that are initiated in the web-app itself? - firefox-addon-webextensions

In a web-extension, I'm registering a content-script programmatically:
browser.contentScripts.register({
matches: ["https://www.example.com/messages/*"],
js: [{file: "my-callback-script"}],
runAt: "document_idle"
});
The page I register it for changes its own URL without a page reload. No, not the bit right of the #, the bit of the URL to the left of the #, but still on the same domain/app. I don't think that web-extensions have a specific API for callbacks for URL changes, only for full page loads. The URL mask 'https://www.example.com/messages/*' is true for before and after, and I was hoping it would just work but it does not.
I'm pretty sure that window.onhashchange is not what I want, and window.addEventListener('popstate', listener); doesn't appear to be it either. How do I do this without polling every few seconds?

Related

google analytics page views are being incremented incorrectly for each page change (angularjs)

I'm using Google analytics for my website (built using AngularJS) to track page views. For some reason though, the page views are incremented incorrectly. For example, when I'm on home page and I switch to the about page, about page gets 1 view, which is correct, but when I switch to another page, that page gets 2 views when it should just get 1. When I switch to another page, that page gets 3 views and so on until I reload the website. Reloading the website will reset the incrementation back to 1, and it'll start counting up again, which means there appears to be a count that is incremented with each state change.
I have this code in all the controllers for each page:
$rootScope.$on('$stateChangeSuccess', function(event) {
if (!$window.ga)
return;
$window.ga('send', 'pageview', { page: $location.path() });
});
What is the cause of the tracking error and how can I resolve it?
$stateChangeSuccess is a global event, which means that if you include the above code in every controller, every time a new controller is instantiated, you are creating a new listener for the event. When a state change occurs, every listener you have registered gets fired, thus the increasing number of calls that are happening.
You actually only need to do this once, probably in your main module run() method, rather than in all of your controllers.

Browser back button doesn't trigger reload after using Angular's $location.path() update

Situation:
On one of the views of my AngularJS app I have two pagination buttons that use $http to request the next and previous data. I'm using ng-click="getNext(url)" rather than an anchor tag with href="#/items/:itemId". The reason I'm doing this is so that I can quickly page through my content asynchronously w/o triggering a page reload. This works just fine, but using this method bypasses updating the page's URL so its possible to have your current content out of sync with your URL's id (i.e. path is #/items/3 but you're currently viewing item 9). This can be easily fixed by updating the URL in the JS using $location.path('items/' + rsp.id). Now I can fetch data in an async manner and still be able to refresh, bookmark, send links and have the correct/current item display.
Problem:
The problem with this is if a user hits getNext() a few times and then tries to go back using the browser's back button the URL updates like it should but for some reason the browser doesn't perform a refresh–it just sits there and updates the URL. This only occurs with when the item in history is from the same view and I have updated the ID with the location service.
Any ideas would be appreciated.
What I tried so far
Promises + Flags
I've been playing with window.onpopstate, but as of right now I don't have any way to have window.onpopstate differentiate between a browser click and a UI click that updates the URL with $location.path(); Right now it fires the event regardless of the source. So I tried setting a flag to assume that every time this event fires its a browser event, but when its a UI-based event I can disabled that flag because my _myRequestFn() will handle it. Even with this promise setup it still fires the window.onpopstate when _myRequestFn() is fired.
var flag = true; // Assume all requests are browser-based
window.onpopstate = function() {
if (flag) {
$route.reload();
}
console.log('onpopstate fired');
};
_myRequestFn = function(id) {
someService.getMyData(id)
.then(function(rsp) {
// Do a bunch of stuff including...
$location.path('items/' + rsp.id);
return rsp;
})
.then(function() {
// Everything is done reset flag
flag = true;
});
};
$scope.getNext(url) {
flag = false;
_myRequestFn(url);
};
Spoofing
Hitting back through #/item/5 > #/item/4 > #/item/3 just updates the URL and not the path, but if the history has a different param #/thing/2 > #/item/2 that triggers a page refresh. Since the browser back button works if the history is from a different param I wanted to see if I loaded a different param it would work. So I created an #/item-a and #/item-b route that loaded the same template and used the same controllers, just toggled from a/b with each request. I would never recommend this solution to someone, I was more just seeing if I could get the refresh to trigger.
Update
Lots of people on IRC are suggesting that I use UI-Router. I'm really trying to use the out of the box Angular solution. Refactoring my whole routing setup to use UI-Router is not an optimal solution.
app.config(['$locationProvider',
function($locationProvider) {
// Enable html5 mode
$locationProvider.html5Mode(true);
}]);
From the docs:
In HTML5 mode, the $location service getters and setters interact with the browser URL address through the HTML5 history API. This allows for use of regular URL path and search segments, instead of their hashbang equivalents.

Change the url with Angular UI-Router, do nothing else

There doesn't appear to be a built-in way to update the url without running all of the other processes, e.g. invoking controllers.
The option "reload", only works with $location.search()
Is there a method, or approach to achieve
How may I achieve something similar to:
$state.go('stateName', params, {justChangeTheUrl: true})
With Angular UI-Router?
If you just want to change the url without having that change processed by ui-router then you can use the following perfectly acceptable hack (coffeescript):
scopeOff = $scope.$on '$stateChangeStart', (e) ->
e.preventDefault()
scopeOff()
$location.path 'just/change/the/url'
Notes
Normally making a change to the browser url via $location.path() would trigger ui-router to begin its processing. This processing starts with the ui-router specific event called $stateChangeStart.
In the code above, however, you are catching and, via e.preventDefault(), throwing away this initial call to $stateChangeStart event and so preventing ui-router from processing the url change.
The watch on the $stateChangeStart event is 'single use', that is the code automatically detaches itself from the event after one run (this is what the call to scopeOff() is doing).
The net effect is to change the url in the browser without triggering ui-router processing and to only apply this restriction to the one url you have just changed. Neat, but not as neat as the new deferIntercept feature that will be with us soon.
Define state parameter in url
$stateProvider.state('stateName', {
url: '/state/path?reload'
});
But be carefull, every $location.search() change on reload param will trigger $stateChangeStart event, and your controller will be recreated

$locationChangeStart never broadcasted; $browser.onUrlChange never called after page load

My angular app is non-standard in that we don't use route provider or UI router. WE use a durandal style of navigation that swaps out views using $http.
locationChangeStart never fires I think due to the fact that we are not changing our URL once in the SPA (though we plan to do so in the future sometimes). I can't test this, but if we remain in the SPA and we change the URL to point to a new view thru code (again, not using routeProvider/UiRouter) should I expect this event to fire?
My main issue is that I cannot handle these events: manual url change, back/forward/history buttons, external link clicks
I see $browser#onUrlChange is the function that broadcasts the $locationChangeStart event but that function only gets called on page load for me, but it seems to be exactly the code that should be getting called.
Documentation:
$browser#onUrlChange
It's only called when the url is changed from outside of angular:
user types different url into address bar
user clicks on history (forward/back) button
user clicks on a link
It's not called when url is changed by $browser.url() method
It says this part also:
NOTE: this api is intended for use only by the $location service. Please use the {#link ng.$location $location service} to monitor url changes in angular apps.
I don't know how to 'use $location service to monitory url changes', so I am listening to the $locationChangeStart event:
$rootScope.$on('$locationChangeStart', function(event, next, current) {
// Breakpoint hit here on page load only
var x = 0;
}
Why doesn't the onUrlChange/$locationChangeStart functions run for me?
thanks!

AngularJS redirect without pushing a history state

I'm working on an AngularJS app that has a catch all route (eg, .when('/:slug', {...)) which is necessary to support legacy url formats from a previous (non-angular) version of the app. The controller that responds to the catch all tries pulling a related object, and, if not found, redirects to a 404 page using the $location.path method. This works at getting the user to the 404 page, but when the user hits back in their browser it takes them back to the page that forced them to the 404 page in the first place and they end up being unable to escape the cycle.
My question is if there is 1) a better pattern for handling this situation, or 2) if there is a way to reroute the user that doesn't force a history push state in the browser?
You can change the url without adding to the history state, found here under "Replace Method". This is effectively the same as calling HTML5's history.replaceState().
$location.path('/someNewPath').replace();
I haven't found that it's possible to change the view without changing the url. The only method to change the view, that I've found, is to change the location path.
The normal operation of the route system is for the $route service to watch for the $locationChangeSuccess event and then begin loading a route. When it's done loading the template, performing the resolve steps and instantiating the controller it then in turn broadcasts a $routeChangeSuccess event. That $routeChangeSuccess is monitored by the ng-view directive, and that's how it knows to swap out the templates and scopes once the new route is ready.
With all of the above said, it may work to have application code emulate the behavior of the $route service by updating the current route and emitting the route change event to get the view to update:
var errorRoute = $route.routes[null]; // assuming the "otherwise" route is the 404
// a route instance is an object that inherits from the route and adds
// new properties representing the routeParams and locals.
var errorRouteInstance = angular.inherit(
errorRoute,
{
params: {},
pathParams: {},
}
);
// The $route service depends on this being set so it can recover the route
// for a given route instance.
errorRouteInstance.$$route = errorRoute;
var last = $route.current;
$route.current = errorRouteInstance;
// the ng-view code doesn't actually care about the parameters here,
// since it goes straight to $route.current, but we should include
// them anyway since other code might be listening for this event
// and depending on these params to behave as documented.
$rootScope.broadcast('$routeChangeSuccess', errorRoute, last);
The above assumes that your "otherwise" route doesn't have any "resolve" steps. It also assumes that it doesn't expect any $routeParams, which is of course true for the "otherwise" route but might not be true if you use a different route.
It's unclear what of the above is depending on implementation details vs. interface. The $routeChangeSuccess event is certainly documented, but the $$route property of the route instance seems to be an implementation detail of the route system given its double-dollar-sign name. The detail that the "otherwise" route is kept in the route table with the key null is possibly also an implementation detail. So with all of this said, this behavior may not remain functional in future versions of AngularJS.
For more information you could refer to the ng-view code that handles this event, which is ultimately what the above code is trying to please, along with the event emitting code that I used as the basis for the above example. As you could infer from these links, the information in this post is derived from the latest master branch of AngularJS, which at the time of writing is labelled as 1.2.0-snapshot.

Resources