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

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!)

Related

Ember: Update ObjectController property from ArrayController action?

Disclaimer: I'm quite new to Ember. Very open to any advice anyone may have.
I have a action in a ArrayController that should set an ObjectController property. How I can access the right context to set that property when creating a new Object?
Here is abbreviated app code show my most recent attempt:
ChatApp.ConversationsController = Ember.ArrayController.extend({
itemController: 'conversation',
actions: {
openChat: function(user_id, profile_id){
if(this.existingChat(profile_id)){
new_chat = this.findBy('profile_id', profile_id).get('firstObject');
}else{
new_chat = this.store.createRecord('conversation', {
profile_id: profile_id,
});
new_chat.save();
}
var flashTargets = this.filterBy('profile_id', profile_id);
flashTargets.setEach('isFlashed', true);
}
},
existingChat: function(profile_id){
return this.filterBy('profile_id', profile_id).get('length') > 0;
}
});
ChatApp.ConversationController = Ember.ObjectController.extend({
isFlashed: false
});
The relevant template code:
{{#each conversation in controller itemController="conversation"}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.profile}} Conversation: {{conversation.id}}</h3>
other stuff
</li>
{{/each}}
I don't see why you need an object that handles setting a property for all the elements in your list. Have each item take care of itself, this means components time.
Controllers and Views will be deprecated anyway, so you would do something like:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [...];
}
});
App.ConversationComponent = Ember.Component.extend({
isFlashed: false,
actions: {
// handle my own events and properties
}
});
and in your template
{{#each item in model}}
{{conversation content=item}}
{{/each}}
So, whenever you add an item to the model a new component is created and you avoid having to perform the existingChat logic.
ArrayController and ItemController are going to be depreciated. As you are new to Ember I think that it would be better for you not to use them and focus on applying to coming changes.
What I can advice you is to create some kind of proxy object that will handle your additional properties (as isFlashed, but also like isChecked or isActive, etc.). This proxy object (actually an array of proxy objects) can look like this (and be a computed property):
proxiedCollection: Ember.computed.map("yourModelArray", function(item) {
return Object.create({
content: item,
isFlashed: false
});
});
And now, your template can look like:
{{#each conversation in yourModelArray}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.content.profile}} Conversation: {{conversation.content.id}}</h3>
other stuff
</li>
{{/each}}
Last, but not least you get rid of ArrayController. However, you would not use filterBy method (as it allows only one-level deep, and you would have the array of proxy objects, that each of them handles some properties you filtered by - e.g. id). You can still use explicit forEach and provide a function that handles setting:
this.get("proxiedCollection").forEach((function(_this) {
return function(proxiedItem) {
if (proxiedItem.get("content.profile_id") === profile_id) {
return proxiedItem.set("isFlashed", true);
}
};
})(this));

Ember.js How to properly filter a model in a component

I have the following auto complete component:
Initial idea from EmberCasts: Building an Autocomplete Widget Part 1
App.AutoCompleteComponent = Ember.Component.extend({
searchText: null,
searchResults: function() {
var model = this.get('model');
var searchText = this.get('searchText');
console.log(this.get('model')); // shows array
if (searchText){
console.log('searching for: ' + searchText); // shows up in console with searchText
var regex = new RegExp(searchText, 'i');
model = model.filterBy('name', function(name) {
console.log(name); // never got reached
return name.match(regex);
});
}
return model;
}.property('searchText')
});
My template:
{{auto-complete model=controllers.categories}}
<script type="text/x-handlebars"s data-template-name="components/auto-complete">
{{input type="text" value=searchText placeholder="Search..."}}
<ul>
{{#each searchResults}}
<li>{{this}}</li>
{{/each}}
</ul>
</script>
The problem is, that no model item get returned. At the initial state of the program all my categories are shown - I will fix that soon. But it shows me that the auto-complete component does work. The model does get returned at first.
I think the FilterBy does not what I expect it should do.
I have tried to change the FilterBy part to this and search exactly for the name:
model = model.filterBy('name', searchText);
But that did also not work. Any ideas?
you're second approach is the correct one with filterBy, if you want to pass a function you would use filter.
model = model.filterBy('name', searchText);
I bet name doesn't exist on your models, or something along those lines. If you need more help show us an example of the categories model.
http://emberjs.jsbin.com/oTIxAjI/1/edit
You'll want to use filter
http://emberjs.jsbin.com/oTIxAjI/4/edit

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

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.

Display collection of elements, hide active element

UPDATE Since asking this question I have redesigned my UI such that I no longer need this feature; however I'm leaving this open and active for the sake of helping others who end up with a similar problem.
I'm listing a collection of elements inside a template and each element has a link that opens it up to the right of the list. When one is clicked, I want to hide just that element and show it again when another one is clicked. My current approach to doing this is to set an attribute (active) to true on the model. This feels wrong for three reasons:
This attribute is not actually part of the model's schema, it's just arbitrary; which makes it seem like a controller concern (see below for why that doesn't work)
I have to first set active to false on all models, forcing me to change another router's model, which may be good or bad, I'm not sure
In the recent PeepCode screencast he showed using #each.{attribute} to bind to an attributes in an array; this makes me feel like there must be something similar I could do (like this.set("#each.active", false)) to set them all in one fell swoop
I wanted to use a method on the controller but it doesn't seem I can pass arguments into functions in Handlebars if statements.
Here's the code I'm using to render the list:
{{#each note in controller}}
{{!v-- I was trying to do {{#if isCurrentNote note}} but that seems to be invalid handlebars}}
{{#unless note.active}}
<li class="sticky-list-item">
{{view Ember.TextArea classNames="sticky-note" valueBinding="note.content"}}
{{#linkTo note note classNames="sticky-permalink"}}
∞
{{/linkTo}}
</li>
{{/unless}}
{{/each}}
And here are the routes:
App.NotesController = Ember.ArrayController.extend({
// v-- this is what I was trying to do, but couldn't pass note in the template
isCurrentNote: function(note){
return this.get("currentNote") == note;
}.property("currentNote")
});
App.NoteRoute = Ember.Route.extend({
setupController: function(controller,model){
this.modelFor("notes").forEach(function(note){
note.set("active", false);
});
model.set("active", true);
}
});
Like I said, what I have works, but it feels wrong. Can anyone confirm my suspicion or help ease my soul a bit?
Thanks!
to me this looks like something that should be done mostly by the NotesView with a NotesController that stores the Note selection
Here is the fiddle: http://jsfiddle.net/colymba/UMkUL/6/
the NotesController would hold all the notes and a record of the selected one:
App.NotesController = Ember.ArrayController.extend({
content: [],
selectedNote: null,
selectNote: function(id){
var note = this.get('content').findProperty('id', id);
this.set('selectedNote', note);
}
});
with the NotesViewobserving that selection and showing/hiding elements of the list accordingly
App.NotesView = Ember.View.extend({
templateName: 'notes',
refresh: function(){
var view = this.$(),
selection = this.get('controller.selectedNote');
if (view) {
view.find('li').show();
if (selection) view.find('li.note_'+selection.id).hide();
}
}.observes('controller.selectedNote')
});
Here is the Note object and it's 2 templates (when in a list or displayed in full). The ListView handles the click event and passes the id to the NotesController.
App.Note = Ember.Object.extend({
name: '',
content: ''
});
App.NoteView = Ember.View.extend({
templateName: 'note'
});
App.NoteListItemView = Ember.View.extend({
tagName: 'li',
templateName: 'noteListItem',
classNameBindings: ['noteID'],
noteID: function(){
return 'note_' + this._context.id;
}.property(),
click: function(e){
this.get('controller').selectNote(this._context.id);
}
});
in the NotesView template everything is displayed and if there is a selectedNote, we display the Note again in full:
{{#each note in controller}}
{{#with note}}
{{view App.NoteListItemView}}
{{/with}}
{{/each}}
{{#if selectedNote}}
{{#with selectedNote}}
{{view App.NoteView}}
{{/with}}
{{/if}}
the Routes to put it together
App.Router.map(function() {
this.resource('notes', { path: "/notes" });
});
App.IndexRoute = Ember.Route.extend({
enter: function() {
this.transitionTo('notes');
}
});
App.NotesRoute = Ember.Route.extend({
model: function() {
return [
App.Note.create({id: 1, name: 'Milk', content: '15% fresh milk'}),
App.Note.create({id: 2, name: 'Juice', content: 'Orange with pulp'}),
App.Note.create({id: 3, name: 'Cereals', content: 'Kelloggs Froot Loops'}),
];
},
setupController: function(controller, model) {
controller.set('content', model);
},
renderTemplate: function(controller, model) {
this.render('notes', { outlet: 'content' });
}
});

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}}