Can Emberjs computed property depends on other model property? - ember.js

I have a model:
app.ObjectOne = Em.Object.extend({
id: null,
count: null
});
And another model, which computed property 'SomeProperty' I want to depend on property 'count' from ObjectOne
app.ObjectTwo = Em.Object.extend({
id: null,
someProperty: function(){
return count+5;
}.property('app.SomeObjectController.count')
});
So, can I do it that way?
Or is there any other way to do it;
The main idea is that data of ObjectOne model comes after ObjectTwo, so I what to recompute property and rerender view

If I understand well, the behavior you want is exactly what Ember.js can bring to you.
First, here is a very basic template:
<script type="text/x-handlebars">
Here, the template is updated each time this property is updated (equivalent to bound property)
{{App.objectTwo.someProperty}}
</script>​
Here is the javascript. I don't know if you really want to use an ObjectController here, but you mention it, so I have use it. An ObjectController's act as a proxy around it's content property. That means here that someObjectController.get('count') will try to get the count property from the controller itself, and if it does not exist, then the controller retrieve the count property from its content.
App = Ember.Application.create();
App.someObjectController = Ember.ObjectController.create();
App.ObjectOne = Em.Object.extend({
id: null,
count: null
});
App.objectOne = App.ObjectOne.create({
id: 1,
count: 42
});
App.someObjectController.set('content', App.objectOne);
App.ObjectTwo = Ember.Object.extend({
id: null,
someProperty: function(){
return App.someObjectController.get('count')+5;
}.property('App.someObjectController.count')
});
App.objectTwo = App.ObjectTwo.create({
id: 1
});
//in 3 seconds the count property is set to 0. You can see the template is updated
Ember.run.later(function(){
App.objectOne.set('count', 0);
},3000);
Here is the working jsfiddle: http://jsfiddle.net/Sly7/M3727/4/

Related

EmberJS, polling, update route's model, re-render component

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

Accessing component property in parent controller in Ember

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.

Ember computed.alias in nested views

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

Unit testing Ember.Object and DS.Model metaForProperty is inconsistent

I'm working on building up unit tests for our Ember applications. The current set of tests I'm targeting are our Models. I've got a set of tests that work really well for Models based on Ember Data, but seem to fail when based on Ember.Object. Here are two examples:
App.Person = DS.Model.extend({
First: DS.attr("string"),
Last: DS.attr("string")
});
App.Person2 = Ember.Object.extend({
First: null,
Last: null
});
And the test which passes for DS.Model:
it('has a valid attribute: First', function() {
var property = App.Person.metaForProperty('First');
expect( property.type ).to.eql('string');
expect( property.isAttribute ).to.eql(true);
});
Then, when using the same structure for Ember.Object:
it('has a valid attribute: First', function() {
var property = App.Person2.metaForProperty('First');
});
I get the following error:
Error: Assertion Failed: metaForProperty() could not find a computed property with key 'First'.
at new Error (native)
at Error.Ember.Error (http://0.0.0.0:3385/app/js/components/ember/ember.js:844:19)
at Object.Ember.assert (http://0.0.0.0:3385/app/js/components/ember/ember.js:73:11)
at Function.Mixin.create.metaForProperty (http://0.0.0.0:3385/app/js/components/ember/ember.js:13247:11)
at Context.<anonymous> (http://0.0.0.0:3385/tests/model-person-test.js:6:35)
at invoke (http://0.0.0.0:3385/tests/bower_components/ember-mocha-adapter/adapter.js:60:8)
at Context.suite.on.context.it.context.specify.method (http://0.0.0.0:3385/tests/bower_components/ember-mocha-adapter/adapter.js:102:13)
at Test.require.register.Runnable.run (http://0.0.0.0:3385/tests/assets/mocha.js:4200:15)
at Runner.require.register.Runner.runTest (http://0.0.0.0:3385/tests/assets/mocha.js:4591:10)
at http://0.0.0.0:3385/tests/assets/mocha.js:4637:12
Can anyone offer insight as to what might be going wrong?
Yeah, First/Last aren't computed properties on Person2, they are just properties.
This would work
App.Person2 = Ember.Object.extend({
First: null,
Last: null,
Blah: function(){
}.property('First')
});
var j = App.Person2.metaForProperty('Blah');
console.log(j);
When you do DS.attr() Ember Data is actually injecting a computed property right there, see: Ember Data attr Source
Here's an updated set of assertions that work for me. It looks like because it's only using Ember.Object, the extra goodies from DS.Model simply aren't there.
it('has a valid attribute: First', function() {
var person = App.Person.create({
id: 1,
First: 'Andy'
});
expect( App.Person.proto().hasOwnProperty('First') ).to.eql(true);
expect(typeof person.get('First')).to.eql('string');
expect(person.get('First')).to.eql('Andy');
});

How can I observe Array field in Ember.Object?

I've this code (http://jsfiddle.net/stephane_klein/gyHmS/2/) :
App = Ember.Application.create({});
App.Item = Ember.Object.extend({
title: null,
parent: null
});
App.MyList = Ember.Object.extend({
title: null,
content: [],
changed: function() {
console.log('here');
}.observes('content')
});
App.list = App.MyList.create({
title: "foobar",
content: [
App.Item.create({
item: "item1"
}),
App.Item.create({
item: "item2"
})
]
});
console.log(App.list.content);
App.list.content.pushObject(
App.Item.create({
item: "item3"
})
);
console.log(App.list.content);
Why "console.log('here')" is never called ?
I want set App.Item.parent when App.Item is inserted in App.MyList. I don't know how to observe App.MyList.content field.
Thanks for your help.
Best regards,
Stephane
You're not changing the content property, you're just pushing an object in there.
You have two solutions:
You can observe each item of the content (using .observes('content.#each')), but take attention, the method could be called several times
Or manually notify that this property has changed (using this.notifyPropertyChange('content'))
Here is the first solution: jsfiddle using #each
And here is the second solution: jsfiddle using notifyPropertyChange
You also have to notice you should not use directly App.list.content but App.list.get('content') instead. Take a look at this article written by Roy Daniels if you want more information.
EDIT
Please notice the use of #each has slightly changed. The Ember.Array##each documentation says:
Returns a special object that can be used to observe individual
properties on the array. Just get an equivalent property on this
object and it will return an enumerable that maps automatically to the
named key on the member objects.
If you merely want to watch for any items being added or removed to
the array, use the [] property instead of #each.
Lets see that with an example:
App.Post = Ember.Object.extend({
createdAt: null
});
App.Blog = Ember.Object.extend({
posts: null,
init: function() {
this._super();
this.set 'posts', [];
},
newerPost: function() {
return this.get('posts').sortBy('createdAt').get('firstObject');
}.property('posts.#each.createdAt'),
postsCount: function() {
return this.get('posts.length');
}.property('posts.[]')
});
newerPost needs to observes a particular property of each posts, whereas postsCount just needs to know when the posts array changes.