angular anchors break when switching to html5mode - javascript

I have an annoying issue I'm not sure there's a solution to;
We run html5mode in production, but not in development. The reason is that this is difficult to setup on spring-boot's embedded tomcat. We are coding our html anchors like this:
Route
which of course breaks when not in html5mode where angular will expect the href to be "#/route" instead - which is a real pain. The only way we've found to avoid this problem, is to code links like this:
Route
and have a global function in i.e. $rootScope:
$scope.goto = function (route) {
$location.path(route);
}
But this just seems wrong. I had hoped that using ng-href insted of href on the anchor would sort this out (perhaps detecting if we are in html5mode or not, and auto-add the hash to the url so that we do not need to refactor the entire app if we switch modes), but it doesn't.
Is there no way of using anchors WITHOUT explicitly stating hash urls?

Ok, I still believe this to be a design flaw in angular, and that this particular fix should exist within angular itself.
This directive is matched directly on the 'href' attribute. I've not done any testing on how this affects performance, but it does perform rewrites on internal url's only.
angular.module(_DIRECTIVES_).directive('href', ['$location', function ($location) {
'use strict';
return {
restrict: 'A',
scope: {
href: '#'
},
link: function (scope, element, attrs) {
// Check if we need to rewrite the href
var currentBase = $location.$$protocol + '://' + $location.$$host;
var newBase = element[0].href;
if (element[0].nodeName == 'A' && newBase.indexOf(currentBase) == 0 && !$location.$$html5) {
// Perform the href AFTER angular has done it's thing. Otherwise angular will rewrite back to the original
scope.$watch('href', function () {
// This function may be run several times. Make sure the rewrite only happens once.
if (scope.href.indexOf('#!') !== 0) {
scope.href = '#!' + scope.href;
element.attr('href', scope.href);
}
});
}
}
};
}]);
I hope this will help anybody else in the same predicament as me. Also hope that angular will fix this in the future.

Related

Unit Testing: Directive with Dynamic Template

I am running across an issue when trying to use an unusual method to grab templates for my page-content. I have abstracted it but suffice to say that the code works fine but the testing is throwing up some issues. I have listed my directive below:
Directive:
return {
restrict: 'A',
scope: {
page: '=pageCode',
},
link: function (scope, element) {
$http.get('templates/' + scope.page + '.html', {cache: $templateCache})
.success(function(html) {
element.replaceWith($compile(html)(scope));
});
}
};
Basically, when this directive is called with the page-code attribute, it grabs it and pulls in a specific partial for that page content. I have tried mocking out a $scope to contain the code but that wasn't working so I am hardcoding the data-page-code="404" for now. The test is as follows:
Directive Test:
it('should compile and pass through the scope', function() {
mockDirectiveHtml= '<div data-page-content data-page-code="404"></div>';
mockTemplateHtml= '<h1>Hi</h1>';
$httpBackend.expectGET('templates/404.html').respond(mockTemplateHtml);
pageContentHtml= $compile(mockDirectiveHtml)($scope);
$scope.$digest();
$httpBackend.flush();
expect(pageContentHtml.html()).toEqual('<h1>Hi</h1>');
});
However when this runs, the console is telling me it is trying to hit GET('templates/undefined.html'). Now I am unsure if I have things in the right order, or if that is even the solution but has anyone came across something like this in the past? I assume the directive is running before the attributes are passed to it so when it comes to grab templates/' + 'scope.code' that value is undefined.

How to apply the Location.url() that I changed

I am trying to so a angular redirect however I have only got part of the way. At the moment I have managed to change the URL however I want to then go to that url instead of just changing it.
scope.socialReturnUrl = function ( path ) {
location.url( path + 'assessment' );
};
My below code seems to only change the URL or the location. What am I missing to apply this so it gets redirected?
I should point out that I've already tried to use scope$apply() however this causes the below error:
scope$apply is not defined
I already have scope as a dependency but I havent got a apply dependency is this something that I need?
Let me know your thoughts!
You need to inject the $location service in your controller in order to change the url. If you don't, you'll be calling the window.location object instead, which is not what you should be doing.
var app = angular.module('myApp');
app.controller('MyController',
['$scope', '$location',
function($scope, $location) {
$scope.socialReturnUrl = function ( path ) {
$location.url( path + 'assessment' );
};
}])
Right... I fixed this myself.
Apparently location.url() will not do a full refresh as seen in the documentation.
https://docs.angularjs.org/guide/$location
However you need to add the dependency $window and use the below code to make the page do a full page reload.
window.location.href = path + 'assessment';

