Ember JS: How to set an attribute in parent component? - ember.js

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.

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

How to make Ember computed array based on DS.hasMany property update when a new record is created?

DEBUG: -------------------------------
DEBUG: Ember : 2.2.0
DEBUG: Ember Data : 2.1.0
DEBUG: jQuery : 2.1.4
DEBUG: Model Fragments : 2.0.0
DEBUG: Ember Simple Auth : 1.0.0
DEBUG: -------------------------------
My model has a computed property upcomingReminders that filters and sorts the DS.hasMany property, reminders.
# app/models/job.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
// ...
reminders: DS.hasMany( 'reminder', { "async": true } ),
undoneReminders: Ember.computed.filterBy('reminders', 'done', false),
upcomingReminders: Ember.computed.sort('undoneReminders', 'undoneSorting')
});
I create a new record in the route like so:
# app/routes/dashboard/jobs/show/reminders/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
let newRecord = this.get('store').createRecord( 'reminder' , {
target: this.modelFor("dashboard.jobs.show")
});
return newRecord;
}
});
And then save it from the controller like so:
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
save() {
this.get('model').save().then( (model) => {
let target = model.get('target');
this.transitionToRoute('dashboard.jobs.show.reminders', target );
});
}
}
});
The template looks like this:
<div class="valign-wrapper right">
{{#link-to "dashboard.jobs.show.reminders.new" model class="right"}}
{{md-btn text="Add Reminder" icon='mdi-content-add' class='green'}}
{{/link-to}}
</div>
{{#each model.upcomingReminders as |reminder|}}
{{reminder-widget reminder=reminder}}
{{else}}
There are no upcoming reminders
{{/each}}
{{outlet}}
When I save the model, it won't show up in the list until I refresh the page, although it will show up in a list of all reminders immediately, and shows up in the list if I change the each block to iterate over model.reminders (the raw DS.hasMany property, as opposed to the sorted & filtered property). Even if I navigate to a route with a list of all reminders, and then return, it won't be there 'til after a refresh.
So how can I trigger a rerender of this computed property?
Make sure, you also have a belongsTo('job') in your reminder model. If that doesnt already do the job, try the following in your controller:
export default Ember.Controller.extend({
actions: {
save() {
this.get('model').save().then( (model) => {
let target = model.get('target');
this.get('store').find('job', model.get('job.id')).then((job) => {
job.get('reminders').pushObject(model);
this.transitionToRoute('dashboard.jobs.show.reminders', target)
});
});
}
}
});

How do I properly call an associated model in a component?

I get undefined when I do the console.log(this.get('item.product.description')); in the component object and an error message in ember inspector, console tab:
Uncaught TypeError: Cannot read property 'length' of undefined
I suspect this is happening because item.product.description is coming from a promise (async call). On page load, the promise isn't fulfilled yet. However, what I don't want to do, is create a .then block in the component, like:
this.get('item.product').then((product) => {
This just makes the component not so isolated, since it expects item.product to be a promise, instead of an actual string.
What other approaches should I consider?:
// Route
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return this.store.find('user', params.user_id);
}
});
// Template
{{#each item in model.jobOrders.lastObject.checkout.items}}
{{product-card item=item}}
{{/each}}
The component:
// Component template
<p class="name">{{item.product.name}}</p>
<p class="description">{{truncatedDescription}}</p>
// Component object
import Ember from 'ember';
export default Ember.Component.extend({
truncatedDescription: Ember.computed('item.product.description', function() {
console.log(this.get('item.product.description'));
var truncatedText = this._truncate(this.get('description'), 48);
console.log(truncatedText);
return truncatedText;
}),
actions: {
// ...
},
// Private
_truncate(text, limit) {
if (text.length > limit){
text = text.substr(0, limit - 3) + '...';
}
console.log(text);
return text;
}
});
One possibility would be to pass the description itself to the component instead of the item.
// Template
{{#each item in model.jobOrders.lastObject.checkout.items}}
{{product-card description=item.product.description}}
{{/each}}
This way, when item.product.description resolves, it will update the computed property truncatedDescription in your component.

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

Ember - How to set a variable when in admin route

I want to set some state variable which can be used to add a css class to my header template only when I am in the admin section of my app (i.e. any url beginning with /admin). I have the following routes:
this.resource('admin', { path: '/admin' }, function() {
this.route('dashboard', { path: '/' });
// more admin routes...
}
this.route('user')
// more routes...
Here is the setupController in my ApplicationRoute:
App.ApplicationRoute = App.Route.extend({
setupController: function(){
this.controllerFor('header').set('isInAdmin', false);
}
}
And the same in AdminRoute:
App.AdminRoute = App.Route.extend({
setupController: function(){
this.controllerFor('header').set('isInAdmin', true);
}
});
This works fine but once I have navigated into an admin route, and navigate back to a non-admin route, I'm not sure how to reset the isInAdmin variable.
The application controller have a computed property called currentPath. That property is equal the current path, for example, transitioning to admin/dashboard will return admin.dashboard in currentPath, going to /foo return foo etc.
So you can use the following code to know when a admin route is entered:
App.ApplicationController = Ember.Controller.extend({
isInAdmin: function() {
var currentPath = this.get('currentPath');
// if the first hierarchy is admin, so is a admin route
return currentPath.split('.')[0] === 'admin';
}.property('currentPath')
});
And in your application template isInAdmin will be avaliable:
<script type="text/x-handlebars" data-template-name="application">
...
<div {{bind-attr class=isInAdmin}}>
...
</div>
...
</script>
Give a look in that fiddle to see this in action http://jsfiddle.net/marciojunior/j5PRe/
I just read about the deactivate hook on Ember.Route:
http://emberjs.com/api/classes/Ember.Route.html#method_deactivate
This hook is executed when the router completely exits this route. It
is not executed when the model for the route changes.
So I added:
App.AdminRoute = App.Route.extend({
deactivate: function(){
this.controllerFor('header').set('isInAdmin', false);
}
});
which also works, however Márcio's answer is probably a little neater