AngularJS: Anchor links (for external visitors, not internal) [duplicate] - angularjs

Is it possible to use anchor links with Angularjs?
I.e.:
Top
Middle
Bottom
<div name="top"></div>
...
<div name="middle"></div>
...
<div name="bottom"></div>
Thank you

There are a few ways to do this it seems.
Option 1: Native Angular
Angular provides an $anchorScroll service, but the documentation is severely lacking and I've not been able to get it to work.
Check out http://www.benlesh.com/2013/02/angular-js-scrolling-to-element-by-id.html for some insight into $anchorScroll.
Option 2: Custom Directive / Native JavaScript
Another way I tested out was creating a custom directive and using el.scrollIntoView().
This works pretty decently by basically doing the following in your directive link function:
var el = document.getElementById(attrs.href);
el.scrollIntoView();
However, it seems a bit overblown to do both of these when the browser natively supports this, right?
Option 3: Angular Override / Native Browser
If you take a look at http://docs.angularjs.org/guide/dev_guide.services.$location and its HTML Link Rewriting section, you'll see that links are not rewritten in the following:
Links that contain target element
Example: link
So, all you have to do is add the target attribute to your links, like so:
Go to inpage section
Angular defaults to the browser and since its an anchor link and not a different base url, the browser scrolls to the correct location, as desired.
I went with option 3 because its best to rely on native browser functionality here, and saves us time and effort.
Gotta note that after a successful scroll and hash change, Angular does follow up and rewrite the hash to its custom style. However, the browser has already completed its business and you are good to go.

I don't know if that answers your question, but yes, you can use angularjs links, such as:
<a ng-href="http://www.gravatar.com/avatar/{{hash}}"/>
There is a good example on the AngularJS website:
http://docs.angularjs.org/api/ng.directive:ngHref
UPDATE: The AngularJS documentation was a bit obscure and it didn't provide a good solution for it. Sorry!
You can find a better solution here: How to handle anchor hash linking in AngularJS

You could try to use anchorScroll.
Example
So the controller would be:
app.controller('MainCtrl', function($scope, $location, $anchorScroll, $routeParams) {
$scope.scrollTo = function(id) {
$location.hash(id);
$anchorScroll();
}
});
And the view:
Scroll to #foo
...and no secret for the anchor id:
<div id="foo">
This is #foo
</div>

$anchorScroll is indeed the answer to this, but there's a much better way to use it in more recent versions of Angular.
Now, $anchorScroll accepts the hash as an optional argument, so you don't have to change $location.hash at all. (documentation)
This is the best solution because it doesn't affect the route at all. I couldn't get any of the other solutions to work because I'm using ngRoute and the route would reload as soon as I set $location.hash(id), before $anchorScroll could do its magic.
Here is how to use it... first, in the directive or controller:
$scope.scrollTo = function (id) {
$anchorScroll(id);
}
and then in the view:
Text
Also, if you need to account for a fixed navbar (or other UI), you can set the offset for $anchorScroll like this (in the main module's run function):
.run(function ($anchorScroll) {
//this will make anchorScroll scroll to the div minus 50px
$anchorScroll.yOffset = 50;
});

I had the same problem, but this worked for me:
<a ng-href="javascript:void(0);#tagId"></a>

Works for me:
<a onclick='return false;' href="" class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" ng-href="#profile#collapse{{$index}}"> blalba </a>

You need to only add target="_self" to your link
ex. Services<div id="services"></div>

Or you could simply write:
ng-href="\#yourAnchorId"
Please notice the backslash in front of the hash symbol

If you are using SharePoint and angular then do it like below:
<a ng-href="{{item.LinkTo.Url}}" target="_blank" ng-bind="item.Title;" ></a>
where
LinkTo and Title is SharePoint Column.

The best choice to me was to create a directive to do the work, because $location.hash() and
$anchorScroll() hijack the URL creating lots of problems to my SPA routing.
MyModule.directive('myAnchor', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elem, attrs, ngModel) {
return elem.bind('click', function() {
//other stuff ...
var el;
el = document.getElementById(attrs['myAnchor']);
return el.scrollIntoView();
});
}
};
});

You can also Navigate to HTML id from inside controller
$location.hash('id_in_html');

Related

Initialize jQuery tabs in a directive that update on route change with ui-router

