Ember view with dynamic class names - ember.js

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']
});

Related

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/

Ember.js View without Wrapping Div?

I have a 3 column layout in my app. I'm using Semantic UI. So, the layout is:
<div class='ui celled grid'>
<div class='left column'>...</div>
<div class='middle column'>...</div>
<div class='right column'>...</div>
</div>
So, straightforward.
In my application.hbs it is simply:
<div class='ui celled grid'>
<div class='left column'><!-- menu --></div>
{{outlet}}
</div>
And the other 2 columns are in my sub-controller/templates. And this works fine until I need a View. If I need a View, then Ember makes the HTML layout become:
<div class='ui celled grid'>
<div class='left column'><!-- menu --></div>
<div class='ember-view'>
<div class='middle column'><!-- content --></div>
<div class='right column'><!-- content --></div>
</div>
</div>
And the wrapping <div class='ember-view'> breaks my layout. Because I don't always need a View I need a way to make the HTML the same for with or without a View.
At this point, I see 2 solutions. I'll either have to rework my layout in some way (that I'm yet to work out). Or I can get rid of the wrapping div.
But is it possible to get rid of the wrapping div? I tried this:
export default Ember.View.extend({
tagName: null
});
But that doesn't work. I also tried a span, but that still breaks my layout.
Any ideas?
Thanks.
Try using
tagName:''
The empty string as the tagName's value on your view.
Update:
When I brought this to the attention of ember.js contributors, they suggested inheriting the view from
Ember._MetamorphView
https://github.com/emberjs/ember.js/pull/4790

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'
});

Ember.js best practices for adding subnav

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.

Ember.js - CollectionView, add an attribute to the div wrapper of the child view

I'm trying to combine two ebmer.js examples: Integrating with jQuery UI and the todos example from emberjs.com. I want to have a todo list that is sortable.
Everything went smooth until I got to a point where I wanted to serialize the sortable. For that to work, I need to be able to add an attribute to the sortable items.
this is the template:
{{#collection Todos.TodosListView}}
{{#view Todos.TodoView contentBinding="content" checkedBinding="content.isDone"}}
<label>{{content.title}}</label>
{{/view}}
{{/collection}}
Todos.TodosListView is a CollectionView, similar to the menu in the jQuery UI example. Todos.TodoView is a Checkbox.
This generates the following html:
<div class="ember-view todo-list ui-sortable" id="ember267">
<div class="ember-view" id="ember284">
<input type="checkbox" class="ember-view ember-checkbox todo" id="ember297">
<label>
<script type="text/x-placeholder" id="metamorph-1-start"></script>
something to do
<script type="text/x-placeholder" id="metamorph-1-end"></script>
</label>
</div>
</div>
What I need to be able to do is edit the <div> that wraps the <input>. Assuming the todo's id is 1, I want to add serial=todos_1. I tried to add didInsertElement to TodoView and add an attribute to the parent view, but I didn't have access to the content of the view (the todo itself).
Is this possible?
Thanks for your help.
EDIT:
I found a workaround - adding the ID to the DOM as a hidden element.
The updated template:
{{#collection Todos.TodosListView}}
{{#view Todos.TodoView contentBinding="content" checkedBinding="content.isDone" serial="content.serial"}}
<label>{{content.title}}</label>
<span style="display: none;" class="todo-id">{{content.id}}</span>
{{/view}}
{{/collection}}
Todos.TodoView.didInsertElement:
didInsertElement: function() {
var $div = this.get('parentView').$();
var id = $div.children('.todo-id').text();
$div.attr('serial', 'todos_' + id);
}
Generated html:
<div class="ember-view todo-list ui-sortable" id="ember267">
<div class="ember-view" id="ember284" serial="todos_100">
<input type="checkbox" class="ember-view ember-checkbox todo" id="ember297">
<label>
<script type="text/x-placeholder" id="metamorph-1-start"></script>
something to do
<script type="text/x-placeholder" id="metamorph-1-end"></script>
</label>
<span class="todo-id" style="display: none;">
<script type="text/x-placeholder" id="metamorph-2-start"></script>
100
<script type="text/x-placeholder" id="metamorph-2-end"></script>
</span>
</div>
</div>
I would still like to know if there's a more elegant way of achieving this.
You can create a computed property serial and add this property to the attributeBindings (documented here) of your itemViewClass of Todos.TodosListView, see http://jsfiddle.net/pangratz666/6X4QU/:
Todos.TodosListView = Ember.CollectionView.extend({
itemViewClass: Ember.View.extend({
attributeBindings: ['serial'],
serial: function() {
return 'todos_' + Ember.getPath(this, 'content.id');
}.property('content.id').cacheable()
})
});