I'm trying to create a reusable generated element that can react to changing outside data. I'm doing this in an included view and using computed.alias, but this may be the wrong approach, because I can't seem to access the generic controller object at all.
http://emberjs.jsbin.com/nibuwevu/1/edit
App = Ember.Application.create();
App.AwesomeChartController = Ember.Object.extend({
data: [],
init: function() {
this.setData();
},
setData: function() {
var self = this;
// Get data from the server
self.set('data', [
{
id: 1,
complete: 50,
totoal: 100
},
{
id: 2,
complete: 70,
total: 200
}
]);
}
});
App.IndexController = Ember.Controller.extend({
needs: ['awesome_chart']
});
App.ChartView = Ember.View.extend({
tagName: 'svg',
attributeBindings: 'width height'.w(),
content: Ember.computed.alias('awesome_chart.data'),
render: function() {
var width = this.get('width'),
height = this.get('height');
var svg = d3.select('#'+this.get('elementId'));
svg.append('text')
.text('Got content, and it is ' + typeof(content))
.attr('width', width)
.attr('height', height)
.attr('x', 20)
.attr('y', 20);
}.on('didInsertElement')
});
And the HTML
<script type="text/x-handlebars">
<h2> Welcome to Ember.js</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<h2>Awesome chart</h2>
{{view App.ChartView width=400 height=100}}
</script>
For what it's worth, this didn't seem to work as a component, either. Is the ApplicationController the only place for code that will be used on multiple pages? The 'needs' seems to work, but the nested view can't access it. If I make a proper Ember.Controller instance to decorate the view, that doesn't seem to work either.
Any help is much appreciated.
Update:
I can't edit my comment below, but I found a good answer on how to use related, and unrelated, models in a single route.
How to use multiple models with a single route in EmberJS / Ember Data?
Firstly, your controllers should extend ObjectController/ArrayController/Controller
App.AwesomeChartController = Ember.Controller.extend({...});
Secondly when you create a view the view takes the controller of the parent, unless explicitly defined.
{{view App.ChartView width=400 height=100 controller=controllers.awesomeChart}}
Thirdly you already had set up the needs (needed a minor tweak), but just as a reminder for those reading this, in order to access a different controller from a controller you need to specify the controller name in the needs property of that controller.
App.IndexController = Ember.Controller.extend({
needs: ['awesomeChart']
});
Fourthly from inside the view your computed alias changes to controller.data. Inside the view it no longer knows it as AwesomeChart, just as controller
content: Ember.computed.alias('controller.data')
Fifthly inside your on('init') method you need to actually get('content') before you attempt to display what it is. content doesn't live in the scope of that method.
var content = this.get('content');
http://emberjs.jsbin.com/nibuwevu/2/edit
First, AwesomeChart does sound like it's gonna be a reusable self-contained component. In which case you should better user Ember.Component instead of Ember.View (as a bonus, you get a nice helper: {{awesome-chart}}).
App.AwesomeChartComponent = Ember.Component.extend({ /* ... */ });
// instead of App.ChartView...
Second, for AwesomeChart to be truly reusable, it shouldn't be concerned with getting data or anything. Instead, it should assume that it gets its data explicitly.
To do this, you basically need to remove the "content:" line from the awesome chart component and then pass the data in the template:
{{awesome-chart content=controllers.awesomeChart.data}}
Already, it's more reusable than it was before. http://emberjs.jsbin.com/minucuqa/2/edit
But why stop there? Having a separate controller for pulling chart data is odd. This belongs to model:
App.ChartData = Ember.Object.extend();
App.ChartData.reopenClass({
fetch: function() {
return new Ember.RSVP.Promise(function(resolve) {
resolve([
{
id: 1,
complete: 50,
total: 100
},
{
id: 2,
complete: 70,
total: 200
}
]);
// or, in case of http request:
$.ajax({
url: 'someURL',
success: function(data) { resolve(data); }
});
});
}
});
And wiring up the model with the controller belongs to route:
App.IndexController = Ember.ObjectController.extend();
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.ChartData.fetch();
}
});
Finally, render it this way:
{{awesome-chart content=model}}
http://emberjs.jsbin.com/minucuqa/3/edit
Related
ive searched for this and have not found an answer. I have 2 routes: "Index", which creates/updates an expense list, and "Charts", which charts the values of the expenses.
In order to handle the expense charts, I have the following function:
getData: function() {
var expenses = this.store.all('expense');
expenses.update();
var retarr = Ember.A();
expenses.forEach(function(expense) {
retarr.pushObject({
label: expense.get('name'),
value: expense.get('amount'),
group: 'expense'
});
});
return retarr;
}.property()
This is then passed to the ember-charts component in the Charts route.
<script type="text/x-handlebars" id='charts'>
<div class="chart-container">
{{horizontal-bar-chart data=getData}}
</div>
However, if I create/delete an expense in the "Index" route and hten transition to the "Charts" route, the DS.RecordArray doesn't update despite calling the "update()" function. As such, the chart does not reflect the created/deleted changes until the page is refreshed.
How do I fix this so the RecordArray auto updates along with the chart? I've broken my head for over two days trying different things. Thanks!
Your property getData should be bound to anything, if this something is an array you should use #each. For example as you can see here:
remaining: function() {
var todos = this.get('todos');
return todos.filterBy('isDone', false).get('length');
}.property('todos.#each.isDone')
I suggest you another approch, let's modify your model:
App.Chart = DS.Model.extend({
// fieds here...
label: function() {
return this.get("name");
}.property("name"),
value: function() {
return this.get("amount");
}.property("amount"),
group: function() {
return "expense";
}.property(),
)};
In your route set myCharts property:
App.ChartRoute = Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller, model);
var charts = this.store.find("chart");
controller.set("myCharts", charts);
}
});
Then you could use your horizontal chart:
<div class="chart-container">
{{horizontal-bar-chart data=myCharts}}
</div>
Note: I didn't tested this code but it should work
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 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 created several controllers and templates like the following for the sole purpose of sorting a property.
For example:
App.ExamplesController = Ember.ArrayController({
sortProperties: ['index']
});
Template ('examples'):
{{#each}}
{{foo}}
{{/each}}
Template ('app'):
<h1>MyApp</h1>
{{render 'examples' offers}}
Is there a less verbose way?
I'm going to add this as a new answer because it's so different. This is pretty tricky, and you'll need to modify it for your exact needs, but here's the basics. You're going to create a sorted-each component. First, the template:
<script type="text/x-handlebars" id="components/sorted-each">
{{#each sortedContent}}
{{yield}}
{{/each}}
<script>
Now we're going to declare the component class. We declare 3 properties that you will pass into the component. Then we created a computed property to sort the content. Then, we override the _yield method because by default, it gives us the wrong context.
App.SortedEachComponent = Em.Component.extend({
content: null,
key: null,
reverse: false,
sortedContent: function() {
return Em.ArrayProxy.createWithMixins(Em.SortableMixin, {
content: this.get('content'),
sortProperties: [this.get('key')],
sortAscending: !this.get('reverse')
});
}.property('content', 'key', 'reverse'),
_yield: function(context, options) {
var get = Ember.get,
view = options.data.view,
parentView = this._parentView,
template = get(this, 'template');
if (template) {
Ember.assert("A Component must have a parent view in order to yield.", parentView);
view.appendChild(Ember.View, {
isVirtual: true,
tagName: '',
_contextView: parentView,
template: template,
context: get(view, 'context'), // the default is get(parentView, 'context'),
controller: get(view, 'controller'), // the default is get(parentView, 'context'),
templateData: { keywords: parentView.cloneKeywords() }
});
}
}
});
Finally, let's use it!
{{#sorted-each content=items sortKey=itemKey1 reverse=true}}
Property1: {{itemKey1}}
Property2: {{itemKey2}}
{{/sorted-each}}
You'll probably want to modify the component a bit to your liking, but as you can see, it does work. (At least it should. Let me know if there's any typos.)
I was literally just looking for this 20 minutes ago. I don't know about in the template, but you can do it in your model hook, which eliminates the need for declaring a controller:
model: function() {
return Em.ArrayProxy.createWithMixins(Em.SortableMixin, {
sortProperties: ['index'],
content: this.get('store').find('widget', [1,2,3,4])
});
}
EDIT: Of course this is kind of mixing in some domain logic into your route, so it might not be the preferred way to do things, but it's what I ended up using.
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.