Ember.js - Communicating between controllers and their views - templates

I'm just in the early stages of learning Ember, and have run into something puzzling.
I'm trying to communicate between two controllers and have their corresponding views update as well.
In a simplified version, I'd like to click a button to fire an event on one controller, which starts a timer on another controller. This works, but the view of the timer is not being updated when the value changes.
Here's what I have:
var App = Ember.Application.create();
App.Route = Ember.Route.extend({
events: {
startTimer: function(data) {
this.get('container').lookup('controller:Timer').start();
}
}
});
App.ApplicationController = Ember.Controller.extend({
actionWord: 'Start',
toggleTimer: function() {
var timer = this.get('container').lookup('controller:Timer');
if(timer.get('running')) {
timer.stop();
} else {
timer.start();
this.set('actionWord', 'Stop');
}
}
});
App.TimerController = Ember.Controller.extend({
time: 0,
running: false,
timer: null,
start: function() {
var self = this;
this.set('running', true);
this.timer = window.setInterval(function() {
self.set('time', self.get('time') + 1);
console.log(self.get('time'));
}, 1000);
},
stop: function() {
window.clearInterval(this.timer);
this.set('running', false);
this.set('time', 0);
}
});
and for the templates:
<script type="text/x-handlebars">
{{ render "timer" }}
<button {{action toggleTimer }} >{{ actionWord }} timer</button>
</script>
<script type="text/x-handlebars" data-template-name="timer">
{{ time }}
</script>
http://jsfiddle.net/mAqYR/1/
UPDATE:
Forgot to mention that if you open the console, you can see the time is being updated inside of the TimeController function, it's just not showing up in the view.
Also, calling the start action on the TimerController directly correctly updates the view.
Thanks!

You were using an out-of-date version of Ember.
I've updated your fiddle to the Ember rc3. Also I've replaced instances of container.lookup with the correct methods. The container is pretty much a private object.
http://jsfiddle.net/3bGN4/255/
window.App = Ember.Application.create();
App.Route = Ember.Route.extend({
events: {
startTimer: function(data) {
this.controllerFor('timer').start();
}
}
});
App.ApplicationController = Ember.Controller.extend({
actionWord: 'Start',
needs: ["timer"],
toggleTimer: function() {
var timer = this.get('controllers.timer');
if(timer.get('running')) {
timer.stop();
} else {
timer.start();
this.set('actionWord', 'Stop');
}
}
});
App.TimerController = Ember.Controller.extend({
time: 0,
running: false,
timer: null,
start: function() {
var self = this;
this.set('running', true);
this.timer = window.setInterval(function() {
self.set('time', self.get('time') + 1);
console.log(self.get('time'));
}, 1000);
},
stop: function() {
window.clearInterval(this.timer);
this.set('running', false);
this.set('time', 0);
}
});

Related

property in route undefined in controller

