Slugs for replacing id path for dynamic segments

Hello all! I’m working on a new project where I am trying to replace the id of a project with a computed property, basically a slug. I’ve looked in the guides and have seen the section on ‘dynamic segments’ and haven’t really figured out why it is not working. I’ve also looked back at past posts and haven’t figured it out from there. Docs here Defining Your Routes - Routing - Ember Guides.

Router

App.Router.map(function() {
  this.resource('projects', {path: '/'}, function(){
    this.route('show', { path: '/:slug' });
  });
});

Project Model

Scrum.Project = DS.Model.extend({
  name: DS.attr('string'),
  description: DS.attr('string'),
  states: DS.hasMany('state', {async: true}),
  slug: function() {
    return this.get('name').replace(/\s+/g, '-').toLowerCase();
  }.property('name') 
});

Project Index Route

App.ProjectsIndexRoute = Ember.Route.extend({
  model: function () {
    return this.store.findAll('project'); 
  }
});

Project Show Route

App.ProjectsShowRoute = Ember.Route.extend({
  model: function(params) {
    Ember.Logger.log(params.slug);
    return this.store.find('project', {slug: params.slug});
  },
  // allows us to use slug as the url
  serialize: function(model, params) {
    return { slug: model.get('slug')};
  }
});
2 Likes

Hi, can you elaborate on what exactly is not working, maybe provide a http://emberjs.jsbin.com?

Hi, thanks for the reply. I created one found here: http://emberjs.jsbin.com/fizuno/1/edit. There are slight modifications in the routes in the jsbin compared to the above code. What happens is that I receive the error below, when clicking one of the projects and then refreshing the page or navigating directly to the say ‘projects/test’. The slug is for creating urls like /projects/this-is-a-project for a project named: ‘This is a Project’.

Error while loading route: TypeError: Cannot read property 'map' of undefined at Ember.EnumerableUtils.map (http://builds.emberjs.com/tags/v1.5.1/ember.js:1918:15)

Well, that took me a while (since I have not worked with the fixtures adapter before), but it all revolves around the FixtureAdapter#queryFixtures method which has to be implemented or otherwise returns undefined, and you can’t map over undefined :wink:

App.ApplicationAdapter = DS.FixtureAdapter.extend({
  queryFixtures: function(fixtures, query, type) {
    console.log('perform query for', arguments);
    return [];
  }
});

You get the idea. However, I think this should be considered a bug (there should at least be a warning that there was an attempt to query a fixture adapter like there used to be?!)

EDIT: Turns out there is: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/fixture_adapter.js#L90 You were probably using the production version which does not contain calls to Ember.assert? Always use development versions to develop :wink:

Hello,

sorry for digging this out but I’m having a similar problem. I’m using ember 1.10, ember-template-compiler 1.10, ember-data-1.0.0-beta-15 and the current localstorage adapter.

The routing works whenever I select my post from the posts list: ember will load the correct slug in the url and show the post. However if I refresh the site in the browser and want to get to the post “directly” through the post slug, I encounter the following error:

Error while processing route: post Cannot read property 'findBy' of undefined TypeError: Cannot read property 'findBy' of undefined
at App.PostRoute.Ember.Route.extend.model (file:///Users/basti/dev/cms-5000/js/models/post.js:35:34)

Router

App.Router.map(function() {
  this.resource('posts', { path: '/' }); // posts list
  this.resource('post',  { path: '/post/:post_slug' }); // single post
});

PostsRoute

App.PostsRoute = Ember.Route.extend({
  model: function () {
    return this.store.find('post');
  }
});

PostsIndexRoute

App.PostsIndexRoute = Ember.Route.extend({
  model: function () {
    return this.modelFor('posts');
  }
});

PostRoute

App.PostRoute = Ember.Route.extend({
  model: function (params) {
    return this.modelFor('posts').findBy('slug', params.post_slug);
  },
  serialize: function (model, params) {
    return {
      post_slug: model.get('slug'), 
      post_id:   model.get('id')
    };
  },
});

One way to achieve this is to have a model specific adapter and implement your own urlForFindQuery like so

// app/adapters/post.js
import Ember from 'ember';
import ApplicationAdapter from './application';

var get = Ember.get;

export default ApplicationAdapter.extend({
    urlForFindQuery: function(query, modelName) {
        var slug = get(query, 'slug');

        if (slug) {
            delete query.slug;
            return this._buildURL(modelName, slug);

            // If you don't want to use a private method, you can do this
            // return this.urlForFind(slug, modelName);
        } else {
            return this._super(query, modelName);
        }
    }
});

So now you can do something like

import Ember from 'ember';

export default Ember.Route.extend({
    model: funciton (params) {
        return this.store.find('post', { slug: params.post_slug });
    }
});

and it will make request to your API, using the show route, for example /api/posts/awesome-slug

~ July 2016

Any new thoughts on this as of Ember 2.6?

We’ve got a work in progress PR for this for the tutorial here: add tutorial for sub-routes by sbatson5 · Pull Request #1443 · emberjs/guides · GitHub

That sounds fantastic. I’ve been trying to get some understanding over here: Dynamic segments other than id in Ember.js - Stack Overflow

I’ll see what I can glean from the PR.

Thanks.