Accessing ArrayController elements using needs - ember.js

I've got the following 2 controllers:
controllers/student/index.js
import Ember from 'ember';
export default Ember.ObjectController.extend({
hasDebt: function(){
var totalCredit = this.get('totalCredit');
var totalCreditSpent = this.get('totalCreditSpent');
if (totalCreditSpent > totalCredit)
{
return true;
}
return false;
}.property('payments.#each', 'lessons.#each'),
});
controllers/students.js
import Ember from 'ember';
export default Ember.ArrayController.extend({
itemController: 'student/index',
sortProperties: ['fullName'],
sortAscending: true,
debts: function(){
var allDebts = [];
var totalDebts = 0;
this.forEach(function(student){
if (student.get('hasDebt'))
{
allDebts.push({
name: student.get('fullName'),
amount: student.get('availableCredit')
});
totalDebts += student.get('availableCredit');
}
});
return {'all': allDebts, 'total': totalDebts};
}.property('this.#each.payments', 'this.#each.lessons'),
});
And everything is working as expected. I'm able to access the hasDebt property of each element through the itemController.
Now I'd like to show the debts in a dashboard in the IndexRoute, so I've created the following additional controller, hoping to be able to access the StudentsController by using needs:
controllers/index.js
import Ember from 'ember';
export default Ember.Controller.extend({
needs: ['students'],
debts: function(){
var debts = [];
console.log( this.get('controllers.students.debts') );
this.get('controllers.students').forEach(function(student){
console.log('student');
});
return debts;
}.property(''),
});
I seem unable to access the StudentsController and any of its properties.
What am I doing wrong?

I believe that a computed property must observe a property in order to ever be populated. In your example:
controllers/index.js
export default Ember.Controller.extend({
needs: ['students'],
students: Em.computed.alias('controllers.students'),
debts: function() {
...
}.property('students.debts')
});
In this example I also made it a little easier to use Students by providing a Computed Alias mapped to students in the controller.
Debugging
It's also very handy to use the browser's console when debugging. Try running something like the following and see what comes back.
App.__container__.lookup('controller:index').get('students')
This assumes your application exists under the App namespace.

Related

Computed property on a service that accesses the store

I wrote a service for loading notifications:
import Ember from 'ember';
export default Ember.Service.extend({
sessionUser: Ember.inject.service(),
store: Ember.inject.service(),
read() {
let currentUserId = this.get('sessionUser.user.id');
return this.get('store').query('notification', {
userId: currentUserId,
read: true
});
},
unread() {
let currentUserId = this.get('sessionUser.user.id');
return this.get('store').query('notification', {
userId: currentUserId,
read: false
});
}
});
I want to change the colour of an icon in the navigation bar when there are unread notifications. The navigation bar is a component:
import Ember from 'ember';
export default Ember.Component.extend({
notifications: Ember.inject.service(),
session: Ember.inject.service(),
hasUnreadNotifications: Ember.computed('notifications', function() {
return this.get('notifications').unread().then((unread) => {
return unread.get('length') > 0;
});
})
});
And the template then uses the hasUnreadNotifications property to decide if the highlight class should be used:
<span class="icon">
<i class="fa fa-bell {{if hasUnreadNotifications 'has-notifications'}}"></i>
</span>
However, it doesn't work. Although the store is called and notifications are returned, the hadUnreadNotifications doesn't resolve to a boolean. I think this is because it returns a promise and the template can't deal with that, but I'm not sure.
Questions
Is it idiosyncratic ember to wrap the store in a service like this. I'm doing this because it feels clumsy to load the notifications in the application route just to show the count.
Why doesn't hasUnreadNotifications return a boolean?
Is it possible to make read and unread properties instead of functions, so a computed property can be created in the service to calculate the count?
Returning promise from computed property will not work. Computed properties are not Promise aware. to make it work you need to return DS.PrmoiseObject or DS.PromiseArray.
You can read other options available from this igniter article.
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Component.extend({
notifications: Ember.inject.service(),
session: Ember.inject.service(),
hasUnreadNotifications: Ember.computed('notifications', function() {
return DS.PromiseObject.create({
promise: this.get('notifications').unread().then((unread) => {
return unread.get('length') > 0;
})
});
})
});

Shared data between various controllers