In the IndexRoute of my Ember hello world app, I start a setInterval function that I wish to allow the end user to turn off (with clearInterval) by clicking a dom element in the template, which triggers an action in the IndexController. So, the setIntervalId is set in the IndexRoute, and I need to pass it to clearInterval in the IndexController, but the way I have it below, the setIntervalId is undefined. I also tried to use App.IndexRoute.setIntervalId to no avail.
How would I accomplish this?
(function() {
window.App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_ACTIVE_GENERATION: true
});
App.IndexRoute = Ember.Route.extend({
setIntervalId: 0,
model: function() {
this.setIntervalId = setInterval(this.someInterval, 5000)
},
someInterval: function(){
var datasource = 'http://hackernews/blahblah';
return new Ember.$.ajax({url: datasource, dataType: "json", type: 'GET'}).then(function(data){
return data;
})
},
});
App.IndexController = Ember.ObjectController.extend({
actions: {
clearTimeout: function(){
console.log('clearing interval', this.setIntervalId); //undefined
clearInterval(this.setIntervalId);
}
}
})
})();
template
<script type="text/x-handlebars" data-template-name="index">>
<h1>Hi Babe</hi>
{{ outlet }}
<label {{action "clearTimeout" on="click"}}>clear timeout</label>
</script>
To set the model, you need to return the value in the route’s model function:
model: function() {
return this.setIntervalId = setInterval(this.someInterval, 5000)
}
To access the model in the controller, you need to use this.get('model').
actions: {
clearTimeout: function(){
console.log('clearing interval', this.get('model');
clearInterval(this.get('model'));
}
}

Schedule for every afterRender for a View

I would like to run code every time a view is rendered. The closest I can get is to listen to every property that could change and explicitly schedule something on the run loop for afterRender, but I would love to just have a lifecycle hook like afterRender. The property approach gets fragile, since you have to keep the list of properties up to date based on what can affect the render.
Controller:
App.IndexController = Ember.Controller.extend({
count: 0,
actions: {
add: function() {
var count = this.get('count');
count += 1;
this.set('count', count);
}
}
});
View:
App.IndexView = Ember.View.extend({
changed: function() {
Ember.run.scheduleOnce('afterRender', this.after);
}.observes('controller.count'),
after: function() {
console.log('after render', this.$('span').text());
}
});
Template:
<button {{action "add"}}>add</button> <span>{{count}}</span>
http://emberjs.jsbin.com/wujogejeso/3/edit?html,css,js,output
To schedule the code afterRender, you can use the didInsertElement lifecycle hook in place of your changed function.
App.IndexView = Ember.View.extend({
didInsertElement: function() {
Ember.run.scheduleOnce('afterRender', this.after);
},
after: function() {
console.log('after render', this.$('span').text());
}
});

Delete item from ember-tables

I'm trying add a delete button with an ember action from a controller. For some reason Ember.Handlebars.compile('<button {{action "deletePerson"}}>Delete</button> returns a function and not the compiled string.
Here's a jsbin
Here's the relevant portion of code:
App.ApplicationController = Ember.Controller.extend({
columns: function() {
...
buttonColumn = Ember.Table.ColumnDefinition.create({
columnWidth: 100,
headerCellName: 'Action',
getCellContent: function(row) {
var button = Ember.Handlebars.compile('<button {{action "deletePerson" this}}>Delete</button>');
return button; // returns 'function (context, options) { ...'
}
});
...
}.property()
...
After looking through the link from #fanta (http://addepar.github.io/#/ember-table/editable) and a lot of trial and error, I got it working.
Here's the working jsbin.
Here are some key points:
Instead of using getCellContent or contentPath in the ColumnDefinition, you need to use tableCellViewClass and to create a view that will handle your cell
Pass in this to the action on your button — and modify content off that. One gotcha is to edit content, you need to copy it using Ember.copy
Here's the relevant code:
App.ApplicationController = Ember.Controller.extend({
columns: function() {
...
buttonColumn = Ember.Table.ColumnDefinition.create({
columnWidth: 100,
headerCellName: 'Action',
tableCellViewClass: 'App.PersonActionCell'
});
...
}.property(),
onContentDidChange: function(){
alert('content changed!');
}.observes('content.#each'),
...
});
App.PersonActionCell = Ember.Table.TableCell.extend({
template: Ember.Handlebars.compile('<button {{action "deletePerson" this target="view"}}>Delete</button>'),
actions: {
deletePerson: function(controller){
// Will NOT work without Ember.copy
var people = Ember.copy(controller.get('content'));
var row = this.get('row');
// For some reason people.indexOf(row) always returned -1
var idx = row.get('target').indexOf(row);
people.splice(idx, 1);
controller.set('content', people);
}
}
});

Template not updating when controller property changes

Caveat: This is part of my first ember app.
I have an Ember.MutableArray on a controller. The corresponding view has an observer that attempts to rerender the template when the array changes. All the changes on the array (via user interaction) work fine. The template is just never updated. What am I doing wrong?
I'm using Ember 1.2.0 and Ember Data 1.0.0-beta.4+canary.7af6fcb0, though I guess the latter shouldn't matter for this.
Here is the code:
var ApplicationRoute = Ember.Route.extend({
renderTemplate: function() {
this._super();
var topicsController = this.controllerFor('topics');
var topicFilterController = this.controllerFor('topic_filter');
this.render('topics', {outlet: 'topics', controller: topicsController, into: 'application'});
this.render('topic_filter', {outlet: 'topic_filter', controller: topicFilterController, into: 'application'});
},
});
module.exports = ApplicationRoute;
var TopicFilterController = Ember.Controller.extend({
topicFilters: Ember.A([ ]),
areTopicFilters: function() {
console.log('topicFilters.length -> ' + this.topicFilters.length);
return this.topicFilters.length > 0;
}.property('topicFilters'),
getTopicFilters: function() {
console.log('getTopicFilters....');
return this.store.findByIds('topic', this.topicFilters);
}.property('topicFilters'),
actions: {
addTopicFilter: function(t) {
if(this.topicFilters.indexOf(parseInt(t)) == -1) {
this.topicFilters.pushObject(parseInt(t));
}
// this.topicFilters.add(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
},
removeTopicFilter: function(t) {
this.topicFilters.removeObject(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
}
}
});
module.exports = TopicFilterController;
var TopicFilterView = Ember.View.extend({
topicFiltersObserver: function() {
console.log('from view.... topicFilters has changed');
this.rerender();
}.observes('this.controller.topicFilters.[]')
});
module.exports = TopicFilterView;
// topic_filter.hbs
{{#if areTopicFilters}}
<strong>Topic filters:</strong>
{{#each getTopicFilters}}
<a {{bind-attr href='#'}} {{action 'removeTopicFilter' id}}>{{topic}}</a>
{{/each}}
{{/if}}
var TopicsController = Ember.ArrayController.extend({
needs: ['topicFilter'],
all_topics: function() {
return this.store.find('topic');
}.property('model', 'App.Topic.#each'),
actions: {
addTopicFilter: function(t) {
App.__container__.lookup('controller:topicFilter').send('addTopicFilter', t);
}
}
});
module.exports = TopicsController;
// topics.hbs
<ul class="list-group list-unstyled">
{{#each all_topics}}
<li class="clear list-group-item">
<span class="badge">{{entryCount}}</span>
<a {{bind-attr href="#"}} {{action 'addTopicFilter' id}}>{{topic}}</a>
</li>
{{/each}}
</ul>
your observes should just be controller.topicFilters.[]
And honestly this is a very inefficient way of doing this, rerendering your entire view because a single item changed on the array. If you show your template I can give you a much better way of handling this.
Here's an example, I've changed quite a few things, and guessed on some others since I don't know exactly how your app is.
http://emberjs.jsbin.com/uFIMekOJ/1/edit

How to do Ember integration testing for route transitions?

I'm having a problem doing integration testing with ember using Toran Billup's TDD guide.
I'm using Karma as my test runner with Qunit and Phantom JS.
I'm sure half of if has to do with my beginner's knowledge of the Ember runloop. My question is 2 parts:
1) How do I wrap a vist() test into the run loop properly?
2) How can I test for transitions? The index route ('/') should transition into a resource route called 'projects.index'.
module("Projects Integration Test:", {
setup: function() {
Ember.run(App, App.advanceReadiness);
},
teardown: function() {
App.reset();
}
});
test('Index Route Page', function(){
expect(1);
App.reset();
visit("/").then(function(){
ok(exists("*"), "Found HTML");
});
});
Thanks in advance for any pointers in the right direction.
I just pushed up an example application that does a simple transition when you hit the "/" route using ember.js RC5
https://github.com/toranb/ember-testing-example
The simple "hello world" example looks like this
1.) the template you get redirected to during the transition
<table>
{{#each person in controller}}
<tr>
<td class="name">{{person.fullName}}</td>
<td><input type="submit" class="delete" value="delete" {{action deletePerson person}} /></td>
</tr>
{{/each}}
</table>
2.) the ember.js application code
App = Ember.Application.create();
App.Router.map(function() {
this.resource("other", { path: "/" });
this.resource("people", { path: "/people" });
});
App.OtherRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo('people');
}
});
App.PeopleRoute = Ember.Route.extend({
model: function() {
return App.Person.find();
}
});
App.Person = Ember.Object.extend({
firstName: '',
lastName: ''
});
App.Person.reopenClass({
people: [],
find: function() {
var self = this;
$.getJSON('/api/people', function(response) {
response.forEach(function(hash) {
var person = App.Person.create(hash);
Ember.run(self.people, self.people.pushObject, person);
});
}, this);
return this.people;
}
});
3.) the integration test looks like this
module('integration tests', {
setup: function() {
App.reset();
App.Person.people = [];
},
teardown: function() {
$.mockjaxClear();
}
});
test('ajax response with 2 people yields table with 2 rows', function() {
var json = [{firstName: "x", lastName: "y"}, {firstName: "h", lastName: "z"}];
stubEndpointForHttpRequest('/api/people', json);
visit("/").then(function() {
var rows = find("table tr").length;
equal(rows, 2, rows);
});
});
4.) the integration helper I use on most of my ember.js projects
document.write('<div id="foo"><div id="ember-testing"></div></div>');
Ember.testing = true;
App.rootElement = '#ember-testing';
App.setupForTesting();
App.injectTestHelpers();
function exists(selector) {
return !!find(selector).length;
}
function stubEndpointForHttpRequest(url, json) {
$.mockjax({
url: url,
dataType: 'json',
responseText: json
});
}
$.mockjaxSettings.logging = false;
$.mockjaxSettings.responseTime = 0;
I'm unfamiliar with Karma, but the portions of your test that needs to interact with ember should be pushed into the run loop (as you were mentioning)
Ember.run.next(function(){
//do somethin
transition stuff here etc
});
To check the current route you can steal information out of the ember out, here's some information I stole from stack overflow at some point.
var router = App.__container__.lookup("router:main"); //get the main router
var currentHandlerInfos = router.router.currentHandlerInfos; //get all handlers
var activeHandler = currentHandlerInfos[currentHandlerInfos.length - 1]; // get active handler
var activeRoute = activeHandler.handler; // active route
If you start doing controller testing, I wrote up some info on that http://discuss.emberjs.com/t/unit-testing-multiple-controllers-in-emberjs/1865