I've been looking for mechanism to update the model of a Route, and has the Component (called from within the template associated with that route) reacts to that event, and re-render itself.
So I have the index template like this (I pass in the model of the IndexController, which to my understanding is just a proxy to IndexRoute -- I don't have IndexController defined, by the way):
<script type="text/x-handlebars" id="index">
Below is the bar-chart component
<br/>
{{bar-chart model=model}}
</script>
And I have my component template like this:
<script type="text/x-handlebars" data-template-name="components/bar-chart">
</script>
My component is implemented in a separate JS file, like this:
App.BarChartComponent = Ember.Component.extend({
classNames: ['chart'],
model: null,
chart: BarChart(),
didInsertElement: function() {
Ember.run.once(this, 'update');
},
update: function() {
var data = this.get('model').map(function(sales) {
return sales.get('amount');
});
d3.select(this.$()[0]).selectAll('div.h-bar')
.data(data)
.call(this.get('chart'));
}
});
The BarChart() function is simply returns a function object that performs the DOM manipulation to generate the graph using D3.
My IndexRoute is defined like this:
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('sales');
}
});
During this experiment, I use fixture:
App.Sales = DS.Model.extend({
amount: DS.attr('number')
});
idx = 1;
App.Sales.FIXTURES = [
{id: idx++, amount: 2}, {id: idx++, amount: 6}, {id: idx++, amount: 12},
{id: idx++, amount: 17}, {id: idx++, amount: 8}
];
I need to implement a mechanism to periodically poll the store and update the model of the Route, and has EmberJS's magic invoke again the render function (the value assigned to "chart" field in the BarChart component).
What's the correct way to do that? I've been trying to use setInterval and calling refresh() method of the Route, but have not been successful so far.
Thanks for your help!,
Raka
ADDITION (I put my additional comment here for the formatting).
I added the call to setInterval in my app.js, like this:
setInterval(function () {
App.Sales.FIXTURES.shift();
App.Sales.FIXTURES.push({
id: idx++,
amount: Math.round(Math.random() * 20)
});
App.IndexRoute.refresh();
}, 1500);
But I'm getting JavaScript error, telling me that App.IndexRoute is undefined. I intend to call the 'refresh' method on the Route object because I'm hoping the model hook to be re-executed. How do I obtain a reference to instance of IndexRoute from my setInterval function?
Is this the correct / best way to trigger the refresh, btw?
(and, following the suggestion from Oliver below, I also added observes('model') to my 'update' function in the controller. So it is like this now:
App.BarChartComponent = Ember.Component.extend({
classNames: ['chart'],
model: null,
chart: BarChart(),
didInsertElement: function() {
...
},
update: function() {
...
}.observes('model')
});
ADDITION 2 (response to EmberJS, polling, update route's model, re-render component )
Got it! Thx.
Now for the updating use case (the number of elements in the backend stays the same, the ids stay the same, only the "amount" changes over time). I modified setInterval block to this:
setInterval(function () {
App.Sales.FIXTURES.forEach(function(elem) {
elem.amount = elem.amount + 5;
});
console.log('z');
}, 1500);
The problem now, the "update" method in BarChartComponent that observes "model.#each" never gets called (as if the changes I did in the elements of the fixture wasn't heard by the BarChartComponent).
What instruction(s) do I need to add?
ADDITION 3 (detail for EmberJS, polling, update route's model, re-render component ):
I added the definition of IndexController to my code, just to confirm that my changes to the elements in the FIXTURE was heard at least by the Controller (it is).
So, the problem now is making that change is also heard by the Component. How? Should I call some "render" function from my controller to ask the component to redraw itself?
App.IndexController = Ember.ArrayController.extend({
totalAmount: function() {
console.log("iiii");
var total = 0;
this.forEach(function(sales) {
console.log("a... " + sales.get('amount'));
total += sales.get('amount');
});
return total;
}.property('#each.amount')
});
App.IndexRoute is actually a class definition, not an instance.
For your particular case there are some important things to note here, find('type') returns the all filter which automatically updates as you add/remove items from the store. So you could just call find again anywhere in the code (for that type), and it would automatically update your collection. Additionally you would want to control the updating at the route level, that way you don't keep updating when you aren't in scope.
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('sales');
},
setupController: function(controller, model){
this._super(controller, model); // do the default implementation since I'm overriding this func
this.startRefreshing();
},
startRefreshing: function(){
this.set('refreshing', true);
Em.run.later(this, this.refresh, 30000);
},
refresh: function(){
if(!this.get('refreshing'))
return;
this.store.find('sales')
Em.run.later(this, this.refresh, 30000);
},
actions:{
willTransition: function(){
this.set('refreshing', false);
}
}
});
Example: http://emberjs.jsbin.com/OxIDiVU/825/edit
Additionally, for your component, you'd want to watch the array, not just the model itself (since the model reference won't update, meaning the observes method wouldn't be called). You would do something like this
watchingForModelChange: function(){
}.observes('model.[]')
You just need to watch the model. No?
update: function() {
var data = this.get('model').map(function(sales) {
return sales.get('amount');
});
d3.select(this.$()[0]).selectAll('div.h-bar')
.data(data)
.call(this.get('chart'));
}.property('model')
I want to access a property, say selectedItem defined in a component from a parent controller. How to achieve this? I want to access this item so that I can open a modal defined as a partial with this. If anyone can suggest any better solution that is also welcome.
You could bind the property to a property of the controller. Something like this:
App.FooBarComponent = Ember.Component.extend({
selectedItem: null,
// set the property somewhere in your component
});
In your controller
App.SomeController = Ember.Controller.extend({
fooBarSelectedItem: /* empty (null) or a value */
});
In your template
{{foo-bar selectedItem=controller.fooBarSelectedItem}}
I have a "count-down" component, which renders a clock, and when the count-down ends, I need to disable some buttons on the view around the component. The solution is similar to #splattne's answer, but it's newer Ember 3.1 syntax and the shared value is not part of the model.
Component:
export default Component.extend({
'remaining_time': computed('timer_end', 'dummy', function() {
let now = new Date();
let remaining = this.get('timer_end') - now;
if (remaining < 0) {
scheduleOnce('afterRender', this, function(){
this.set('event_listener.expired', true);
});
this.set('should_run', false);
return "Countdown Closed";
}
...
}),
});
Template:
{{count-down timer_end=model.timer_end event_listener=countdown_status}}
Controller:
export default Controller.extend({
countdown_status: Object.create({'expired': false}),
controls_enabled: computed('countdown_status.expired', function() {
return !this.get('countdown_status.expired');
}),
...
});
Note the scheduleOnce('afterRender': it was necessary for https://github.com/emberjs/ember.js/issues/13948. You will only need it if your component changes the shared value before the whole view is rendered, which is unfortunately what mine did.
I am using a radialProgress as a jQuery plugins (homemade), and I need to implement it for ember but I have some issue to do that.
Quick explanation for the plugins :
var chart = $(yourElement).pieChart(options); // initialise the object to an element
chart.setCompleteProgress( complete, false ); // set how many item you have to complete the task
chart.incrementProgress(); // increment + 1 every time you call it
It's a very simple progress pie.
In my case my task are located inside my controller, but the chart as to select a dom element so I need to initialise it inside my view.
My task in the controller are called from the router from the setupController to reload the model over time.
Here is a small sample of what I would like to do :
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller) {
var promise = controller.getModel();
this._super(controller, promise);
}
})
App.ApplicationController = Ember.ArrayController.extend({
getModel: function() {
// chart.setcompleteProgress();
// A lot of code are here to get some data
// chart.incrementProgress();
return newModel;
}
})
App.ApplicationView = Ember.View.extend({
didInsertElement: function() {
var chart = $(element).pieChart(opts);
}
})
I don't know how to pass the chart object from the view to the controller to be able to have access to my plugin function.
Che chart won't be inserted into the DOM until the didInsertElement therefore you can't attempt to manipulate it in the route during setupController etc. I'd suggest creating a method in the controller setupChart and calling that on didInsertElement.
App.ApplicationView = Ember.View.extend({
prepPieChart: function() {
var chart = $(element).pieChart(opts);
this.get('controller').setupPieChart(chart);
}.on('didInsertElement')
})
App.ApplicationController = Ember.ArrayController.extend({
setupPieChart: function(chart) {
chart.setcompleteProgress();
// A lot of code are here to get some data
chart.incrementProgress();
}
})
All that being said, maybe it belongs in the view, but I'm not sure of what you're completely doing.
I am migrating my project from older version of EmberJS. In some places i used to get controller instance which is not related to the view, by using following in any view's init() method:
var controller = App.get('router').get('firstController');
But now this throws following error.
Uncaught TypeError: Cannot call method 'get' of undefined
This may be because it is not able to get the Router object. Now how to get controller instance which is not related to the view? or how to get the Router Object
The 'needs' feature allows a controller to access to other controllers, which allows a controller's view to access other controllers. (a good explanation of needs in Ember: http://darthdeus.github.com/blog/2013/01/27/controllers-needs-explained/)
As explained in Cannot access Controller in init function of View in 1.0.0rc, the controller property of a view is not yet set when init() is called, so you will need to access controller at a later time in the view's life cycle. This could be the willInsertElement() or didInsertElement() hooks, for example.
Here is an example demonstrating using needs access another controller from a view:
http://jsbin.com/ixupad/186/edit
App = Ember.Application.create({});
App.ApplicationController = Ember.Controller.extend({
doSomething: function(message) {
console.log(message);
}
});
App.IndexView = Ember.View.extend({
templateName: 'index',
init: function() {
this._super();
// doesn't work, controller is not set for this view yet see:
// https://stackoverflow.com/questions/15272318/cannot-access-controller-in-init-function-of-view-in-1-0-0rc
//this.get('controller.controllers.application').doSomething("from view init");
},
willInsertElement: function() {
this.get('controller.controllers.application').doSomething("from view willInsertElement");
},
clickMe: function() {
this.get('controller.controllers.application').doSomething("from clickMe");
}
});
App.IndexController = Ember.Controller.extend({
needs: ['application']
});
hi i have the following route:
MB3.PlaylistRoute = Ember.Route.extend({
model: function(params) {
return MB3.Playlist.find(params.playlist_id);
}
});
The playlist has a hasMany realtion with tracks. in the playlist view i want do do some logic with an attribute of the first track of the playlist.
so i added this code:
MB3.PlaylistView = Ember.View.extend({
didInsertElement: function() {
console.log(this.get("controller.tracks").objectAt(0).get("title"));
}
});
The problem is title is undefined (i think because it is not yet loaded. the second thing i tried is waiting for the didLoad event:
MB3.PlaylistView = Ember.View.extend({
didInsertElement: function() {
var self=this;
this.get("controller.tracks").on("didLoad", function() {
console.log(self.get("controller.tracks").objectAt(0).get("title"));
});
}
});
but this logges null as well. How do i accomplish that?
Like Adrien said in the comments, it seems you are running into issue 587. That said, I don't think you actually need the "didLoad" callback in this case. Instead, try using a computed property to get the video_id or track title. For example:
MB3.PlaylistView = Ember.View.extend({
firstTrackTitle: function() {
return this.get('controller.tracks.firstObject.title');
}.property('controller.tracks.firstObject.title')
});
Then in your template, embed the player if this property is defined:
{{#if view.firstTrackTitle}}
embed code here
{{/if}}
FWIW I would put this logic in controller instead of view, but same idea.