Ember.js: How to bind event handler to dom element? - ember.js

I'm using the jquery plugin scrollTo to create buttons to 'jump' to particular items in a long list. I bet there is a way to loop through each item and bind the event to it's corresponding button, but I don't know how to approach that. Sounds like something that would occur in the view though...
How can I bind a jQuery event to each 'item' rendered in the ember template below?
Ember 'items' template
<div class="buttons">
{{#each}}<a href="#" {{bindAttr class="isActive slug"}}></a>{{/each}}
</div>
<section class='region region-main-upper clearfix items'>
{{#each}}
<div {{bindAttr class=":postcard-left :item isActive slug"}}>
<div><img class='img-polaroid' {{bind-attr src=image}}></div>
<div>
<h4>{{title}}</h4>
<div class="span2">
<span class="item-property clear-row">Cost: {{printUSD itemCost}}</span>
<span class="item-property clear-row">Percent: {{percentVec}}%</span>
</div>
{{render 'options' options}}
</div>
</div>
{{/each}}
</section>
Straight javascript
This works, but is undesirable:
(function($){
$(document).ready(function() {
// this does what I am trying to achive, but is not maintainable...
$( "body" ).on( "click", ".solar", function(e) {
$('.items').stop().scrollTo( '.item.solar', 800 );
e.preventDefault();
});
$( "body" ).on( "click", ".coal", function(e) {
$('.items').stop().scrollTo( '.item.coal', 800 );
e.preventDefault();
});
});
})(jQuery);

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 {{action}} doesn't work

I want button to call removeAlbum function in controller. But it does nothing. When i click the button nothing happens and there are no errors... What should i do to fix that?!
This is my template:
<script type="text/x-handlebars" data-template-name="albums">
{{#if App.albumsController.total}}
<div><h1>Количество альбомов: {{App.albumsController.total}}</h1></div>
{{#each content in App.albumsController}}
<div class='album'>
<div class='image'>
<a {{action showAlbum content href=true}}>
<img {{bindAttr src="content.avatarUrl"}}>
</a>
</div>
<div class='info'>
<h2>
<a {{action showAlbum content href=true}}>
{{content.title}}
</a>
</h2>
<div>{{content.description}}</div>
</div>
<div class='actions'>
<div>
<button {{action removeAlbum content target="App.albumsController"}}>Delete</button>
</div>
</div>
<div class='clear'></div>
</div>
{{/each}}
{{else}}
<div><h1>Loading</h1></div>
{{/if}}
</script>
This is my controller:
App.albumsController = Em.ArrayController.create({
content: [],
total: null,
loadAlbums: function(){
this.set('content', []);
this.set('total', null);
var self = this;
$.post(App.connection.url, {method: 'albums.getAll', params: {user_id: App.connection.userId}}, function(response){
self.set('total', response.albums.total);
response.albums.album.forEach(function(item){
var buf = App.AlbumInList.create({
id: item.id,
title: item.title,
description: item.description,
avatarUrl: item.thumbnail_video.thumbnails.thumbnail[1]._content
});
self.pushObject(buf);
});
});
},
removeAlbum: function(x){
console.log('remove it');
}
});
Try to set your target to controller instead of App.albumsController. Should solve the problem.
<button {{action removeAlbum content target="controller"}}>Delete</button>

Action on self created View

If I've a simple template with a Button which has an Action, and create a ember View using this template, how can I let the action target the function on teh View.
Example:
http://jsfiddle.net/Krutius/DxsXz/
Handlebars / HTML:
<div id="content">
<div id="main"></div>
</div>
<script type="text/x-handlebars" data-template-name="test">
<button class="btn btn-primary" {{action go}}>Suchen</button>
</script>​
JavaScript:
$(function() {
App = Em.Application.create({
rootElement: "#content"
});
Em.View.create({
templateName: 'test',
go: function() {
alert: "go";
}
}).append("#main");
});​
The problem is your append call: Ember.View#append does not take any arguments, see the code.
I don't know what the final application / html should look like, so there are several answers how you could solve your problem. The simplest would be to inline the button into a template for the application, see http://jsfiddle.net/pangratz666/vY9PE/:
Handlebars:
<script type="text/x-handlebars" data-template-name="test">
<div id="content">
<div id="main">
<button class="btn btn-primary" {{action go}}>Suchen</button>
</div>
</div>
</script>​
JavaScript:
App = Em.Application.create({});
Em.View.create({
templateName: 'test',
go: function(evt) {
console.log('go', evt);
}
}).append();​
Ember.js IS able to insert an element at a specific position though, but this should only be necessary for an applications' main view. So you could use Ember.View#appendTo(target). But again, this should only be used in rare cases.

In EmberJS my event triggers all the sub-views instead of just the targeted one

i'm learning EmberJS and building a comment section that allows 1 level of sub comments. I have an Ember View listing all the comments, when you click "reply" on a particular comment, it should display a textarea input for a user to write a sub-comment.
In my EmberJS code when you click "reply" it shows the textarea input for all the comments not just the specific one. Any advice would be appreciated :)
// View
App.commentsView = Em.View.create({
templateName: 'commentsTmpl',
showReply: false,
reply: function(e) {
e.view.set('showReply', true);
e.preventDefault();
}
});
App.replyCommentsView = Em.View.extend({
showReplyBinding: 'App.commentsView.showReply'
});
// Template
<script data-template-name="commentsTmpl" type="text/x-handlebars">
</h2>comment</h2>
{{#each App.commentsController}}
<div class="comment-group clearfix">
<div class="comment">
<img class="comment-pic" {{bindAttr src="userPic"}} alt="user pic">
<div class="comment-content">
{{userName}}
<span class="comment-body">{{text}}</span>
<div class="comment-actions clearfix">
<a href="#" {{action "reply"}}>Reply</a>
</div>
</div>
</div>
{{#view App.replyCommentsView}}
{{#if showReply}}
<div class="comment-reply">
<h2>sub-comment</h2>
<textarea class="txt-comment-reply" rows="2" cols="65"></textarea>
</div>
{{/if}}
{{/view}}
</div>
{{/each}}
</script>
Currently you are binding the showReply to App.commentsView which is the whole container. To be make it easy activate single comments, I'd suggest looking into a CollectionView, this way each of your comments will have their own view and you can toggle showReply on an individual comment's view.
Something like this: (Sorry, I haven't tested it)
App.commentsView = Em.View.create({
templateName: 'commentsTmpl'
});
App.CommentView = Em.View.extend({
classNames: "comment-group clearfix".w(),
showReply: false,
reply: function(e) {
e.preventDefault()
this.set("showReply", true)
}
})
// Template
<script data-template-name="commentsTmpl" type="text/x-handlebars">
</h2>comment</h2>
{{#collection contentBinding="App.commentsController" itemViewClass="App.CommentView"}}
<div class="comment">
<img class="comment-pic" {{bindAttr src="content.userPic"}} alt="user pic">
<div class="comment-content">
{{content.userName}}
<span class="comment-body">{{content.text}}</span>
<div class="comment-actions clearfix">
<a href="#" {{action "reply"}}>Reply</a>
</div>
</div>
</div>
{{#if showReply}}
<div class="comment-reply">
<h2>sub-comment</h2>
<textarea class="txt-comment-reply" rows="2" cols="65"></textarea>
</div>
{{/if}}
{{/each}}
</script>

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