I am attmpting to initialize a jquery plugin for tabs. The whole directive thing is new to me but I got the following to work. However when I click another list item it does not re-initialize the tab section. Is there a way to re-initialize this every time I click on a new item and the ui-router changes the route or content?
EDIT: Maybe some needed clarity:
the tabs are simply a jQuery plugin to make my life easier. However the tabs need initialization before they work. I normally do this with a jQuery script on the bottom of my HTML but that didn't work with the Angular setup I have. Each item I click opens a details section with tabs. As is the first time I click a item it instantiates successfully and the tabs appear. However if I click another item the tabs don't work. It only works for one item even though they both have the attribute for my directive "tabstuff". UI router is just how I am controlling switching data.
app.directive('tabstuff', function() {
var linker = function(scope, element, attr){
$(document).ready(function(){
$('ul.tabs').tabs();
});
};
return {
restrict: 'A',
link: linker
};
});
Also as I mentioned I am new to the directive idea. Please feel free to help me improve any part of my code.
I think it's much better to use native angular features to create tabs directive.
You can use sytabs
index.html
<script src="path/to/sy-tabs.js"></script>
app.js
angular.module('yourApp', ['sy.tabs']);
To use syTabs, insert something like this in your HTML:
<sy-tabs>
<sy-pane title="Pane 1">
<p>Lorem Ipsum...</p>
</sy-pane>
<sy-pane title="Pane 2">
<p>Lorem Ipsum...</p>
</sy-pane>
...
</sy-tabs>

Preserve traditional anchor behavior with ng-include

I am not building a single-page application, but rather a "traditional" site that uses AngularJS in places. I've hit the following problem (using 1.3.0-beta.6):
Standard, working anchor links:
Link text
... [page content]
<a id="foo"></a>
<h1>Headline</h1>
[more content]
That works fine. Now I introduce a template partial somewhere:
<script type="text/ng-template" id="test-include.html">
<p>This text is in a separate partial and inlcuded via ng-include.</p>
</script>
which is invoked via:
<div ng-include="'test-include.html'"></div>
The partial is included properly, but the anchor link no longer works. Clicking on "Link text" now changes the displayed URL to /#/foo rather than /#foo and the page position does not change.
My understanding is that using ng-include implicitly tells Angular that I want to use the routes system and overrides the browser's native anchor link behavior. I've seen recommendations to work around this by changing my html anchor links to #/#foo, but I can't do that for other reasons.
I don't intend to use the routes system - I just want to use ng-include without it messing with browser behavior. Is this possible?
The reason is that angular overrides the behavior of standard HTML tags which include <a> also. I'm not sure when this change happened because angular v1.0.1 works fine with this.
You should replace the href attribute with ngClick as:
<a ng-click="scroll()">Link text</a>
And in a controller so:
function MyCtrl($scope, $location, $anchorScroll) {
$scope.scroll = function() {
$location.hash('foo');
$anchorScroll();
};
};
Demo: http://jsfiddle.net/HB7LU/3261/show/
Or simply use double hash as:
<a href='##foo'>Link text</a>
Demo: http://jsfiddle.net/HB7LU/3262/show/
Update: I did not know that you want no modification in HREF. But you can still achieve the desired result by overriding the existing a directive as:
myApp.directive('a', function() {
return {
restrict: 'E',
link: function(scope, element) {
element.attr('href', '#' + element.attr('href'));
}
};
});
Demo: http://jsfiddle.net/HB7LU/3263/
My understanding is that using ng-include implicitly tells Angular
that I want to use the routes system and overrides the browser's
native anchor link behavior. I've seen recommendations to work around
this by changing my html anchor links to #/#foo, but I can't do that
for other reasons.
Routing system is defined in a separate module ngRoute, so if you did not injected it on your own - and I am pretty sure you did not - it is not accessible at all.
The issue is somehow different here.
ng-include depends on: $http, $templateCache, $anchorScroll, $animate, $sce. So make use of ng-include initiate all these services.
The most natural candidate to investigate would be $anchorScroll. The code of $anchorScroll does not seem to do any harm, but the service depends on $window, $location, $rootScope. The line 616 of $location says:
baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to ''
So basically the base href is set to '', if it was no set before.
Now look HERE - from BalusC answer :
As to using named anchors like , with the tag
you're basically declaring all relative links relative to it,
including named anchors. None of the relative links are relative to
the current request URI anymore (as would happen without the
tag).
How to mitigate the issue?
I do not have much time today, so cannot test it myself, but what I would try to check as the first option is to hook up to '$locationChangeStart' event and if the new url is of #xxxxxx type just prevent the default behaviour and scroll with $anchorScroll native methods instead.
Update
I think this code should do the work:
$scope.$on("$locationChangeStart", function (event, next, current) {
var el, elId;
if (next.indexOf("#")>-1) {
elId = next.split("#")[1];
el = document.getElementById(elId);
if(el){
// el.scrollIntoView(); do not think we need it
window.location.hash = "#" + elId;
event.preventDefault();
}
}
});
This is the best solution, and works in recent versions of Angular:
Turn off URL manipulation in AngularJS
A lot late to the party but I found that adding a simple target="_self" fixes it.
Link
Rather than applying the angular application to the entire page, you can isolate the application to just the places you want to perform an ng-include. This will allow links outside the scope of the application to retain their normal functionality, while allowing links within the application to be handled as desired.
See this plunkr:
http://plnkr.co/edit/hOB7ixRM39YZEhaz0tfr?p=preview
The plunkr shows a link outside the app that functions as normal, and a link within the app that is handled using an overriding a directive to restore normal functionality. HTML5 mode is enabled to retain 'standard' URLs (rather than 'hashbang' [without the bang!] URLs).
You could equally run the whole of the page within the app, but I thought it would be worth demonstrating how to isolate angular to certain parts of the page in any case.

