Computed property on a service that accesses the store - ember.js

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;
})
});
})
});

Related

The proper way to store/load statics in Ember App

currently I'm thinking of the way to load statics into my Ember app.
The problem:
I have app branded logo, app name, app title (browser tab label), texts for routes etc.
What I'm doing now is the following:
model() {
let defaultHeaderModel = {
logo: '/img/logo-cloud.svg',
brand: {
name: 'CloudCenter',
logo: '/img/logo-cloud.svg'
},
userLinks: [{
text: 'Logout',
route: 'logout'
}],
navigation: [{
text: 'Login',
route: 'login'
}]
};
}
As you can see all of the values are hardcoded. What I'd like to do is to somehow load that "statics" and use them through some variables. For ex: header.logo = resources.logo.
My thoughts:
1) Use environment - store all of that values in the config.js and import it where needed. Cons: not sure if that data belongs to environment
2) ES6 POJO which can be imported to the app.
3) .json and some staticsService which will load .json file and through it I will have access to that values.
Are there any standardized approach to do such things? Or maybe better suggestions?
You can create service, and have method(loadData) which will return Promise and will be resolved with your JSON data and update property in service. You need to call loadData in beforeModel hook, after the all the promises resolved only then it will move to model hook.
Refer twiddle basic demonstration
services/my-service.js
import Ember from 'ember';
export default Ember.Service.extend({
defaultHeaderModel:{},
loadData(){
var myServiceDataLoadPromise = Ember.RSVP.Promise.resolve({one:1});
myServiceDataLoadPromise.then(result =>{
this.set('defaultHeaderModel',result);
});
return myServiceDataLoadPromise;
}
});
routes/application.js
inside beforeModel hook, you can load service with data, it can be done any of the route which requires data.
import Ember from 'ember';
export default Ember.Route.extend({
myService: Ember.inject.service(),
beforeModel()
{
return Ember.RSVP.all([this.get('myService').loadData()]);
}
});
controllers/application.js
import Ember from 'ember';
export default Ember.Controller.extend({
myService: Ember.inject.service(),
appName: 'Ember Twiddle'
});
templates/application.hbs
{{myService.defaultHeaderModel.one}}

How do I access the variable of a dynamic route in EmberJS

