Recently, I played with the Hot Towel MVC template and tried to integrate Bootstrap's scrollspy. This was very difficult for me as my main topic is rather C# and WPF than JavaScript and Html. But after a struggle here and there, I found a solution. Not perfect (any improvements are welcome), but it's sufficient for me.
In shell.js, in the activate-method, we register the normal route ('alpha'), and for each anchor-link within the alpha-page, another route that maps to the same page ('beta' and 'gamma'). Now, when you click on an anchor on page 'alpha', the activate-method of the corresponding viewmodel will be called with the anchor-value as parameter (see below).
Nothing special for the html-part. The anchor-links are just normal relative anchor-links and the classes are mainly from bootstrap for the scrollspy- and affix-plugin.
The activate-function is called whenever a route to this viewmodel is activated. When the normal alpha-route is activated, it is called with "#/alpha" as hash and nothing happens. When an anchor-route is activated, it is called with "#/beta" for example and we scroll to this anchor with jQuery.
The viewAttached-function is called when the databinding has finished. Clicking an anchor-link calls this method not for the first time and nothing happens. The important work is done on the first call: the height of the navigation-div is fitted to its content, the affix-parameter is set and the scrollspy is activated (twice, because I possibly do something wrong here *g*).
The problem is that Sammy.js, which Durandal uses for routing, does the routing via hashes (#), which apparently are also used for anchor-navigation. In order to prevent Sammy.js to incorrectly interpret the anchor-links, I added an invisible route for them to the viewmodel of the current page and route it there manually. Hard to describe, see the code!
//shell.js: router.mapNav('alpha', 'viewmodels/alpha', 'Alpha'); router.mapRoute('beta', 'viewmodels/alpha', 'Alpha', false); router.mapRoute('gamma', 'viewmodels/alpha', 'Alpha', false);
In shell.js, in the activate-method, we register the normal route ('alpha'), and for each anchor-link within the alpha-page, another route that maps to the same page ('beta' and 'gamma'). Now, when you click on an anchor on page 'alpha', the activate-method of the corresponding viewmodel will be called with the anchor-value as parameter (see below).
//alpha.html:
Nothing special for the html-part. The anchor-links are just normal relative anchor-links and the classes are mainly from bootstrap for the scrollspy- and affix-plugin.
//alpha.js: define(['durandal/plugins/router'], function (router) { var vm = { activate: activate, viewAttached: viewAttached, loaded: false, items: [ { name: "Beta", link: "beta" }, { name: "Gamma", link: "gamma" }, ] }; return vm; function activate(routeParameters) { if (routeParameters.routeInfo.hash != "#/alpha") { $(document.body).animate({ 'scrollTop': $(routeParameters.routeInfo.hash.replace("/", "")).offset().top }, 100); return false; } return true; } function viewAttached() { if (!vm.loaded) { vm.loaded = true; $('.bs-docs-sidebar').height($(".bs-docs-sidenav").height()); $('.bs-docs-sidenav').affix({ offset: $('#nav').position() }); $('.bs-docs-sidebar').scrollspy(); $('[data-spy="scroll"]').each(function() { var $spy = $(this).scrollspy('refresh'); }); } }
The activate-function is called whenever a route to this viewmodel is activated. When the normal alpha-route is activated, it is called with "#/alpha" as hash and nothing happens. When an anchor-route is activated, it is called with "#/beta" for example and we scroll to this anchor with jQuery.
The viewAttached-function is called when the databinding has finished. Clicking an anchor-link calls this method not for the first time and nothing happens. The important work is done on the first call: the height of the navigation-div is fitted to its content, the affix-parameter is set and the scrollspy is activated (twice, because I possibly do something wrong here *g*).
//app.css: .affix { position: fixed; }
// index.cshtml: < body data-spy="scroll" data-target=".bs-docs-sidebar">
Thanks for the article! I was looking to do something similar myself today and found your post (referenced from StackOverflow).
ReplyDeleteThere's one problem with your solution though; if you refresh the page while your page URL refers to one of your bookmarks (e.g. '#/gamma'), the scroll won't work because the $("#gamma") won't find any elements. This is because activate gets called before the view has been attached.
I've found a work-around for this though, as well as a nice way to define only a single route which can also process your bookmarks for you. If you're interested, I've written about it here:
http://quickduck.com/blog/2013/04/23/anchor-navigation-durandal/
Enjoy!
This comment has been removed by the author.
DeleteHi Gerrod, the link to quickduck isn't working for me- could you please update it?
DeleteThanks!