Ember.js best practices for adding subnav - ember.js

I was curious as to what the best way is to implement sub navigation in Ember. In application.hbs I have the main nav with 'about', 'programs', etc. When 'about' is clicked I would like another list of pages to display on the row directly below the main nav. I'm already using {{outlet}} inside of application.hbs so I can't put another where I want the subnav to display.
Here's what I have:
<div class="row left-nav">
{{#linkTo "index"}}<img class="ew_logo" src="assets/ew.png">{{/linkTo}}
</div>
<div class="row nav">
<div class="large-12 colummns">
<ul class="inline-list top-nav">
<li><h6>ABOUT</h6></li>
<li><h6>//</h6></li>
<li><h6>CONDITIONS</h6></li>
<li><h6>//</h6></li>
<li><h6>PROGRAMS</h6><li>
<li><h6>//</h6></li>
<li><h6>TESTIMONIALS</h6></li>
</ul>
</div>
</div>
<div class="row subnav">
<div class="large-12 colummns">
// Would like SUBNAV to display here
</div>
</div>
{{outlet}}

You could use a named outlet and hook into the renderTemplate function of your route to render it into the right place, see here for a simple demo. (sorry, but the styles are obviously screwed)
Example if your named outlet is inside your index template then you could:
Index Template
...
<div class="row subnav">
<div class="large-12 colummns">
{{outlet 'subnav'}}
</div>
</div>
...
Subnav template
<script type="text/x-handlebars" id="subnav">
<h2>Hello I'm Subnav!</h2>
</script>
Index Route
App.IndexRoute = Ember.Route.extend({
renderTemplate: function() {
this.render(); // this renders the index template per se
// and this additional call renders the subnav template into the named outlet
this.render('subnav', { //the name of your template
outlet: 'subnav', //the name of the named outlet
into: 'index' //the name of the template where the named outlet should be rendered into
});
}
});
Note, you can have as many named outlet's as you want as long you render them like mentioned above, no matter what route you are in.
Hope it helps.

Related

Ember 2 - Hide / show content component

I have a component app/components/offer-listing.js:
import Ember from 'ember';
export default Ember.Component.extend({
isOfferShowing: false,
actions: {
offerShow() {
if (this.get('isOfferShowing')) {
this.set('isOfferShowing', false);
} else {
this.set('isOfferShowing', true);
}
}
}
});
and his template app/templates/components/offer-listing.hbs:
<div class="offer__container">
<div class="row">
<div class="gr-3">
<div class="offer__avatar" style="background-image: url('{{ offer.avatar }}')"></div>
</div>
<div class="gr-9">
<div class="offer__name" {{action "offerShow"}}>{{ offer.firstname }} {{ offer.lastname }}</div>
<div class="offer__age" {{action "offerShow"}}>{{ offer.age }} ans</div>
{{#if isOfferShowing}}
<div class="offer__description" {{action "offerShow"}}>{{offer.description}}</div>
{{else}}
<div class="offer__description" {{action "offerShow"}}>{{word-limit offer.description 50}}</div>
{{/if}}
{{#if isOfferShowing}}
<div class="+spacer"></div>
<a class="offer__button"><i class="fa fa-envelope"></i> Contacter par email</a>
<a class="offer__button"><i class="fa fa-phone"></i> Voir le numéro de téléphone</a>
{{/if}}
</div>
</div>
</div>
which is rendered in app/templates/index.hbs:
{{#each model as |offerUnit|}}
{{offer-listing offer=offerUnit}}
{{/each}}
The example is working great, however I would like to hide every "more" content when a new one is showing.
A working solution for this is available here : Using Ember component's methods inside template
Basically, either you keep a reference to the selected element in your controller and pass it to each of your offer-listing components. This way they could compare themselves with this reference to known if they need to be displayed or not.
Or you set a flag in each of your offer model depending on whether is needs to be displayed or not.

Why didInsertElement hook trigger before elements rendered inside {{#each}} block?

I'm new to Ember, I want to add some query DOM manipulation code to the element in the {{#each}} block. So I google it up and found the solution from this guide:
views/products/index.js
import Spinner from 'appkit/utils/someJqueryCode';
Ember.View.reopen({
didInsertElement : function(){
this._super();
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
},
afterRenderEvent : function(){
// implement this hook in your own subclasses and run your jQuery logic there
}
});
export default Ember.View.extend({
afterRenderEvent: function() {
Spinner();
}
});
templates/products/index.hbs
<div class='panel panel-default products'>
<div class='panel-heading'>
<h2 class='panel-title'>Our Prodcuts</h2>
</div>
<div class='panel-body'>
<ul class='row'>
{{#each}}
<li class='col-md-4'>
<div class='thumbnail'>
<img {{bind-attr src=url alt=alt}} />
</div>
<div class='caption'>
<h3 class='name-me'>{{name}}</h3>
<p>{{description}}</p>
<div class='row no-gutter'>
<div class='col-xs-3'>
<button class='btn btn-primary'>Buy</button>
</div>
</div>
</div>
</li>
{{/each}}
</li>
</div>
</div>
But I seems after the point when afterRenderEvent() is triggered, all the elements in the {{#each}} block hasn't been rendered to the DOM yet, thus, the jQuery code return undefined
What's the right way to do it?
Your view's didInsertElement hook will fire as soon as the application route is rendered, which happens before the index route. You might think that putting it in the index.js file will work, but it's going to just extend the default application view behavior.
You need to create a more focused view that lives within your index.hbs file. One that is only concerned with your spinner jQuery doohickey. That, and an each/else conditional could work nicely here. For example:
{{#each}}
{{#view App.SpinnerDoohickeyView}}
<li class='col-md-4'>
<div class='thumbnail'>
<img {{bind-attr src=url alt=alt}} />
</div>
<div class='caption'>
<h3 class='name-me'>{{name}}</h3>
<p>{{description}}</p>
<div class='row no-gutter'>
<div class='col-xs-3'>
<button class='btn btn-primary'>Buy</button>
</div>
</div>
</div>
</li>
{{/view}}
{{else}}
<li>Empty collection!</li>
{{/each}}
Notice that I've wrapped each list item in its own view. You could wrap the whole ul if you wanted... this is just an example. The idea is that you are only creating views when you have a model.
And now you can define the view, and simply use the didInsertElement hook to work with jQuery:
App.SpinnerDoohickeyView = Ember.View.extend({
didInsertElement: function () {
this.$('li').css('background', 'blue');
}
});
If you have a model to render, jQuery should be able to safely access it this way. Good luck!
Here's some further reading and some code from the Ember folks that looks like what I've shown you here: http://emberjs.com/guides/views/handling-events/

Link-to cannot find nested resource on passing two models

Here is the app i am learning to build.
categories has many products.
Accordion of category - heading and products as body is in categories template
router.js
MyApp.Router.map(function () {
this.resource('categories', {path: '/'}, function () {
this.resource('category', { path: '/:category_id'}, function () {
this.resource('product', {
path: '/:product_id'
});
});
});
});
My categories handlebar
<div class="panel-group" id="accordion1">
{{#each category in model}}
{{view MyApp.AccordionView context=category}}
{{/each}}
</div>
and my accordion view
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
{{#view MyApp.AccordionTitleView }}{{name}} {{/view}}
</h4>
</div>
<div {{bind-attr id=id}}class="panel-collapse collapse">
<div class="panel-body">
<table class="table">
<tbody>
{{#each products itemController="product"}}
<tr><td>{{#link-to 'category.product' category content }}{{content.name}}{{/link-to}}</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
U P D A T E S
my issue got resolved by changing
{{#link-to 'category.product' category content }} in my accordion view to
{{#link-to product' category content }}
but as per ember guides
Ember guides
I can pass two model like this sample code
<p>
{{#link-to 'photo.comment' 5 primaryComment}}
Main Comment for the Next Photo
{{/link-to}}
</p>
i dont understand if its because my router hierarchy. but in any case i think it should give some nice error. I an a noob wonder what experts think on this
You have to use 'product' in your link-to instead of 'category.product'.
This is because in MyApp.Router.map() you made product a resource, not a route.
Resources' route names are unprefixed, whereas routes' route names are prefixed with their parent resource.
See http://emberjs.com/guides/routing/defining-your-routes/#toc_nested-resources

Ember view with dynamic class names

Considering the following:
Parent template:
{{view App.SomeView id="42" panelClass="default"}}
View template:
<div class="col-md-3 col-sm-6">
<div class="panel panel-{{panelClass}}">
<div class="panel-heading">
<h3 class="panel-title">
{{name}}
</h3>
</div>
<div class="panel-body">
{{description}}
</div>
</div>
</div>
View JS:
App.SomeView = Ember.View.extend({
templateName: 'views/some-view'
});
How can I achieve output HTML where the panel class gets set properly? At the moment it doesn't work because it wants to bind, so it inserts the ember metamorph script tags, instead of just plain text for the panel class.
Also, the template is wrapped in an extra div. How would I modify it so that the ember-view wrapping div is actually the first div in the template (the one with col-md-3 col-sm-6)?
The bind-attr helper exists for that reason. Here's the guide entry.
<div {{bind-attr class=":panel panelClass"}}></div>
Also, not sure if you can use a prefix on panelClass in the template. If might be easier just to use a computed property to add the panel- beforehand.
I'm sorry, I didn't see your second question about the extra div. The guide explains here how to extend the element.
App.SomeView = Ember.View.extend({
classNames: ['col-md-3', 'col-sm-6']
});

How to set itemController for instance variable in ember.js indexController?

I have a Ember.js page that displays a table of items and shows details of one of the items when it is selected. The controller looks like this:
CV.IndexController = Ember.ArrayController.extend({
itemController: "CurrVitae",
selectedCurrVitae: false,
actions: {
selectCurrVitae: function(currVitae) {
this.set('selectedCurrVitae', currVitae)
}
}
});
And the index controller is used in a template like this:
<div class="container">
<div class="curr-vitae-list">
{{#each curr_vitae in controller}}
<div class="curr-vitae-item row">
<div class="col-sm-2" {{action selectCurrVitae curr_vitae}}>
{{curr_vitae.name}}
</div>
<div class="col-sm-2">
<!-- NOTE: This method is defined on the item
controller (not the model) so the itemController is
available at this point. -->
{{curr_vitae.createdAtDisplay}}
</div>
<div class="col-sm-7">
<div class="embedded-cell">
{{curr_vitae.summary}}
</div>
<div class="embedded-cell">
{{curr_vitae.objective}}
</div>
</div>
</div>
{{/each}}
</div>
<div class="curr-vitae-view">
<h2>Details</h2>
<!-- EDIT: I have tried setting this
as {{#if selectedCurrVitae itemController="currVitae" }}
to match the way the #each handles item controllers but that did
not seem to work -->
{{#if selectedCurrVitae }}
<!-- NOTE: Down here, however, the item controller is not available
so I can't use methods defined on the item controller for the
currently selected instance. -->
{{ partial "cv_index_details" }}
{{/if}}
</div>
</div>
Question: The problem I'm running in to is that the itemController I've set in the index controller is not available when rendering the selectedCurrVitae in the cv_index_details.
More details:
Specifically, in the partial I want to reuse a editor component (taken from Noel Rappin's Ember.js book). So if the cv_index_details partial looks like this:
<h3 class="selected_cv">{{selectedCurrVitae.name}}</h3>
<div class="row selected_cv_summary">
<h4>Summary</h4>
{{block-editor emberObject=selectedCurrVitae propName="summary" action="itemChanged"}}
</div>
<div class="row selected_cv_experiences">
<h4>Experiences</h4>
{{#each experience in selectedCurrVitae.experiences itemController="experience"}}
{{ partial "experience_detail" }}
{{/each}}
</div>
So in this template, the itemChanged action is not found for the selectedCurrVitae instance. However, I use the same block-editor component for the experience instance and that works correctly; the itemChanged action defined on the ExperienceController is found.
selectedCurrVitae is outside of the itemController, the itemController is only applied inside the each (on each item, hence the name).
Probably the easiest way to accomplish this will be to reuse the item controller and use render.
Template
{{#if selectedCurrVitae }}
{{ render "cv_index_details" }}
{{/if}}
Controller
CV.CvIndexDetailsController = CV.CurrVitaeController.extend();
Or
Template
{{#if selectedCurrVitae }}
{{ render "currVitae" }}
{{/if}}
View
CV.CurrVitaeView = Ember.View.extend({
templateName: 'cv_index_details'
});