Prototyping an Angular Directive

Based on instruction from this question, I have added the following code to my application within the config stage:
$provide.decorator('formDirective', function($delegate) {
var directive = $delegate[0];
directive.controller.prototype.inputs = {};
console.log(directive.controller);
return $delegate;
});
All I want to do is create another field and a few methods to the existing angular form object. All of that appears to be defined within the formDirective controller but when I prototype new fields and methods into that controller they are not available after my application is completed bootstrapping. Is there something I'm missing, is this even possible without modifying the source?
Thanks in advance!
EDIT Code Pen of Design Patterns Here
http://codepen.io/anon/pen/EadRBo
Thanks for your help. I did get this working and if your curious, this is why it was so important:
$provide.decorator('formDirective', function($delegate) {
var directive = $delegate[0];
directive.controller.prototype.$validate = function () {
var form = this;
var p;
for(p in form) {
if(form.hasOwnProperty(p) && p.indexOf('$') < 0) {
form[p].$setTouched();
}
}
};
A simple way to mark every element as touched causing the fields to be invalidated and error logic to kick in. I wanted this when a form was attempted to be submitted so that the user could see all the fields that were required. This also helps to keep my controllers slim without the extra overhead of an additional service.

Using $routeProvider with <form action> and <input name> [duplicate]

I'd like to read the values of URL query parameters using AngularJS. I'm accessing the HTML with the following URL:
http://127.0.0.1:8080/test.html?target=bob
As expected, location.search is "?target=bob".
For accessing the value of target, I've found various examples listed on the web, but none of them work in AngularJS 1.0.0rc10. In particular, the following are all undefined:
$location.search.target
$location.search['target']
$location.search()['target']
Anyone know what will work? (I'm using $location as a parameter to my controller)
Update:
I've posted a solution below, but I'm not entirely satisfied with it.
The documentation at Developer Guide: Angular Services: Using $location states the following about $location:
When should I use $location?
Any time your application needs to react to a change in the current
URL or if you want to change the current URL in the browser.
For my scenario, my page will be opened from an external webpage with a query parameter, so I'm not "reacting to a change in the current URL" per se. So maybe $location isn't the right tool for the job (for the ugly details, see my answer below). I've therefore changed the title of this question from "How to read query parameters in AngularJS using $location?" to "What's the most concise way to read query parameters in AngularJS?". Obviously I could just use javascript and regular expression to parse location.search, but going that low-level for something so basic really offends my programmer sensibilities.
So: is there a better way to use $location than I do in my answer, or is there a concise alternate?
You can inject $routeParams (requires ngRoute) into your controller. Here's an example from the docs:
// Given:
// URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
// Route: /Chapter/:chapterId/Section/:sectionId
//
// Then
$routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
EDIT: You can also get and set query parameters with the $location service (available in ng), particularly its search method: $location.search().
$routeParams are less useful after the controller's initial load; $location.search() can be called anytime.
Good that you've managed to get it working with the html5 mode but it is also possible to make it work in the hashbang mode.
You could simply use:
$location.search().target
to get access to the 'target' search param.
For the reference, here is the working jsFiddle: http://web.archive.org/web/20130317065234/http://jsfiddle.net/PHnLb/7/
var myApp = angular.module('myApp', []);
function MyCtrl($scope, $location) {
$scope.location = $location;
$scope.$watch('location.search()', function() {
$scope.target = ($location.search()).target;
}, true);
$scope.changeTarget = function(name) {
$location.search('target', name);
}
}
<div ng-controller="MyCtrl">
Bob
Paul
<hr/>
URL 'target' param getter: {{target}}<br>
Full url: {{location.absUrl()}}
<hr/>
<button ng-click="changeTarget('Pawel')">target=Pawel</button>
</div>
To give a partial answer my own question, here is a working sample for HTML5 browsers:
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script src="http://code.angularjs.org/1.0.0rc10/angular-1.0.0rc10.js"></script>
<script>
angular.module('myApp', [], function($locationProvider) {
$locationProvider.html5Mode(true);
});
function QueryCntl($scope, $location) {
$scope.target = $location.search()['target'];
}
</script>
</head>
<body ng-controller="QueryCntl">
Target: {{target}}<br/>
</body>
</html>
The key was to call $locationProvider.html5Mode(true); as done above. It now works when opening http://127.0.0.1:8080/test.html?target=bob. I'm not happy about the fact that it won't work in older browsers, but I might use this approach anyway.
An alternative that would work with older browsers would be to drop the html5mode(true) call and use the following address with hash+slash instead:
http://127.0.0.1:8080/test.html#/?target=bob
The relevant documentation is at Developer Guide: Angular Services: Using $location (strange that my google search didn't find this...).
It can be done by two ways:
Using $routeParams
Best and recommended solution is to use $routeParams into your controller.
It Requires the ngRoute module to be installed.
function MyController($scope, $routeParams) {
// URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
// Route: /Chapter/:chapterId/Section/:sectionId
// $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'}
var search = $routeParams.search;
}
Using $location.search().
There is a caveat here. It will work only with HTML5 mode. By default, it does not work for the URL which does not have hash(#) in it http://localhost/test?param1=abc&param2=def
You can make it work by adding #/ in the URL. http://localhost/test#/?param1=abc&param2=def
$location.search() to return an object like:
{
param1: 'abc',
param2: 'def'
}
$location.search() will work only with HTML5 mode turned on and only on supporting browser.
This will work always:
$window.location.search
Just to summerize .
If your app is being loaded from external links then angular wont detect this as a URL change so $loaction.search() would give you an empty object . To solve this you need to set following in your app config(app.js)
.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider)
{
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(true);
}]);
Just a precision to Ellis Whitehead's answer. $locationProvider.html5Mode(true); won't work with new version of angularjs without specifying the base URL for the application with a <base href=""> tag or setting the parameter requireBase to false
From the doc :
If you configure $location to use html5Mode (history.pushState), you need to specify the base URL for the application with a tag or configure $locationProvider to not require a base tag by passing a definition object with requireBase:false to $locationProvider.html5Mode():
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
you could also use $location.$$search.yourparameter
I found that for an SPA HTML5Mode causes lots of 404 error problems, and it is not necessary to make $location.search work in this case. In my case I want to capture a URL query string parameter when a user comes to my site, regardless of which "page" they initially link to, AND be able to send them to that page once they log in. So I just capture all that stuff in app.run
$rootScope.$on('$stateChangeStart', function (e, toState, toParams, fromState, fromParams) {
if (fromState.name === "") {
e.preventDefault();
$rootScope.initialPage = toState.name;
$rootScope.initialParams = toParams;
return;
}
if ($location.search().hasOwnProperty('role')) {
$rootScope.roleParameter = $location.search()['role'];
}
...
}
then later after login I can say
$state.go($rootScope.initialPage, $rootScope.initialParams)
It's a bit late, but I think your problem was your URL. If instead of
http://127.0.0.1:8080/test.html?target=bob
you had
http://127.0.0.1:8080/test.html#/?target=bob
I'm pretty sure it would have worked. Angular is really picky about its #/

In Angular: are the compile function's pre and post methods the same as link's pre and post

In an angular directive's compile function there is a pre and post. Is this pre and post really the same as the link function?
For example in the code below, is the link function the same (shortcut if you will) as the pre and post of the compile function below it?
Link
....
link: {
pre: function(scope, elem, attr) {
//stuff
},
post: function(scope, elem, attr) {
//stuff
}
}
....
Compile...
....
compile: function(tElem, tAttrs){
return {
pre: function(scope, iElem, iAttrs){
//stuff
},
post: function(scope, iElem, iAttrs){
//stuff
}
}
}
.....
Compile is run first (and is usually where you maipulate your "template" dom elements). Link is run second, and is usually where you attach your directive to $scope.
They also run in a specific order, so you can use that fact when you're designing directives that require some "parent" directive setup in order to operate properly (like a tr:td sorta thing).
There's a really great article on timing for compile vs link you can take a look at for more clarity.
Also - there's a very low level stack answer to a similar question you might like (note that its NOT the one listed first, it's the one most upvoted).
So, What's the Difference?
So are compile pre/post link "the same" as the link function? You decide.
If you define compile on a directive, the framework ignores your link function (because the compile function is supposed to return pre/post link functions).
It's a little bit like link overloads compile.postLink and link.pre overloads compile.preLink.
When this overloading happens, are you aware of anything different happening (i.e. any other functionality being added) as supposed to just returning pre and post from compile?
If you look at the source code,
when the $directiveProvider registers the directives, if the compile property is missing and the link property exists, it creates a compile property that is an empty function that returns the link property.
So the answer is that the link functions returned by the compile function are the same as the link functions provided by the link property of the DDO. No other functionality is added.

Categories

Resources