19

I have a login box on homepage that I need to link to. The login box has id="login" in html and I have a link to it like this <li><a href="#login">Login</a></li> so on click it takes me to that login div but when I hit refresh or go directly to the link with anchor I get Uncaught Error: No route matched the URL 'login'

Anybody has any ideas how I can accomplish this simple task in Ember? Thanks.

Update

Here's how my code looks like:

The navigation

 <ul class="nav navbar-nav pull-right">
  <li><a href="#login">Signup</a></li> 
  <li>{{#linkTo 'about'}}About{{/linkTo}}</li>
 </ul>

and somewhere below on the page I have

<section id="login">
 -- some content
</section>
eddie.vlagea
  • 667
  • 1
  • 6
  • 18
  • WIP: see https://github.com/emberjs/ember.js/issues/4098 anounced in 1.8 and we could have it in 1.9 http://emberjs.com/guides/deprecations/#toc_deprecate-location-hash-paths-that-don-t-include-a-forward-slash-e-g-foo-not-foo – givanse Nov 12 '14 at 22:36
  • This is a duplicate to: http://stackoverflow.com/questions/16489533/using-html-anchors-in-ember-js – vanthome Jan 25 '15 at 17:28

7 Answers7

15

Query Params

Updated answer based on the Query Params approach (currently featured flag as of Dec 21 2013)

Based on alexspellers original JSFiddle, complete demo can be found here: http://jsfiddle.net/E3xPh/

In your Router, add support for query params

App.Router.map ->
  @resource 'index', path: '/', queryParams: ['anchor']

Using the Route of your choice, setup a property for the anchor query param in the setupController method.

App.IndexRoute = Em.Route.extend
  setupController: (controller, context, queryParams) ->
    controller.set 'anchorLocation', queryParams.anchor

Finally in your Controller make an observer for the anchorLocation property.

App.IndexController = Em.ArrayController.extend
  showAnchor: (->
    $elem = $(@anchorLocation)
    $scrollTo = $('body').scrollTop($elem.offset().top)
  ).observes 'anchorLocation'

Now you can use the following code in your templates to scroll to an anchor or point your browser to /#/?anchor=#login for example.

{{#linkTo anchor='#login'}}Show login{{/linkTo}}

Simple action approach

Possible answer based on what you wrote in the comments to the first answer. Hacked together something simple here.

http://jsbin.com/osEfokE/11

Clicking the Index link takes you to the IndexRoute and scrolls you to the login box, however the URL is not reflecting this change and typing #login will not work either.

App.ApplicationRoute = Ember.Route.extend({
    events: {
        goToLink: function(item, anchor) {
            var $elem = $(anchor);
            var $scrollTo = $('body').scrollTop($elem.offset().top);

            this.transitionToRoute(item.route).then($scrollTo);  //.transitionTo is depricated
        }
    }
});

Instead of using linkTo, you will use goToLink in your template when you want to scroll to an anchor.

<ul>
  <li><a href="#/" {{action goToLink "index" "#login"}}>Index</a></li>
  <li>{{#linkTo about}}About{{/linkTo}}</li>
  <li>{{#linkTo contact}}Contact{{/linkTo}}</li>
</ul>
tomatom
  • 419
  • 4
  • 9
kroofy
  • 984
  • 2
  • 7
  • 21
  • 1
    is there any way of getting this to work on load? So if someone goes to #/login the page loads and scrolls to that section of the page? – DavidVII Dec 20 '13 at 18:53
  • The only way would be to adapt this to be using the query params now available to Ember. Meaning you point your browser to `/#/home?show=login` for example. I haven't tried myself, but I could maybe take a look at it and update this answer. – kroofy Dec 20 '13 at 23:04
  • Added possible solution for your use case in the updated answer. – kroofy Dec 20 '13 at 23:56
  • Hey kroofy, I just tried your solution but query-params has changed over the last weeks and it doesn't seem to work / I am not getting it to work anymore. Would be really helpful if you could update this or point me into the right direction. – Markus Jan 20 '14 at 15:54
  • @Markus they are working on a rewrite of the query params, will be released soon (it seems), it will no longer require you to add `queryParams` to your route but instead to your controller, there's a doc page for the new query params here: http://emberjs.com/guides/routing/query-params/ I will update my answer when the new query params approach is released to the public to avoid my answer to become irrelevant (again). – kroofy Jan 21 '14 at 16:27
  • @kroofy I looked at it and tried it out, but failed so far. I will try a bit more but yeah really appreciate if you are going to update this once we know it won't be changed anymore. Cheers. – Markus Jan 25 '14 at 23:22
  • @kroofy thank you for your very detailed answer! I used your simple action approach. Since Ember has changed its api I updated your solution to Ember Version 1.7 (see my answer below) – Pascal Sep 14 '14 at 12:44
8

The problem is that Ember used the hash part in the URL to store the current state of your application. Spontaneously i see two possible solutions.

1 - *Don't let Ember use the hash part of your URLs.* Therefore use the HTML5 history location implementation of Ember. This will result in URLs like yourdomain.com/users/1/ without #.

App.Router.reopen({
  location: 'history'
});

2 - Don't use this technique. Instead use jQuery to do the scrolling to the relevant part. This could look like this:

<ul class="nav navbar-nav pull-right">
  <li><a {{action jumpToLogin}}>Signup</a></li> 
  <li>{{#linkTo 'about'}}About{{/linkTo}}</li>
</ul>

And in the corresponding view:

App.YourView = Ember.View.extend({
  jumpToLogin : function(){
    $('html, body').animate({
        scrollTop: $("#login").offset().top
    }, 2000);
  }
});

This may seem a lot of code for this small feature, but i guess this is a nicer user experience right? Actually you can improve this approach by extracting this logic into a mixin, so you don't have to repeat it over and over:

App.ScrollToMixin = Ember.Mixin.create({
  scrollDuration : 2000, //default
  scrollTo : function(selector){
    $('html, body').animate({
        scrollTop: $(selector).offset().top
    }, this.get("scrollDuration");
  )
});
// mix it into your View
App.YourView = Ember.View.extend(App.ScrollToMixin, {});

And use it in your template:

<ul class="nav navbar-nav pull-right">
  <li><a {{action scrollTo "login"}}>Signup</a></li> 
  <li>{{#linkTo 'about'}}About{{/linkTo}}</li>
</ul>

PS: I haven't tested the code with the mixin. I am not absolutely sure wether the String "login" gets passed to the action handler exactly like that. So you would have to test :-)

mavilein
  • 11,648
  • 4
  • 43
  • 48
  • I had just completed a JSBin before mavilein answered, http://jsbin.com/osEfokE/2/ similar to what he described in the jQuery technique. – kroofy Aug 29 '13 at 09:13
  • @mavilein I really like your approach (haven't tested the mixin yet either though) but the main problem is still there: when I'm on the About page and click login nothing happens when it should redirect me to homepage and scroll to the login box. In plain html this is accomplished very easy with a link like Login. I need to somehow reproduce that behaviour. – eddie.vlagea Aug 29 '13 at 10:16
  • Ah ok. I did not understand your question that way. I will think a little bit about it. But the code above will be part of the solution for sure as it replicates the browsers behaviour with jumping to anchors. – mavilein Aug 29 '13 at 10:42
  • Just a theory. Using the linkTo helper, pointing it to the IndexRoute and pass a second argument "#login", after transitioning to the Route check if the second argument exists and if it contains a hash, if so scroll to its position with the previous code. – kroofy Aug 29 '13 at 14:22
  • `location: 'history'` doesn't work. It still chomps the hash. Tracked on issue [#4098](https://github.com/emberjs/ember.js/issues/4098). – Bluu Sep 22 '14 at 18:58
  • If you do not want the next route navigated to be in the last scroll position of the previous route, you may want to reset scrollTo `resetScroll: function(){window.scrollTo(0,0);}.on('deactivate')`. – user2517182 Jun 26 '16 at 16:18
5

I use a component, which could also be extended to support query params via an action.

var ScrollToComponent = Ember.Component.extend({
  tagName: 'a',
  anchor: '',

  scrollTo: function () {
    var anchor = this.get('anchor'),
      $el = Ember.$(anchor);

    if ($el) {
      Ember.$('body').scrollTop($el.offset().top);
    }
  }.on('click')
});

This is how you'd use it:

{{#scroll-to anchor=anchor}}Jump To Anchor{{/scroll-to}}

Where anchor is #my-id.

Edit

Here's an ember-cli addon that does this https://www.npmjs.com/package/ember-scroll-to

knownasilya
  • 5,998
  • 4
  • 36
  • 59
  • This was helpful! I modified it a little. Since I was creating a [skip link](http://www.bignerdranch.com/blog/web-accessibility-skip-navigation-links/) I wanted a couple of things. 1st the link should retain link characteristics such as pointer when hovering. To do this, the attribute `href` must be set. Then I wanted the content to be focused once it is scrolled to. Since people who navigate via the keyboard would want to continue once arriving at the content area, instead of cycling back to the last focused element. Here is [the modified component](https://www.codehive.io/boards/pZUuwIk). – michaellee Dec 01 '14 at 20:42
  • @michaellee that link is broken. – knownasilya Dec 01 '14 at 21:05
  • sorry, dns has been down :( Should be back now :) – michaellee Dec 02 '14 at 01:45
1

I got this working in Ember with the simple action approach from kroovy's answer.

Since Ember has changed its API I had to alter kroovys approach at little bit.

Two things have changed:

  1. Events in an Ember.Route are deprecated and replaced by actions in Ember.Controller
  2. instead of transitionTo you have to use transitionToRoute

Here is how I got it working in Ember Version 1.7.0

Ember Handlebars-Template new.hbs

{{!-- Navigation --}}
<ul>
 <li {{action 'goToLink' "new" "#part1"}}>Part 1</li>
 <li {{action 'goToLink' "new" "#part2"}}>Part 2</li>
</ul>

{{!-- content --}}
<div id="part1" class="row">...</div>
<div id="part2" class="row">...</div>

Ember App.Controller NewController.js

App.NewController = Ember.ArrayController.extend({

 actions: {

    goToLink: function(item, anchor) {
        console.log('NewController > goToLink');
        var $elem = $(anchor);
        var $scrollTo = $('body').scrollTop($elem.offset().top);

        this.transitionToRoute(item.route).then( $scrollTo);
    }
}
});
Pascal
  • 1,984
  • 1
  • 21
  • 25
1

The problem with most of the previous answers is that if the route does not change when clicking on a "link" with a "href hash/fragment/id" in it then the ember router will not fire the hooks necessary to perform most of previous solutions.

This is evident in pages with multiple "headings" where you want to allow a user to jump from a "nav/index or list of links" to the relevant section of the page (like on terms of service or about pages).

In these situations ember requires that you "opt into a full transition" so that the router will fire it's hooks even though the route has not changed but the query params have. See "opting into a full transition" for more information.

NOTE: The only documented hook that will be fired when using this "opt in" is the model hook.

I believe that this "full transition" opt-in was only intended to be used when refreshing models on query param change for things like sorting or filtering.

Admittedly this seems like a bit of a hack, but the above "opt-in" can be used for scrolling to our div /anchor tag in question. In the future the opt-in might be able to fire other more appropriate hooks as well (setupController specifically would be nice).

Also In case anyone else is googling. I also tried using the "willTransition" and "didTransition" actions on the route as well. While they did get fired when transitioning from other routes sadly they also fail to fire when transitioning from the same route to itself.

Finally it should be obvious to some (but wasn't for me at first) that in order to get the browser to scroll to the id in question the dom will need to be fully loaded. So we have to perform the scroll inside the Ember.run.schedule('afterRender', callback.

FYI I am using the latest ember-cli, ember.js and ember-data generally available at the moment:

  • ember-cli: 1.13.7
  • ember: 1.13.6
  • ember-data: 1.13.7

app/router.js

  this.route('about', {
    queryParams: ['anchor']
  });

routes/about.js

import Ember from 'ember';

export default Ember.Route.extend({
  queryParams: {
    anchor: {
      refreshModel: true
    }
  },
  model: function(params) {
    var aboutController = this.controllerFor('about');
    aboutController.set('anchorLocation', params.anchor);
  }
});

app/controllers/about.js

import Ember from "ember";
export default Ember.Controller.extend({
  queryParams: ['anchor'],
  anchor: '',
  showAnchor: function() {
    var _this = this;
    Ember.run.schedule('afterRender', function scrollToAnchor(){
      var elem = Ember.$(_this.anchorLocation);
      elem.get(0).scrollIntoView(true);
    });
  }.observes('anchorLocation'),
});

app/templates/footer.js

          <ul>
            <li>
              {{#link-to "about" (query-params anchor='#contact')}}Contact{{/link-to}}
            </li>
            <li>
              {{#link-to "about" (query-params anchor='#support')}}Support{{/link-to}}
            </li>
            <li>
              {{#link-to "about" (query-params anchor='#team')}}Team{{/link-to}}
            </li>
          </ul>
Jesse Sanford
  • 607
  • 1
  • 8
  • 18
  • While not appropriate inline within my given answer, my opinion is appropriate for a comment. It seems to me that this much commentary, LoC and volume of questions and answers is extreme for such a common use case of plain old html. While I know that some work has been done to make this available within the core of the framework: https://github.com/emberjs/ember.js/issues/4098 it seems just silly that this is still so difficult. – Jesse Sanford Aug 20 '15 at 12:35
0

My problem was that if I clicked on the destination link from another page, it would go to the destination but not scroll down to the correct portion of the page. After a few hours here was my fix:

note: I'm using ember-cli, and emblem and ember 1.8

link in my navigation bar (in application.emblem)

li 
 a#concept-link click=jumpToConcept
  | Le Concept 

then if I'm already on page I just scroll up. If I'm not already on page then I go to that page with query params concept=true

action in application controller

scrollToConcept: function() {
  Ember.$(document).ready(
    Ember.$('html, body').animate({
        scrollTop: Ember.$("#concept").offset().top
    }, 750)
  )
},

 //allow clicking of link with specific id to go to that part of page
jumpToConcept : function(){
  if (this.get('currentPath') === 'index') {
    this.send('scrollToConcept');
  } else {
    this.transitionToRoute('index', { queryParams: {concept: true}});
  }
}

in index controller I then add the concept query params

export default Ember.Controller.extend(

  queryParams: ['concept'],
  concept: null
}

I then add a scrollToConcept event in index view (the page I just went to) that listens for page loading and checks the concept queryParam. If concept=true I then scroll to the concept part of the page.

index view

scrollToConcept: function() {
    if (this.controller.get('concept') === 'true') {
        Ember.$('html, body').animate({
        scrollTop: Ember.$("#concept").offset().top
    }, 750);
    }
}.on('didInsertElement')

then for normal index links, I add an action that sets concept=null so that the concept query param doesn't show up in the url.

link in nav bar

a click=goToIndex
  h3 id={logoClass} Happy Dining

then in application controller I go the the index route, set the concept query param to null (so it doesn't show up in the url) and scroll to the top (just incase it was lower on the page)

actions: {
 goToIndex: function() {
  this.transitionToRoute('index', { queryParams: {concept: null}});
  Ember.$(window).scrollTop(0);
 }
}

Hope that helps people in the future!!!

monty_lennie
  • 2,871
  • 2
  • 29
  • 49
0

If anyone's trying to solve this problem with Ember 3, I've got a solution. It makes use of the same queryParams approach as others have mentioned, but I couldn't get anything to work on the controller. The only approach that I found worked was to use the route + jQuery.ready function.

// route/application.js
import Route from '@ember/routing/route';
import $ from 'jquery';

export default Route.extend({
  queryParams: {
    anchor: null
  },

  setupController: function(controller, context, params) {
    let anchor = params.queryParams.anchor;

    // On ready, find the anchor offset and go there
    $().ready(function() {
      let anchorOffset = $("#" + anchor).offset().top
      window.scrollTo(0, anchorOffset);
    });
  }
});

Then in the template, use your queryParams as usual:

{{#link-to "path.to.page" (query-params anchor="my-anchor")}}My Link{{/link-to}}

Only issue here is that it's reloading the page every time, but apart from that seems to work - hope it helps someone.