I want to display a crumble path with Ember. How can I iterate through the current path?
In my opinion there are two approaches:
The ember-way
EDIT: see my answer below
I keep this question up-to-date with the current status of displaying breadcrumbs. You can browse through the revisions of this question to see the history.
There are a couple of goals here:
Listen on route change
Finding current route
displaying list of the current route
display working links to the steps in the route
Controller
App.ApplicationController = Ember.Controller.extend({
needs: ['breadcrumbs'],
currentPathDidChange: function() {
path = this.get('currentPath');
console.log('path changed to: ', path);
this.get('controllers.breadcrumbs').set('content',this.get('target.router.currentHandlerInfos'));
}.observes('currentPath')
});
App.BreadcrumbsController = Em.ArrayController.extend({});
Router
App.ApplicationRoute = Ember.Route.extend({
renderTemplate: function() {
this.render();
this.render('breadcrumbs', {
outlet: 'breadcrumbs',
into: 'application',
controller: this.controllerFor('breadcrumbs')
});
}
});
Template
{{! application template }}
<div class="clearfix" id="content">
{{outlet "breadcrumbs"}}
{{outlet}}
</div>
{{! breadcrumbs template }}
<ul class="breadcrumb">
{{#each link in content}}
<li>
<a {{bindAttr href="link.name"}}>{{link.name}}</a> <span class="divider">/</span>
</li>
{{/each}}
</ul>
The current problems to tackle are:
When I go to the URL: #/websites/8/pages/1 the output for the breadcrumbs is: (I removed all the script-tag placeholders
<ul class="breadcrumb">
<li>
application <span class="divider">/</span></li>
<li>
sites <span class="divider">/</span>
</li>
<li>
site <span class="divider">/</span>
</li>
<li>
pages <span class="divider">/</span>
</li>
<li>
page <span class="divider">/</span>
</li>
<li>
page.index <span class="divider">/</span>
</li>
</ul>
The URL's should be a valid route
The menu is now hardcoded with {{#linkTo}} to the routes, I tried to make that dynamic, like here but a transitionTo doesn't trigger the currentPath-observer
The other way
Most is the same as above, but there are a couple of differences. The breadcrumbs are made by looping over location.hash instead of getting it from the Router.
The ApplicationController becomes:
ApplicationController = Ember.Controller.extend({
needs: ['breadcrumbs'],
hashChangeOccured: function(context) {
var loc = context.split('/');
var path = [];
var prev;
loc.forEach(function(it) {
if (typeof prev === 'undefined') prev = it;
else prev += ('/'+it)
path.push(Em.Object.create({ href: prev, name: it }));
});
this.get('controllers.breadcrumbs').set('content',path)
}
});
ready : function() {
$(window).on('hashchange',function() {
Ember.Instrumentation.instrument("hash.changeOccured", location.hash);
});
$(window).trigger('hashchange');
}
We need to subscribe the custom handler in the ApplicationRoute
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller, model) {
Ember.Instrumentation.subscribe("hash.changeOccured", {
before: function(name, timestamp, payload) {
controller.send('hashChangeOccured', payload);
},
after: function() {}
});
}
});
So far the alternative approach is working best for me, but it's not a good way of doing it because when you configure your Router to use the history instead of location.hash this method won't work anymore.
Based on your current breadcrumb output I guess you have an error in your router.
The following command should return array with current breadcrumb:
App.get('Router.router.currentHandlerInfos');
Your router should be nested:
this.resource('page 1', function () {
this.resource('page 2');
});
You can use #linkTo instead of a tag in your breadcrumb, you will get active class for free.
I came up with a much simpler solution that I posted to the Ember discourse.
I found a (Ember-way) solution to display breadcrumbs. It is based on the router instead of my location.hash.
Infrastructure
First we need to make the infrastructure for the breadcrumbs before we add or remove items from the breadcrumbs array.
Menu
In my app.js I define a NavItem-object. This is a skeleton for all navigatable items. I use it to define my menu-items, but we are also going to use it for the breadcrumbs.
App.NavItem = Em.Object.extend({
displayText: '',
routeName: ''
});
// define toplevel menu-items
App.dashboardMenuItem = App.NavItem.create({
displayText: 'Dashboard',
routePath: 'dashboard',
routeName: 'dashboard'
});
App.sitesMenuItem = App.NavItem.create({
displayText: 'Websites',
routePath: 'sites.index',
routeName: 'sites'
});
Controllers
We need a BreadcrumbsController to keep the breadcrumbs in a central place
App.BreadcrumbsController = Em.ArrayController.extend({
content: []
});
My ApplicationController depends on the BreadcrumbsController
App.ApplicationController = Ember.Controller.extend({
needs: ['breadcrumbs']
});
The BreadcrumbsView is a subview of ApplicationView
Views
App.ApplicationView = Ember.View.extend({
BreadcrumbsView: Ember.View.extend({
templateName: 'breadcrumbs',
init: function() {
this._super();
this.set('controller', this.get('parentView.controller.controllers.breadcrumbs'));
},
gotoRoute: function(e) {
this.get('controller').transitionToRoute(e.routePath);
},
BreadcrumbItemView: Em.View.extend({
templateName:'breadcrumb-item',
tagName: 'li'
})
})
});
Templates
In my application-template I output the breadcrumbsview above the outlet
{{view view.BreadcrumbsView}}
{{outlet}}
I'm using Twitter Bootstrap so my markup for my breadcrumbs-template is
<ul class="breadcrumb">
{{#each item in controller.content}}
{{view view.BreadcrumbItemView itemBinding="item"}}
{{/each}}
</ul>
The breadcrumb-item-template
<a href="#" {{action gotoRoute item on="click" target="view.parentView"}}>
{{item.displayText}}
</a> <span class="divider">/</span>
Routing
We need to respond to the routing in our app to update the breadcrumbs.
When my SitesRoute (or any other toplevel route) is activated, we push the NavItem to the Breadcrumbs, but I also want to do that with the rest of my toplevel routes, so I first create a TopRoute
App.TopRoute = Em.Route.extend({
activate: function() {
this.controllerFor('menu').setActiveModule(this.get('routeName'));
var menuItem = app.menuItems.findProperty('routeName',this.get('routeName'));
this.controllerFor('breadcrumbs').get('content').pushObject(menuItem);
},
deactivate: function() {
var menuItem = app.menuItems.findProperty('routeName',this.get('routeName'));
this.controllerFor('breadcrumbs').get('content').removeObject(menuItem);
}
});
All my toproutes extend from this route, so the breadcrumbs are automatically updatet
App.SitesRoute = App.TopRoute.extend();
For deeper levels it works almost the same, all you have to do is use the activate and deactivate hooks to push/remove objects from the Breadcrumbs
App.SiteRoute = Em.Route.extend({
activate: function() {
var site = this.modelFor('site');
this.controllerFor('breadcrumbs').get('content').pushObject(app.NavItem.create({
displayText: site.get('name'),
routePath: 'site',
routeName: this.get('routeName')
}));
},
deactivate: function() {
var site = this.modelFor('site');
this.controllerFor('breadcrumbs').get('content').removeAt(1);
}
});
Related
I'm trying to create something like a carousel in Ember, where the current item shown is controlled by one of four buttons on the page (for simplicity's sake, I'm only showing two buttons). I'm currently implementing this using outlets and routes, but I'd much prefer to have I have this work without altering the route. Here's how I'm currently going about this:
<!-- index.hbs (snipped, simplified) -->
<div>
{{outlet highlights}}
</div>
<!-- buttons for controlling outlet content (only showing 2 for now) -->
<div>
<a class="button" href="#" {{action "showHighlight" "placeholder1"}}>
Highlight 1
</a>
<a class="figcaption" href="#" {{action "showHighlight" "placeholder2"}}>
Highlight 2
</a>
</div>
My index controller handles the action that's called when the buttons are pressed:
App.IndexController = Ember.ArrayController.extend({
highlights: ['placeholder1', 'placeholder2'],
actions: {
showHighlight: function(highlight) { // render a new view into
this.transitionToRoute('/highlights/' + highlight);
}
}
});
My routes for the highlights:
App.Router.map(function() {
this.resource('highlights', function() {
this.route('placeholder1', { path: '/placeholder1' });
this.route('placeholder2', { path: '/placeholder2' });
this.route('placeholder3', { path: '/placeholder3' });
this.route('placeholder4', { path: '/placeholder4' });
});
});
For each placeholder route:
App.Placeholder1Route = Ember.Route.extend({
renderTemplate: function() {
this.render({
into: 'index',
outlet: 'highlights'
});
}
});
Is there a better way to do this, without having to use routes?
I ended up solving this by handling the action in the route and choosing the proper template to render:
App.IndexRoute = Ember.Route.extend({
actions: {
showHighlight: function(name) {
var highlightTemplate = 'highlight_' + name;
this.render(highlightTemplate, {
into: 'index',
outlet: 'highlights'
}
}
}
});
Using this technique, you don't need to specify any routes for the highlights and you don't need an IndexController (unless you want one for handling other events).
I am using EmberJs version 1.4 and I have the following set of templates and routes. The idea is that when the user goes to the "Widgets" route the returned model is only a collection of Widget Ids and Widget names to create the links and then when the user clicks on a link a call to a service will get all of the selected widget data to be displayed on the "Widget" template.
JavaScript code
window.Awesome = Ember.Application.create();
Awesome.Router.map(function() {
this.resource("awesome", {path: "/"}, function(){
this.route('login');
});
this.resource("widgets", function () {
this.resource('widget', { path: '/:widgetId' }, function () {
this.route('general', { path: 'info' });
this.route('configuration');
this.route('operations');
})
});
});
Awesome.WidgetsRoute = Awesome.AuthenticationRoute.extend({
model: function(){
//TODO: Call a service to get the model.
return { widgets: [{ widgetId: 1, widgetName: "Great Widget" }, { widgetId: 2, widgetName: "Fantastic Widget" }, { widgetId: 3, widgetName: "Brutal Widget" }] };
}
});
Awesome.WidgetIndexRoute = Awesome.AuthenticationRoute.extend({
model: function (params) {
var receivedWidgetId = params.widgetId;
return { widgetName: "Hardcoded Widget", widgetId: receivedWidgetId };
}
});
Handlebars templates
<script type="text/x-handlebars" data-template-name="widgets">
<section class="span3 left-section">
<div class="btn-group-vertical btn-group-justified registration-actions-menu">
<button id="createNewWidget" class="btn btn-link">Create New Widget</button>
<button id="joinWidgetTeam" class="btn btn-link">Join Widget Team</button>
</div>
<div class="registered-widgets-menu">
<div class="btn-group-vertical">
{{#each widget in widgets}}
{{#link-to 'widget' widget class="btn btn-link"}}{{widget.widgetName}}{{/link-to}}
{{/each}}
</div>
</div>
</section>
<section class="span8">
{{outlet}}
</section>
</script>
<script type="text/x-handlebars" data-template-name="widget">
<div id="widgetOptions">
<!-- TODO: Change the anchors for handlebars link-to helpers. -->
<h1>{{widgetName}}</h1> <h5>{{widgetId}}</h5>
<ul id="widgetNavigation">
<li>Widget Info</li>
<li>Widget Configuration</li>
<li>Widget Operations</li>
</ul>
</div>
<div id="widgetContent">
<!-- TODO: Design some awesome widget content. -->
Some awesome widget content
</div>
</script>
The thing I do not understand is why when I click on any of the widget links from the "Widgets" template and the "Widget" template is displayed, even though I can see that the model hook on the WidgetIndexRoute gets executed, the displayed widgetName is not the hard coded one but the one that was selected on the list which leads me to believe that even though I would call a service to get additional data, this data would not be available for the template.
The other thing I do not understand is that when I debug the code the params.widgetId is undefined but when I try running the url with an arbitrary value, said value is displayed on the template but the widgetName is empty.
Any help is appreciated.
Out of completeness just in case this might have anything to do with it, both routes are extending this other one to support authentication:
Awesome.AuthenticationRoute = Ember.Route.extend({
beforeModel: function(transition){
if(!Awesome.get('loggedUser')){
this.redirectToLogin(transition);
}
},
redirectToLogin: function(transition) {
var loginController = this.controllerFor('awesome.login');
loginController.set('attemptedTransition', transition);
this.transitionTo('awesome.login');
}
});
Please check out this post at Ember Blog about new version of Ember (1.5):
ROUTES INHERIT MODEL
Ember routes and leaf resources (without nested routes) will inherit the parent route's model.
Take the following example:
App.Router.map(function(){
this.resource('post', function(){
this.route('edit');
});
});
App.PostRoute = Ember.Route.extend({
model: function(){
return {title: 'ZOMG', text: 'AWESOME'};
}
});
App.PostEditRoute = Ember.Route.extend({
model: function(){
return this.modelFor('post');
}
});
Now in 1.5, you do not have to define the model hook for PostEditRoute as the default implementation is to use the parent routes model.
I have a list of matches, and when I click one, I want to display the match. I know that I can do a Master-Detail style page, where when I click one, I can see the outlet somewhere on the same page, but that is not what I want.
I want it so that when I click on a link, it goes to an entirely new page for the match. I'm not really sure how to go about doing that.
Here is my route for #/matches (in coffeescript)
App.MatchesRoute = Ember.Route.extend(
model: ->
App.Match.find()
)
Here is my matches.handlebars
<div id="matches">
{{#each match in controller}}
{{#linkTo "match" match class="panel six columns"}}
Match between {{match.player.name}} and {{match.opponent.name}}
{{/linkTo}}
<br />
{{/each}}
</div>
// I know that if I have this outlet, it will render `match.handlebars`
// right here, but I want it to be it's own page.
// {{outlet}}
I've only been working with Ember for a few days, and all of the examples I've found use Master-Detail views.
Please let me know of any other information I can provide from my code.
<Edit date="March 11th 2013">
I've pushed a this repository in GitHub. This is a conceptual app that uses renderTemplate somewhat the way you're describing.
</Edit>
In your child route, use the renderTemplate hook in order to tell your application to render a specific template in a specific {{outlet}}. Example:
Source Fiddle
App.Router.map(function() {
this.resource('matches', { path: 'matches' }, function() {
this.route('match', { path: 'match/:match_id' });
});
});
App.MatchesRoute = Em.Route.extend({
model: function() {
return App.Match.find();
},
setupController: function(controller, model) {
model = App.Match.find();
controller.set('content', model);
},
renderTemplate: function() {
this.render('matches', {
into: 'application'
})
}
});
App.MatchesMatchRoute = Em.Route.extend({
model: function(params) {
return App.Match.find(params.match_id);
},
setupController: function(controller, model) {
controller.set('content', model);
},
renderTemplate: function() {
this.render('match', {
into: 'application'
})
}
});
This MatchesMatchRoute is setup to render its template (matches/match) into the application template. And since there is only one {{outelet}} this template (see handlebars below), we don't have to specify anything:
<script type="text/x-handlebars">
<h1>App</h1>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="matches">
<h2>Matches</h2>
<ul>
{{#each match in controller}}
<li>
{{#linkTo matches.match match}}
{{match.title}}
{{/linkTo}}
</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="match">
<h3>{{title}}</h3>
<p>{{description}}</p>
</script>
If you have a scenario with multiple outlets, you have to hame them, like in the handlebars below:
<script type="text/x-handlebars">
<h1>App</h1>
{{outlet main}}<br />
{{outlet nested}}
</script>
Then your routes will have to specify the outlet as well. Example:
Source Fiddle
[...route code...]
renderTemplate: function() {
this.render('content', {
into: 'application',
outlet: 'main'
});
this.render('buttons', {
into: 'application',
outlet: 'nested'
});
}
[...route code...]
You can cause a template to render into a different template's outlet by using the renderTemplate hook when defining the route (see the guide: http://emberjs.com/guides/routing/rendering-a-template/)
For your example it might look like this:
App.MatchRoute = Ember.Route.extend({
renderTemplate: function() {
this.render({ into: 'matches' });
}
});
I've got an app with basic functionality built out. I'm not going through and adding additional features. In this case I need to convert a simple button, currently using linkTo, to a View. Problem is that I'm not sure how to convert one to the other and still keep the link intact.
How do I do this conversion? Here's the code I have now:
<script type="text/x-handlebars" data-template-name="accountItem">
{{#each account in controller}}
{{#linkTo "account" account}}
<img {{bindAttr src="account.icon"}} />
{{/linkTo}}
{{/each}}
</script>
and here's the code I'm going to have:
<script type="text/x-handlebars" data-template-name="accountItem">
{{#each account in controller}}
{{#view "Social.AccountButtonView"}}
<img {{bindAttr src="account.icon"}} />
{{/view}}
{{/each}}
</script>
Social.AccountButtonView = Ember.View.extend({
tagName: 'a',
classNames: ['item-account'],
click: function(){
// do something
}
});
I would assume that I'd be building on top of the click handler in the View, but I'm not sure how to pass the reference to item being iterated over, nor how to reference the correct route within the View.
Assistance please?
Update 1
The first version renders an href attribute with a value of #/accounts/4 based on the Router I have set up:
Social.Router.map(function() {
this.resource('accounts', function(){
this.resource('account', { path: ':account_id'});
});
});
When I convert the current code to a view, how do I mimic the functionality that linkTo provides?
You can define a property binding for account in your handlebars template.
This binding works like this:
<script type="text/x-handlebars">
<h1>App</h1>
{{#each item in controller}}
{{#view App.AccountView accountBinding="item"}}
<a {{bindAttr href="view.account.url"}} target="_blank">
{{view.account.name}}
</a>
{{/view}}
{{/each}}
</script>
Note that I added accountBinding, so the general rule is propertyName and Binding as a suffix. And remember that when you add a property to a view, you will not be able to access it directly, instead you will have to access it with view.propertyName as shown above.
Just keep in mind that you must have a View class when using the {{view}} helper:
window.App = Em.Application.create();
App.AccountView = Em.View.extend(); // this must exist
App.ApplicationRoute = Em.Route.extend({
model: function() {
return [
{id: 1, name: 'Ember.js', url: 'http://emberjs.com'},
{id: 2, name: 'Toronto Ember.js', url: 'http://torontoemberjs.com'},
{id: 3, name: 'JS Fiddle', url: 'http://jsfiddle.com'}];
}
})
Working fiddle: http://jsfiddle.net/schawaska/PFxHx/
In Response to Update 1:
I found myself in a similar scenario, and ended up creating a child view to mimic the {{linkTo}} helper. I don't really know/think it's the best implementation tho.
You can see my previous code here: http://jsfiddle.net/schawaska/SqhJB/
At that time I had created a child view within the ApplicationView:
App.ApplicationView = Em.View.extend({
templateName: 'application',
NavbarView: Em.View.extend({
init: function() {
this._super();
this.set('controller', this.get('parentView.controller').controllerFor('navbar'))
},
selectedRouteName: 'home',
gotoRoute: function(e) {
this.set('selectedRouteName', e.routeName);
this.get('controller.target.router').transitionTo(e.routePath);
},
templateName: 'navbar',
MenuItemView: Em.View.extend({
templateName:'menu-item',
tagName: 'li',
classNameBindings: 'IsActive:active'.w(),
IsActive: function() {
return this.get('item.routeName') === this.get('parentView.selectedRouteName');
}.property('item', 'parentView.selectedRouteName')
})
})
});
and my Handlebars looks like this:
<script type="text/x-handlebars" data-template-name="menu-item">
<a {{action gotoRoute item on="click" target="view.parentView"}}>
{{item.displayText}}
</a>
</script>
<script type="text/x-handlebars" data-template-name="navbar">
<ul class="left">
{{#each item in controller}}
{{view view.MenuItemView itemBinding="item"}}
{{/each}}
</ul>
</script>
I'm sorry I can't give you a better answer. This is what I could come up with at the time and haven't touched it ever since. Like I said, I don't think this is the way to handle it. If you are willing to take a look into the {{linkTo}} helper source code, you'll see a modular and elegant implementation that could be the base of your own implementation. I guess the part you're looking for is the href property which is being defined like so:
var LinkView = Em.View.extend({
...
attributeBindings: ['href', 'title'],
...
href: Ember.computed(function() {
var router = this.get('router');
return router.generate.apply(router, args(this, router));
})
...
});
So I guess, from there you can understand how it works and implement something on your own. Let me know if that helps.
How do I pass a route name to a {{linkTo}} dynamically?
For example, given this code:
App.Router.map(function() {
this.resource('anon', {path: '/main'},
function() {
this.route('home', {path:'/home'});
this.route('about', { path: '/about' });
this.route('contact', { path: '/contact' });
});
});
App.NavController = Ember.ArrayController.extend({
selectedNav:'',
setNav:function(value){
var nav = App.Nav.find(value);
var items = nav.get('navItems');
this.set('content', items);
}
});
these templates:
<script type="text/x-handlebars" data-template-name="nav">
<ul class="nav">
{{#each in controller}}
{{ partial "basicNav"}}
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="_basicNav">
<li>{{#linkTo navItemPath}}{{navItemName}}{{/linkTo}}</li>
</script>
and these models with the following fixture data:
App.Nav = DS.Model.extend({
navItems:DS.hasMany('App.NavItem'),
name:DS.attr('string')
});
App.NavItem = DS.Model.extend({
nav:DS.belongsTo('App.Nav'),
navItemName:DS.attr('string'),
navItemPath:DS.attr('string')
});
App.Nav.FIXTURES = [
{
id: 10,
name: 'Anon',
navItems: [100,200,300]
}
];
App.NavItem.FIXTURES = [
{
id:100,
nav:10,
navItemName:'Home',
navItemPath:'anon.home'
},
{
id:200,
nav:10,
navItemName:'Contact',
navItemPath:'anon.contact'
},
{
id:300,
nav:10,
navItemName:'About',
navItemPath:'anon.about'
}
];
How do I pass navItemPath to the {{linkTo}} helper? In this code snippet:
{{#linkTo navItemPath}}{{navItemName}}{{/linkTo}}
ember complains that it can't find the "navItemPath" route, like it's looking for it literally. If I replace that with a valid literal route like:
{{#linkTo 'anon.home'}}{{navItemName}}{{/linkTo}}
ember will render the linkTo with the navItemName as expected, so I know the controller is passing it the right data, but of course all the routes are goofy. Am I missing something obvious?
You can't do that with LinkTo helper, you need to bind the href of your link to navItemPath using bindAttr
<a {{bindAttr href="navItemPath"}}>{{navItemName}}</a>
Make sure the the logic rending navItemPath's value takes into account the location API