1
votes

I am trying to figure out how to convert a route into a modal, such that you can navigate to it via any other route WHILE preserving underlying(previous) template.

For example:

http://example.com/site/index goes to index.hbs

http://example.com/site/page2 goes to page2.hbs

http://example.com/site/article/1234 goes to article.hbs if user comes from another domain(fresh start)

BUT http://example.com/site/article/1234 opens up article.hbs inside the "article-modal" outlet if user comes any other route.

Here is the router.js

Market.Router.map(function() {
this.route('index', { path: '/' });
this.route('start', { path: 'start' });
this.route('article', { path: 'article/:article_id' });
this.route('404', { path: '*:' });
});

here is application.hbs

<div class="main-container">{{outlet}}</div>
{{outlet "article-modal"}}

and here is article.js route Alternative case #1

Em.Route.extend({
beforeModel: function(transition, queryParams) {
    if(!Em.isEmpty(this.controllerFor('application').get('currentRouteName'))) {
        this.render('article', {
    into: 'application',
    outlet: 'article-modal'
  });
        return Em.RSVP.reject('ARTICLE-MODAL');
    }
},

model: function(params) {
    return this.store.find('article', params.id);
},

actions: {
error: function(reason) {
        if(Em.isEqual(reason, 'ARTICLE-MODAL')) { // ARTICLE-MODAL errors are acceptable/consumed
            //
            return false;
        }
        return true;
}
}
});

and here is article.js route Alternative case #2

Em.Route.extend({

renderTemplate: function() {
    if(!Em.isEmpty(this.controllerFor('application').get('currentRouteName'))) {
        this.render({into: 'index', outlet: 'article-modal'});
    } else {
        this.render({into: 'application'});
    }

},

model: function(params) {
    return this.store.find('product', params.id);
},
});

Problem with case #1 is that browser address bar does not reflect current route. If user goes from index route to article route the browser address bar still shows /index.. So if he presses back button app breaks.

Problem with case #2 is that it discards the contents of index.hbs because the article route is not nested.

Is it possible to even have such functionality with Ember?

Thanks :)

2

2 Answers

2
votes

This is my second answer to this question. My original answer (https://stackoverflow.com/a/27947475/1010074) didn't directly answer OP's question, however, I outlined three other approaches to handling modals in Ember in that answer and am leaving it there in case it's helpful to anyone else.

Solution: define multiple routes with the same path

While Ember doesn't usually allow you to define two routes that use the same path, you can actually have a nested route and an un-nested route with the same effective path, and Ember works with it just fine. Building off of option 3 from my original answer, I have put together a proof of concept that I think will work for you. Here's a JS Fiddle: http://jsfiddle.net/6Evrq/320/

Essentially, you can have a router that looks something like this:

App.Router.map(function () {
    this. resource("index", {path: "/"}, function(){ 
        this.route("articleModal", {path: "/article"});    
    });
    this.route("article", {path: "/article"});
});

And within your templates, link to the index.articleModal route:

{{#link-to "index.articleModal"}}View article!{{/link-to}}

Since articleModal renders inside of index, your index route isn't un-rendered. Since the URL path changes to /article, a reload of the page will route you to your regular Article route.

Disclaimer: I am unsure if this is exploiting a bug in current Ember or not, so mileage here may vary.

1
votes

Edit: Just re-read OP's question and realized I didn't understand his question, so I created a new answer (https://stackoverflow.com/a/27948611/1010074) outlining another approach that I came up with after experimenting with something.

Option 1: Ember's suggested method for handling Modals

The Ember website has a "cookbook" for how they recommend handling modal dialogs: http://emberjs.com/guides/cookbook/user_interface_and_interaction/using_modal_dialogs/

Essentially, you would create an action in a route that opens the modal:

App.ApplicationRoute = Ember.Route.extend({
  actions: {
    openArticleModal: function(article) {
      return this.render(article, {
        into: 'application',
        outlet: 'modal',
        controller: this.controllerFor("article")
      });
    }
  }
});

And then call this.send("openArticleModal", article) either from your controller / another route or you could do something like <button {{action "openArticleModal" article}}>View Artice</button> in your template.

Essentially this method takes the modal out of a routed state, which means the modal won't be URL bound, however if you need to be able to open the modal from anywhere in the app and not un-render the current route, then it's one of your few options.

Option 2: If you need URL-bound modals that can be opened from anywhere

For a current project, I have done something that works for this use case by using query params. To me, this feels a little hacky, but it works fairly well in my tests so far (others in the community - if you have opinions on this, please let me know). Essentially, it looks like this:

App.ApplicationController = Ember.Controller.extend({
   queryParams: ["articleId"],
   articleId: null, 

   article: function() {
      if(!this.get("articleId") return null;
      return this.get("store").find("article", this.get("articleId"));
   }
});

In application.hbs:

{{#if article.isFulfilled}}
    {{render "articleModal" article.content}}
{{/if}}

Then I can use normal {{link-to}} helpers and link to the query param:

{{#link-to (query-params articleId=article.id)}}View Article{{/link-to}}

This works, but I'm not entirely happy with this solution. Something slightly cleaner might be to use an outlet {{outlet "article-modal"}} and have the application route render into it, but it might take more LOC.

Option 3: If the modal is only ever opened from one route

You can make the route that the modal will open into a parent of the modal route. Something like this:

Market.Router.map(function() {
    this.resource('articles', { path: '/articles' }, function() {
        this.route('modal', { path: '/:article_id' }); 
    });
});

This works well if your modal can only "open" from within a single route. In the example above, the modal will always open on top of the articles route, and if you link-to the modal route from anywhere else in the app, the articles route will render underneath the modal. Just make sure that the "close" action of your modal transitions you out of the modal route, so a user can't close your modal but and still be on the modal route.