How do I bind to the active class of a link using the new Ember router? - ember.js

I'm using Twitter Bootstrap for navigation in my Ember.js app. Bootstrap uses an active class on the li tag that wraps navigation links, rather than setting the active class on the link itself.
Ember.js's new linkTo helper will set an active class on the link but (as far as I can see) doesn't offer any to hook on to that property.
Right now, I'm using this ugly approach:
{{#linkTo "inbox" tagName="li"}}
<a {{bindAttr href="view.href"}}>Inbox</a>
{{/linkTo}}
This will output:
<li class="active" href="/inbox">Inbox</li>
Which is what I want, but is not valid HTML.
I also tried binding to the generated LinkView's active property from the parent view, but if you do that, the parent view will be rendered twice before it is inserted which triggers an error.
Apart from manually recreating the logic used internally by the linkTo helper to assign the active class to the link, is there a better way to achieve this effect?

We definitely need a more public, permanent solution, but something like this should work for now.
The template:
<ul>
{{#view App.NavView}}
{{#linkTo "about"}}About{{/linkTo}}
{{/view}}
{{#view App.NavView}}
{{#linkTo "contacts"}}Contacts{{/linkTo}}
{{/view}}
</ul>
The view definition:
App.NavView = Ember.View.extend({
tagName: 'li',
classNameBindings: ['active'],
active: function() {
return this.get('childViews.firstObject.active');
}.property()
});
This relies on a couple of constraints:
The nav view contains a single, static child view
You are able to use a view for your <li>s. There's a lot of detail in the docs about how to customize a view's element from its JavaScript definition or from Handlebars.
I have supplied a live JSBin of this working.

Well I took what #alexspeller great idea and converted it to ember-cli:
app/components/link-li.js
export default Em.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
active: function() {
return this.get('childViews').anyBy('active');
}.property('childViews.#each.active')
});
In my navbar I have:
{{#link-li}}
{{#link-to "squares.index"}}Squares{{/link-to}}
{{/link-li}}
{{#link-li}}
{{#link-to "games.index"}}Games{{/link-to}}
{{/link-li}}
{{#link-li}}
{{#link-to "about"}}About{{/link-to}}
{{/link-li}}

You can also use nested link-to's:
{{#link-to "ccprPracticeSession.info" controller.controllers.ccprPatient.content content tagName='li' href=false eventName='dummy'}}
{{#link-to "ccprPracticeSession.info" controller.controllers.ccprPatient.content content}}Info{{/link-to}}
{{/link-to}}

Building on katz' answer, you can have the active property be recomputed when the nav element's parentView is clicked.
App.NavView = Em.View.extend({
tagName: 'li',
classNameBindings: 'active'.w(),
didInsertElement: function () {
this._super();
var _this = this;
this.get('parentView').on('click', function () {
_this.notifyPropertyChange('active');
});
},
active: function () {
return this.get('childViews.firstObject.active');
}.property()
});

I have just written a component to make this a bit nicer:
App.LinkLiComponent = Em.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
active: function() {
return this.get('childViews').anyBy('active');
}.property('childViews.#each.active')
});
Em.Handlebars.helper('link-li', App.LinkLiComponent);
Usage:
{{#link-li}}
{{#link-to "someRoute"}}Click Me{{/link-to}}
{{/link-li}}

I recreated the logic used internally. The other methods seemed more hackish. This will also make it easier to reuse the logic elsewhere I might not need routing.
Used like this.
{{#view App.LinkView route="app.route" content="item"}}{{item.name}}{{/view}}
App.LinkView = Ember.View.extend({
tagName: 'li',
classNameBindings: ['active'],
active: Ember.computed(function() {
var router = this.get('router'),
route = this.get('route'),
model = this.get('content');
params = [route];
if(model){
params.push(model);
}
return router.isActive.apply(router, params);
}).property('router.url'),
router: Ember.computed(function() {
return this.get('controller').container.lookup('router:main');
}),
click: function(){
var router = this.get('router'),
route = this.get('route'),
model = this.get('content');
params = [route];
if(model){
params.push(model);
}
router.transitionTo.apply(router,params);
}
});

You can skip extending a view and use the following.
{{#linkTo "index" tagName="li"}}<a>Homes</a>{{/linkTo}}
Even without a href Ember.JS will still know how to hook on to the LI elements.

For the same problem here I came with jQuery based solution not sure about performance penalties but it is working out of the box. I reopen Ember.LinkView and extended it.
Ember.LinkView.reopen({
didInsertElement: function(){
var el = this.$();
if(el.hasClass('active')){
el.parent().addClass('active');
}
el.click(function(e){
el.parent().addClass('active').siblings().removeClass('active');
});
}
});

Current answers at time of writing are dated. In later versions of Ember if you are using {{link-to}} it automatically sets 'active' class on the <a> element when the current route matches the target link.
So just write your css with the expectation that the <a> will have active and it should do this out of the box.
Lucky that feature is added. All of the stuff here which was required to solve this "problem" prior is pretty ridiculous.

Related

Ember.js persist classNameBindings on transition to different routes

I'm fairly new to ember and I've been trying to tackle this problem for a couple of days but I can't seem to find a solution anywhere online.
I have a page with a list of all posts, each post has one tag (like a hashtag), either 'Fitness', 'Knowledge' or 'Social'. At the top of the page I have 3 view helpers and each view helper represents a tag (fitness, knowledge or social). These will be used to filter out the posts with that particular tag name.
My problem is that when I click on a view helper I toggle the "isSelected" property to true, which adds the "isSelected" class via classNameBindings. But when I transition to a different route on the site and come back, the "isSelected" property is reset back to false and the "isSelected" class has been removed. How do I keep these values persistent and in-tact for when I revisit the route?
Here's my code:
<script type="text/x-handlebars" data-template-name="global">
<ul class="categories">
<li>{{view App.Tag class="label fitness" text="fitness"}}</li>
<li>{{view App.Tag class="label knowledge" text="knowledge"}}</li>
<li>{{view App.Tag class="label social" text="social"}}</li>
</ul>
</script>
View:
"use strict";
App.Tag = Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.text}}'),
classNames: ['label'],
classNameBindings: ['isSelected'],
isSelected: false,
click: function () {
this.toggleProperty('isSelected');
}
});
I have also tried using a controller with actions but that way persisted the "isSelected" property but didn't preserve the addition of the class when I revisited the route.
This may not be ideal, but to save the state of the application, you can put the state in the controller. You probably had a simple implementation, but maybe did not specify the isSelected as a property. The below works and you can view the jsbin here
App = Ember.Application.create();
App.Router.map(function() {
this.route('global');
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.GlobalController = Ember.Controller.extend({
activeTags: Ember.A()
})
App.Tag = Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.text}}'),
classNames: ['label'],
classNameBindings: ['isSelected'],
isSelected: function () {
console.log("ON CHANGE", this.get('controller.activeTags'));
return this.get('controller.activeTags').contains(this.text);
}.property('controller.activeTags.#each'),
click: function () {
var tagArray = this.get('controller.activeTags');
if (tagArray.contains(this.text))
this.set('controller.activeTags', tagArray.without(this.text))
else
tagArray.pushObject(this.text);
}
});

How do I programmatically add child views to an Ember view at specific DOM selectors?

I have a view that uses a 3rd party library to render additional DOM elements in the didInsertElement hook. After these new elements are added, I need to add some child views inside them, so that they can render dynamic data.
Here's what I tried:
App.MyView = Ember.View.extend({
didInsertElement: function() {
create3rdPartyDomElements();
var element = this.$('someSelector');
childView = this.createChildView(App.SomeViewClass, attributesDict);
childView.appendTo(element);
}
});
(jsbin: http://jsbin.com/idoyic/3)
This renders my views as expected, but gives the following assertion error with Ember RC 7: "You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead."
I have tried extending ContainerView, as advised here and that works, but I have no way of inserting the child views at specific DOM selectors. It just inserts the child views at the beginning of the parent view.
Can someone please help me? Thanks a lot!
This is how I created:
An implementation where you have the main view, in that case codemirror, in the middle. And it's possible add more views, in the top or bottom.
App.EditorView = Ember.View.extend({
templateName: 'editor-view',
topView: Ember.ContainerView.extend(),
bottomView: Ember.ContainerView.extend(),
CodeMirrorView: Ember.TextArea.extend({
didInsertElement: function() {
this.codeMirror = CodeMirror.fromTextArea(this.get('element'));
}
})
});
The template:
<script type="text/x-handlebars" data-template-name="editor-view">
{{view view.topView viewName="topViewInstance"}}
{{view view.CodeMirrorView}}
{{view view.bottomView viewName="bottomViewInstance"}}
</script>
A view to represent a custom component:
App.MyComponent = Ember.View.extend({
templateName: 'click-here',
message: null,
click: function() {
alert(this.get('message'));
}
});
The implementation:
App.MyEditorView = App.EditorView.extend({
didInsertElement: function() {
this._super();
this.get('topViewInstance').pushObject(App.MyComponent.create({ message: "Hello" }));
this.get('bottomViewInstance').pushObject(App.MyComponent.create({ message: "World" }));
}
});
With this is possible to create a new instance, or extend App.EditorView and insert more views in top or bottom. Because the topView and bottomView are Ember.ContainerViews, all views added will have the bindings, events, and other ember features.
Give a look in that jsbin to see it working http://jsbin.com/ucanam/686/edit
You can render child views into parent view's hidden div, and then detach and append them to arbitrary DOM elements in didInsertElement hook.
http://jsbin.com/qaqome/1/
For related issue (components instead of views) see also this question.
try adding a property in your view, something like this:
App.MyView = Ember.View.extend({
childViewsContainer: Em.ContainerView.create({}),
didInsertElement: function() {
create3rdPartyDomElements();
var element = this.$('someSelector');
childViewsContainer.createChildView(App.SomeViewClass, attributesDict);
childView.appendTo(element);
}
});
then, you can access your childViewsContainer and do what ever you want with it

How to access the current item of an each block in the controller for a view

Given this controller:
ItemController= Ember.Controller.extend({
subItems: Ember.ArrayController.create({
content: App.store.find(App.models.SubItem),
sortProperties: ['name']
}),
currentItemIdBinding: 'App.router.mainController.currentItemId',
item: function() {
return App.store.find(App.models.SubItem, this.get('currentItemId'));
}.property('currentItemId'),
currentSubItems: function () {
return this.get('subItems.content')
.filterProperty('item_id', this.get('item.id'));
}.property('item', 'subItems.#each')
});
and this each block in the template:
{{#each subItem in currentSubItems}}
{{view App.SubItemView}}
{{/each}}
How would I gain access to the "subItem" in the controller for the SubItemView?
Edit:
I stumbled upon a way to do this. If I change the each block slightly:
{{#each subItem in currentSubItems}}
{{view App.SubItemView subItemBinding="subItem"}}
{{/each}}
and add an init method to the SubItemView class:
init: function() {
this._super();
this.set('controller', App.SubItemController.create({
subItem: this.get('subItem')
}));
})
I can get access to the subItem in the controller. This however just feels wrong on more levels than I can count.
Interesting...while browsing ember.js, I found this: https://stackoverflow.com/a/14251255/489116 . It's a little different from what you're asking, but may solve the problem: if I'm reading it correctly, it would automatically associate a subItemController with each subItemView and subItem, without you having to pass the model around. Not released yet though. I'd still like to see other solutions!
How about using Ember.CollectionView instead of {{each}} helper see the following:
App.SubItemsView = Ember.CollectionView.extend({
contentBinding: "controller.currentSubItems",
itemViewClass: Ember.View.extend({
templateName: "theTemplateYouUsedForSubItemViewInYourQuestion",
controller: function(){
App.SubItemController.create({subItem: this.get("content")});
}.property()
})
})
Use it in handlebars as follows
{{collection App.SubItemsView}}

Using functions for binding element class names in Ember templates?

When I have a template similar to:
{{#view App,NavItemView}}
<li {{bindAttr class="isActive:active"}}>Item 1</li>
{{/view}}
And a view of
App.NavItemView = Ember.View.extend({
tagName: 'ul',
isActive: function() {
return false;
}
});
The rendered template will always render the class of 'active' on the li element. So based upon this it doesn't seem possible to have a conditional class set?
Ideally I would like the class of the li element to be turned on and off based upon the result of the function. Am I missing something?
You need to use computed properties for this sort of thing.
App.NavItemView = Ember.View.extend({
tagName: 'ul',
isActive: function() {
return false;
}.property()
});
Check out the computed properties guide for more details.

What's the best idiom for creating an EmberJS view that can show all its child-views, or just one?

Say I have a model App.Page, an ArrayController of App.Page, and an App.PageView to render each App.Page.
I'm trying to figure out how to best implement App.MyPagesView so it works like so:
if App.showAllPages is true: I want MyPagesView to contain an App.PageView(s) for displaying each of the App.Page in App.pages
Else: I want MyPagesView only show one App.PageView, bound to App.pages.currentPage.
The most straightforward implementation that occurs to me is using a template like so:
// MyPagesViewApproach1
{{#unless App.showAllPages}}
{{view App.PageView pageBinding="pages.currentPage"}}
{{else}}
{{#each pages}}
{{view App.PageView pageBinding="this"}}
{{/each}}
{{/unless}}
But won't this create new views for the existing models every time the user toggles showAllPages on and off? Also, I get emberJS warnings about performance issues when I try to use this template.
The PageView(s) could be quite complex and expensive to render. I'd really like to create a PageView once for each Page, and just remove/hide the irrelevant PageViews from the DOM when they're not in use.
App = Ember.Application.create({
showAllPages: false,
pages: Ember.ArrayController.create({
content: []
currentPage: null
}),
ready: function () {
this.pages.pushObject(App.Page.create({title: 'Page One'});
this.pages.pushObject(App.Page.create({title: 'Some Other Page'});
this.pages.pushObject(App.Page.create({title: 'Grrreatest Page Evar'});
this.pagesController.set('currentPage',
this.pagesController.get('firstObject'));
}
});
App.Page = Ember.Object.extend({
title: null
// etc, etc...
});
App.PageView = Ember.View.extend({
templateName: 'page',
page: null // should be bound to an App.Page
});
App.MyPagesView_Approach1 = Ember.View.extend({
pagesBinding: 'Elicitation.pages'
// ???
});
App.MyPagesView_Approach2 = Ember.ContainerView.extend({
// ???
});
And my HTML:
<script type="text/x-handlebars" data-template-name="page">
The title of this page is {{ page.title }}
</script>
{{view App.MyPagesView }}
To recap, what's the proper EmberJS-y way to implement MyPagesView so it responds to App.showAllPages without re-creating all the views each time its toggled?
Should it be some sort of ContainerView? Or should I use the unless/else template shown at the top of the question? Or something entirely different? I feel like a really simple solution exists in EmberJS, but its elluding me.
Here's the best I've come up with, encapsulated as a re-usable View class called "CurrentCollectionView". I'm using CollectionView, and using view.set('isVisible') to hide/show appropriate child views. Basically use it like a CollectionView, but you can set currentContent to hide all but one element of content, or use showAllContent to override currentContent.
App.CurrentCollectionView = Ember.CollectionView.extend({
showAllContent: false,
currentContent: null,
currentContentChanged: function () {
console.log("Elicitation.PagesView.currentContentChanged()");
var showAllContent = this.get('showAllContent');
if (Ember.none(showAllContent) || !showAllContent) {
var contents = this.get('content');
var currentContent = this.get('currentContent');
this.get('childViews').forEach(function (view, i) {
var isVisible = contents.objectAt(i) == currentContent;
view.set('isVisible', isVisible);
});
} else {
this.get('childViews').forEach(function (view) {
view.set('isVisible', true);
});
}
}.observes('currentContent', 'showAllContent', 'childViews')
});
An example of using CurrentCollectionView to implement MyPagesView:
App.MyPagesView = App.CurrentCollectionView.extend({
itemViewClass: App.PageView,
contentBinding: 'App.pages',
currentContentBinding: 'App.pages.currentPage',
showAllContentBinding: 'App.showAllPages',
});
or as using it inline as a template:
{{view App.CurrentCollectionView itemViewClass="App.PageView" contentBinding="App.pages" currentContentBinding="App.pages.currentPage" showAllContentBinding="App.showAllPages"}}
Hope somebody else finds this useful and/or can improve on it (please!)