I've done
ember g route auth
ember g route auth/pending
Which then gave me :
app/
routes/
auth/
pending.js
auth.js
and my router has
this.route('auth', function() {
this.route('pending', { path: '/pending/:steamid/:token'});
});
Which everything is fine, when I visit
http://localhost:4200/auth/pending/1/2
The page loads, but how do I access :steamid and :token outside of the model.
I'd like to use it so that I can set values in my session service
Like:
import Ember from 'ember';
export default Ember.Route.extend({
session: Ember.inject.service(),
steamID: this.get(// Params Some How),
token: this.get(// Params some How)
thing(params) {
this.get('session').set('tokenID', token),
this.get('session').set('steamID', steamID)
}
});
^^ Pseudo code to express what I'm trying to accomplish.
While it's not in the website documentation, looking at the source code of the Transition object passed to some Route hooks (e.g. afterModel and beforeModel) it have a params property which contains the dynamic segment params.
So you can, for example:
import Ember from 'ember';
export default Ember.Route.extend({
session: Ember.inject.service(),
thing(params) {
// Do some check and returns the result
},
beforeModel (transition) {
if (!this.thing(transition.params)) {
transition.abort();
this.transitionTo('/login');
}
}
});
You can set them in your service from many different hooks:
import Ember from 'ember';
export default Ember.Route.extend({
session: Ember.inject.service(),
/* Access from beforeModel */
beforeModel(transition) {
this.get('session').setProperties({
tokenID: transition.params.token,
steamID: transition.params.steamid
});
},
/* Access from model */
model(params, transition) {
this.get('session').setProperties({
tokenID: params.token,
steamID: params.steamid
});
}
});
If you ask me model hook is the best choice. Especially if you want your query params to refresh the model every time they change (see guide).

Ember JS: How to set an attribute in parent component?

My router.js:
Router.map(function() {
this.route('dashboard', { path: '/' });
this.route('list', { path: '/list/:list_id' }, function() {
this.route('prospect', { path: 'prospect/:prospect_id', resetNamespace: true });
});
});
list.hbs:
<div class="content-wrapper">
<div class="content">
{{prospect-list name=model.name prospects=model.prospects openProspect="openProspect"}}
</div>
</div>
{{outlet}}
prospect-list.hbs:
{{#each prospects as |prospect|}}
{{prospect-item prospect=prospect openedId=openedProspectId openProspect="openProspect"}}
{{/each}}
prospect-item.hbs
<td>{{prospect.firstName}}</td>
<td>{{prospect.list.name}}</td>
components/prospect-list.js
import Ember from 'ember';
export default Ember.Component.extend({
openedProspectId: 0,
actions: {
openProspect(prospect) {
this.set('openedProspectId', prospect.get('id'));
this.sendAction('openProspect', prospect);
}
}
});
components/prospect-list.js
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'tr',
classNameBindings: ['isOpen:success'],
isOpen: Ember.computed('openedId', function() {
return this.get('openedId') == this.get('prospect').id;
}),
click: function(ev) {
var target = $(ev.target);
if(!target.is('input'))
{
this.sendAction('openProspect', this.get('prospect'));
}
}
});
Everything works good when I start application in browser with http://localhost:4200, but when I start from http://localhost:4200/list/27/prospect/88 currently loaded prospect (with id 88) is not highlighted in prospect list, because initial openedProspectId set 0.
How can I set openedProspectId in these case?
I can get these id in routes/prospect.js like:
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model(params) {
return this.store.findRecord('prospect', params.prospect_id);
},
afterModel(params) {
console.log(params.id);
}
});
but how I can pass it to openedProspectId? Or should I build my application on another way?
There are a few things that could be reworked here. I would start by using the link-to helper instead of sending actions when the prospect is clicked. This will give you a uniform starting point (the route) and allows the user to open the prospect in a new window if they decide to.
The route will naturally set the property model on the controller. You could pass this into the individual prospect-item components as activeProspect. Then within that component just compare prospect.id == activeProspect.id to determine if the row should be highlighted.
It does seem odd to me to have a separate route to highlight a prospect, but I'm unaware of your business requirements. You might consider using queryParams to produce a url like this list/27?prospect=88 and reserve the route for the 'full view' of the prospect.

Ember setupController and transient controllers

I'm trying to use setupController method to pass some data to the controller from the route and it only works if the controller is a singleton.
The setupController method is called in both situations but the variables are only set on the controller if it's a singleton.
How can I pass data from a route to a transient controller?
Here's a twiddle:
http://ember-twiddle.com/ba55734e925664e363f4
Uncomment/comment the following line to toggle between singleton/transient:
//application.register('controller:application', 'ApplicationController', { singleton: false });
I have not been able to find any information about whether or not this should work. I'm using Ember 1.13.6.
controllers/application.js:
import Ember from 'ember';
export default Ember.Controller.extend({
appName:'Ember Twiddle'
});
initializers/application.js:
export function initialize(container, application) {
//application.register('controller:application', 'ApplicationController', { singleton: false });
}
export default {
name: 'application-init',
initialize: initialize
};
routes/application.js:
import Ember from 'ember';
export default Ember.Route.extend({
setupController: function(controller,model) {
this._super(controller,model);
controller.var1 = "Variable 1";
}
});
templates/application.hbs:
<h1>Welcome to {{appName}}</h1>
<br>
<br>
{{var1}}
<br>
This appears to be an actual bug, since the instance of the controller is different from the instance you have in setupController and the one backing the view.
A workaround would be overriding the renderTemplate hook on your route to pass the instance of the controller versus a string reference which is looked up by default (and creating a new instance of the controller!).
export default Ember.Route.extend({
setupController(controller, model) {
this._super(...arguments);
controller.set('var1', 'foo');
},
renderTemplate(controller, model) {
// note: don't call super here
this.render('application', {
controller: controller,
model: model
});
}
});

Accessing ArrayController elements using needs

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.