Turn on angularjs on only specific DOM hierarchies?

I'd like to turn off angular interpolation from the top level of my site but re-enable it for individual elements.
I'm trying to add some angular functionality to a rather large legacy site, and it's not feasible to add ng-non-bindable everywhere that could possibly contain {{bindable}} brackets. This is especially important because the site may have {{unparseable:er9 >-14?%(% randomness}} within those brackets. (Angular throws a [$parse:syntax] error for that and stops parsing any of the rest of my page)
Ideally, I'd set up something like <html ng-app="MyApp" ng-non-bindable> on every page, and then have <div ng-controller="myController"> on the few places that actually use angular.
So far I haven't figured out a way to do this. I looked at changing the angular parser to ignore text nodes until it sees a controller, but that seems like overkill. I also tried adding ng-app only to the nodes I want the app to live on, but I then have to manually bootstrap each node with the app, and I think that causes me to have multiple copies of the app running simultaneously and any singletons I was hoping for would (e.g. for cacheing) would be instantiated multiple times (unless I'm wrong about this?)
Is there a way to put ng-app and ng-non-bindable on the top level <html> and then manually add the divs I care about to the app?
I've set up a plunkr with a simple example: http://plnkr.co/edit/antMrWmWnKXHcxklh9IY?p=preview
Michal Charemza's comment above led me to a working solution.
I wrote a terminal directive that compiles and attaches to the $rootScope each [ng-bindable] element in the (using jquery):
app.directive('defaultNonBindable', ['$compile', '$rootScope',
function($compile, $rootScope) {
return {
compile: function(scope, elm, attrs) {
var bindables = $('[ng-bindable]');
bindables.each( function() {
var el = angular.element(this),
compiled = $compile(el);
compiled($rootScope);
});
},
terminal: true,
}
}
]);
I then wrap each block that's angular-ified in an ng-bindable div:
<div ng-bindable>
<div ng-controller="MyController">
{{this_works}}
</div>
</div>
I've forked the original plunkr with an example of it working: http://plnkr.co/edit/2rWCy7dwJNqTShXKiLgG?p=preview
One possible solution would be to configure angular in a way, that it is not looking for curly braces, but your own special syntax for interpolating.
var myApp = angular.module('App', []);
myApp.config(function($interpolateProvider) {
$interpolateProvider.startSymbol('[[');
$interpolateProvider.endSymbol(']]');
});
myApp.controller("Ctrl", function($scope){
$scope.value="value";
});
<div>[[value]]</div>
Plunker
I did not test it and there might be some side effects to it, especially for third party code, but it could be a way for managing the transition phase
regards

Changes to scope on click are not being updated across my app