I have a project where I need to build an Ember application. The application will have many routes and some of the routes will have some model.
My problem at the moment is that some information is global, meaning they are present in each page (.hbs) and I need to update it periodically.
I've tried to put information on the application route like the following but it didn't work, the content is not accessible on other routes:
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON('/api/users/current')
}
});
I've also tried to reload the information with a setInterval but this didn't work either.
import Ember from 'ember';
export default Ember.Route.extend({
init: function() {
var thyself = this;
var interval = setInterval(function(){
thyself.my_reload()
}, 1000);
this.set('interval', interval);
this.set('counter', {});
},
my_reload: function() {
var counter = this.get('counter');
if (counter >= 10) {
clearInterval(this.get('interval'));
}
this.set('data', Ember.$.getJSON('/api/users/current'));
}
});
Where can I place this information so it will be available on all routes? And how can I reload the information periodically?
I'm using ember-cli
#NicosKaralis,
you should use service for it.
You can generate it by command: ember generate service name-of-service
And there you should create methods.
When you want to get access from your controller you should inject it in your controller:
nameOfService: Ember.inject.service(), (remember camelCase here)
and if you want some method from your service in your controller you will use it like this (example with computed property, you can also use it without computed property):
someComputedFunctionInController: Ember.computed(function() {
this.get('nameOfService').yourFunctionFromService();
},
nextComputedFunctionInController: Ember.computed(function() {
this.get('nameOfService.getSomethingFromService');
}
for more:
https://guides.emberjs.com/v2.7.0/tutorial/service/
Hope, it will help you.

Getting data out of Ember-data

I am using Ember 1.13.9 an Ember-data 1.13.11 and struggling to have Ember Data do what I would like. As an example, I have a model called "goal" and a
goals: Ember.on('init', Ember.computed(function() {
const {store} = this.getProperties('store');
return store.findAll('goal');
})),
When this runs it does query the database and put the appropriate records into the store BUT getting them out of the store is my problem. I would have thought that once the Promise resolved that I'd be able to iterate over the array of results. Using the inspector I can see that at clients.goals.content.content (where clients is the name of the server I see this from the inspector:
First of all this is pretty deep into the structure. I was hoping Ember's "get" would allow me to simply say something like data.get('content.0.id') but this just comes back as undefined. Second of all the crazy structure continues in that each of these listed objects are InternalModel objects which only have the following structure to them:
Note that:
there are two InternalModels, that is the right number (matches store results)
the id property is available here
there is an internal property called _data which has the other attributes of the record
Ok so in a completely hacky way I could pull out what I need but surely I shouldn't be writing code like:
_goals: Ember.on('init', function() {
const {store} = this.getProperties('store');
store.findAll('goal').then(data => {
let result = [];
data.forEach(item => {
let record = item.get('data'); // this gets what's in _data apparently
record.id = item.get('id');
result.push(record);
}
this.set('goals', result);
}),
Yuck. What am I missing?
If you need to convert Ember model to plain object you can use Model.serialize or Model.toJSON methods.
Update:
If you need to not just extract the data from models but to access fetched models via computed property, there are several ways to implement it.
1) Synchronous property (collection):
Controller:
import Ember from 'ember'
export default Ember.Controller.extend({
goals: [],
someProperty: Ember.computed('goals.#each', function () {
var goals = this.get('goals');
goals.forEach(goal => {
console.log( goal.get('someProperty') );
});
})
});
Route:
import Ember from 'ember'
export default Ember.Route.extend({
setupController: function (controller, model) {
this._super(controller, model);
this.store.findAll('goal').then(goals => {
controller.set('goals', goals);
});
}
});
Template:
{{#each goals as |goal|}}
{{log goal}}
{{/each}}
2) Asynchronous property (promise):
Controller:
import Ember from 'ember'
export default Ember.Controller.extend({
goals: Ember.computed(function () {
var storeGoals = this.store.peekAll('goal') || [];
if (storeGoals.length) {
return RSVP.resolve(storeGoals);
} else {
return this.store.findAll('goal')
}
}),
someProperty: Ember.computed('goals.#each', function () {
var goals = this.get('goals').then(resolvedGoals => {
resolvedGoals.forEach(goal => {
console.log( goal.get('someProperty') );
});
});
})
});
Template:
{{#each goals as |goal|}}
{{log goal}}
{{/each}}

How to load belongsTo/hasMany relationships in route with EmberJS

In my EmberJS application I am displaying a list of Appointments. In an action in the AppointmentController I need to get the appointments owner, but the owner always returns "undefined".
My files:
models/appointment.js
import DS from 'ember-data';
export default DS.Model.extend({
appointmentStatus: DS.attr('number'),
owner: DS.hasMany('person'),
date: DS.attr('Date')
});
models/person.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string')
});
templates/appointmentlist.js
{{#each appointment in controller}}
<div>
{{appointment.date}} <button type="button" {{action 'doIt'}}>Do something!</button>
</div>
{{/each }}
controllers/appointmentlist.js
export default Ember.ArrayController.extend({
itemController: 'appointment'
});
controllers/appointment.js
export default Ember.ObjectController.extend({
actions:{
doIt: function(){
var appointment = this.get('model');
var owner = appointment.get('owner'); //returns undefined
//Do something with owner
}
}
});
Now, I know I can change the owner-property to owner: DS.hasMany('person', {async: true}), and then handle the promise returned from appointment.get('owner');, but that is not what I want.
I have discovered that if I do this {{appointment.owner}} or this {{appointment.owner.name}} in the appointmentlist template, the owner record is fetched from the server. So I guess Ember does not load relationships unless they are used in the template.
I think that the solution to my problem is to use the appointmentlists route to fetch the record in the belongsTo relationship. But I can't figure out how.
Maybe something like this?
routes/appointmentlist.js
export default Ember.Route.extend({
model: function() {
return this.store.find('appointment');
},
afterModel: function(appointments){
//what to do
}
});
EDIT
I did this:
routes/appointmentlist.js
export default Ember.Route.extend({
model: function() {
return this.store.find('appointment');
},
afterModel: function(appointments){
$.each(appointments.content, function(i, appointment){
var owner= appointment.get('owner')
});
}
});
and it works, but I do not like the solution...
You are still asynchronously loading those records, so if you are fast enough you could still get undefined. It'd be better to return a promise from the afterModel hook, or just modify the model hook to do it all.
model: function() {
return this.store.find('appointment').then(function(appointments){
return Ember.RSVP.all(appointments.getEach('owner')).then(function(){
return appointments;
});
});
}
or
model: function() {
return this.store.find('appointment');
},
afterModel: function(model, transition){
return Ember.RSVP.all(model.getEach('owner'));
}
Another way to go is:
export default Ember.Controller.extend({
modelChanged: function(){
this.set('loadingRelations',true);
Ember.RSVP.all(this.get('model').getEach('owner')).then(()=>{
this.set('loadingRelations',false);
});
}.observes('model')
});
This way the transition finishes faster and the relations are loaded afterwards. The loading-state can be observed through loadingRelations.
When there are a lot of relations to load I think this gives a better UX.
You want to load all the assocations in the route, because you want to use Fastboot for search engines and better first time site opened experience.
Holding your assocation loading after primary models are loaded, might not be the best decision.
I am using a syntax to load all assocations in the route:
let store = this.store;
let pagePromise = store.findRecord('page', params.page_id);
let pageItemsPromise = pagePromise.then(function(page) {
return page.get('pageItems');
});
return this.hashPromises({
page: pagePromise,
pageItems: pageItemsPromise
});
And for this.hashPromises I got a mixin:
import Ember from 'ember';
export default Ember.Mixin.create({
hashPromises: function(hash) {
let keys = Object.keys(hash);
return Ember.RSVP.hashSettled(hash).then(function(vals) {
let returnedHash = {};
keys.forEach(function(key) {
returnedHash[key] = vals[key].value;
});
return returnedHash;
});
}
});

how to receive variables sent by transitionToRoute

I read at
http://emberjs.com/guides/controllers/
the following code:
I have a search box and want to send the value of the search box to the SearchController.
App.ApplicationController = Ember.Controller.extend({ // the initial
value of the `search` property search: '',
actions: {
query: function() {
// the current value of the text field
var query = this.get('search');
this.transitionToRoute('search', { query: query });
} } });
How can i get the query parameter in the SearchController and then show it in search.hbs?
I am working with ember- cli.
The router is
import Ember from 'ember';
var Router = Ember.Router.extend({
location: NENV.locationType
});
Router.map(function() {
this.route('search');
});
export default Router;
I set up a route under routes/search.js
export default Ember.Route.extend({
model : function (params) {
console.debug("hi");
return params;
},
setupController: function(controller,model) {
var query = model.query;
console.debug("query is");
console.debug(query);
}
});
When debugging i get an error:
ember More context objects were passed than there are dynamic segments
Thanks,
David
You need to define your search route to be dynamic, so if you change your route definition to something like this
Router.map(function() {
this.resource('search', {path: '/search/:query});
})
This should work as you are expecting. Let me know if anything.
Cheers!