Started using Angular last week, read/watched many tutorials and I'm currently trying to build a newsfeed type application.
Here's the skinny: I have a service that gets data from the server. On the newsfeed itself I have two controllers: one that has the entire newsfeed in its scope and another that has an instance for each newsfeed article. If the user clicks an icon on an individual post it should call a service that has been injected into both controllers and then broadcasts a message that the main controller picks up. The main controller then updates a variable in a filter, filtering the newsfeed content based on the user's selection.
Here's the problem: Everything works fine except that the main controller doesn't update the bound variable in the HTML. I have read close to every SO article on two-way binding within an ng-repeat and the related struggles, but in my case the bound variable falls outside an ng-repeat, hence why I'm posting.
The code:
services.factory('filterService', function() {
var filterService = {};
filterService.filterKey = '';
filterService.getFilter = function() {
return filterService.filterKey;
};
filterService.setFilter = function(name) {
filterService.filterKey = name;
$rootScope.$broadcast('changeFilter');
};
return filterService;
});
app.controller('CommentCtrl', function($scope, $timeout, $http, filterService) {
$scope.setSearchParam = function(param) {
alert('clicked: ' + param)
filterService.setFilter(param);
}
app.controller('FeedCtrl', function($scope, articles, filterService, $timeout) {
$scope.articles = articles;
$scope.model = {
value: ''
};
$scope.$on('changeFilter', function() {
console.log(filterService.filterKey);
$scope.model.value = filterService.filterKey
}
});
});
<div class="articles">
<div class="articleStub" ng-repeat="article in articles|filter:model.value">
<div ng-controller="CommentCtrl">
<div class="{{article.sort}}">
<div class="leftBlock">
<a href="#" ng-click="setSearchParam(article.sort)">
<div class="typeIcon">
<i ng-class="{'icon-comments':article.question, 'icon-star':article.create, 'icon-ok-sign':article.notype}"></i>
</div>
</a>
Note: the FeedCtrl controller is called in the app.config $routeprovider function thing whatever its called
Edited to add: the alert and console checks both work, so I'm assuming the issue is not in the filterService or CommentCtrl.
Here's the Plnkr: http://plnkr.co/edit/bTit7m9b04ADwkzWHv88?p=preview
I'm adding another answer as the other is still valid, but is not the only problem!
Having looked at your code, your problems were two fold:
You had a link to href="#"
This was causing the route code to be re-run, and it was creating a new instance of the controller on the same page, but using a different scope. The way I found this out was by adding the debug line: console.log("running controller init code for $scope.$id:" + $scope.$id); into script.js under the line that blanks the model.value. You'll notice it runs on every click, and the $id of the scope is different every time. I don't fully understand what was happening after that, but having two of the same controller looking after the same bit of the page can't be a good thing!
So, with that in mind, I set href="". This ruins the rendering of the button a bit, but it does cure the problem of multiple controllers being instantiated. However, this doesn't fix the problem... what's the other issue?
angular.element.bind('click', ....) is running 'outside the angular world'
This one is a bit more complicated, but basically for angular data-bindings to work, angular needs to know when the scope gets changed. Most of the time it's handled automagically by angular functions (e.g. inside controllers, inside ng-* directives, etc.), but in some cases, when events are triggered from the browser (e.g. XHR, clicks, touches, etc.), you have to tell angular something has changed. You can do this with $scope.$apply(). There are a few good articles on the subject so I'd recommend a bit of reading (try here to begin with).
There are two solutions to this - one is to use the ng-click directive which wraps the native click event with $scope.$apply (and has the added advantage that your markup is more semantic), or the other is to do it yourself. To minimise the changes to your code, I just wrapped your click code in scope.$apply for you:
element.bind('click', function() {
// tell angular that it needs to 'digest' the changes you're about to make.
scope.$apply(function(){
var param = scope.article.sort;
filterService.setFilter(param);
})
});
Here's a working version of your code: http://plnkr.co/edit/X1AK0Bc4NZyChrJEknkN?p=preview
Note I also set up a filter on the list. You could easily ad a button to clear it that is hidden when there's no filter set:
<button ng-click="model.value=''" ng-show="model.value">Clear filter</button>
Hope this helps :)
I actually think the problem is not that your model.value isn't getting updated - all that code looks fine.
I think the problem lies in your filter.
<div class="articleStub" ng-repeat="article in articles|filter:model.value">
This filter will match any object with any field that contains model.value. What you actually want to do is the following:
<div class="articleStub"
ng-repeat="article in articles|filter:{sort: model.value}:true">
To specify that you only want to match against the sort property of each article. The final true parameter means that it'll only allow strict matches as well, so ed wouldn't match edward.
Note that | filter:{sort: model.value}:true is an angular expression, the :s are like JavaScript commas. If you were to imagine it in JavaScript it would be more like: |('filter',{sort:model.value}, true) where | is a special 'inject a filter here' function..
EDIT:
I'm finding it hard to debug your example without having the working code in front of me. If you can make it into a plunker I can help more, but in the meantime, I think you should try to make your code less complicated by using a different approach.
I have created a plunker that shows an easy way to filter a list by the item that you click. I've used very little code so hopefully it's quite easy to understand?
I would also recommend making your feed items into a directive. The directives can have their own controller so it would prevent you having to do the rather ugly repeating of a ng-controller.

How to set different menu in different controller in angular

I have top menu in my app, and I would like to have different content there depending on controller. In Rails it is easy with content_for, but how to achieve it with angular? I already know this solution: AngularJS: How can I pass variables between controllers? but maybe there is better way to do this?
One of the fun things about Angular is there is, often, no "better way" without understanding the context of your application. There are, "other" ways, which is best depends tremendously. I guess more info might help to customise or recommend a particular answer, too.
However here's my thought stream on this topic:
First thoughts
More defined service
The answer you found is not bad, though I would probably take it a little further and have some sort of 'menu service' rather than a highly generic 'property' store. This menu service could be manipulated by the controllers that ng-view instantiates.
Via the route mappings
Taking it even further, it would be possible to include menu information within the route provider declarations and then, on $routeChangeSuccess or $routeChangeStart have the menu controller update itself based on the data from the routes (perhaps maintaining the service as well so that controllers can contribute "special" menu options, thereby allowing a degree of customisation).
A few more options
If shared services (a Angular best practice, fyi) aren't to your liking or setup and playing with the routes isn't, either (could be tricky) then I can see a few more options:
$rootScope
One is to inject $rootScope (the great grand-daddy of all scopes) and have a collection on there that is your menu items; each controller could then just update that manually.
Custom events
Here $rootScope.$emit() is your friend - you could emit some sort of event and supply menu configuration data. A controller would then be listening ($rootScope.$on()) for the event and update/clear-out/replace it's own list of menu items with the newly-emitted menu list.
Advanced routing
Getting even funkier, you could even try and see if including functions in the resolve part of the routes would do the trick.
References
Info on playing with the scope is on Angular's documentation: http://docs.angularjs.org/api/ng.$rootScope.Scope
Info on complex routing is here: http://www.yearofmoo.com/2012/10/more-angularjs-magic-to-supercharge-your-webapp.html#additional-features-of-controllers-and-routes (yearofmoo are Rails fans, so their opinions might match your own)
For a similar task I wrote an angular js directive that mimics rails content_for
Sorry for CoffeeScript:
App.directive "contentFor", ["$sce", ($sce) ->
compile: (e, attrs, transclude) ->
pre: (scope, iElement, iAttrs, controller) ->
varname = iAttrs["contentFor"]
# Looks for the closest scope where 'varname' is defined
# or pick the top one
targetScope = scope
while targetScope.$parent
if ( if targetScope.hasOwnProperty then targetScope.hasOwnProperty(varname) else targetScope[varname] )
break
else
targetScope = targetScope.$parent
# Store html inside varname
html = iElement.html()
targetScope[varname] = $sce.trustAsHtml(html)
# remove the directive element
iElement.remove()
]
You can use it like this
<span content-for="menuHtml">
<ul>
<li>Menu Item 1</li>
<li>Menu Item 2</li>
<li>Menu Item 3</li>
</ul>
</span>
And here is the equivalent of <%= yield(:menuHtml) %>:
<nav ng-bind-html="menuHtml"></nav>
In the end you could reset menu to a default binding to $routeChangeSuccess event in your controller:
$scope.$on '$routeChangeSuccess', ->
$scope.menuHtml = $sce.trustAsHtml("<ul>...</ul>")
Bind your top level menu to an object in top level scope. Create a method in that controller to set that variable. Within your child controllers call the method to set the appropriate variable that will change the menu based on your controller.
You can leverage services to store menu items if they are not coming from the server.
I think making a direct reference to the div which will be replaced instead of using angular's scopes as in mcasimir's answer is much simpler. Also it has the added benefit of allowing angular to compile the directives which in the other answer will give you an 'untrusted' error. This is what the directive looks like:
.directive('appContentFor', ($compile) -> {
replace: true,
compile: (element,attr,transclude)->
return (scope, element, attrs)->
element.remove()
node = $(attrs.node)
$(node[0]).replaceWith($compile(element.html())(scope))})
By injecting the $compile service angular can do it's job on the html before inserting it where it belongs. Now you can use this directive like this:
<div app-content-for node="div#awesomeContentFor">
<ul id="awesome_inserted_list">
<li>{{1+2}}</li>
<li>{{something_on_current_scope}}</li>
</ul>
</div>
then somewhere else just add the div with id awesomeContentFor:
<div id="awesomecontentfor"></div>

Resources