How do I properly call an associated model in a component? - ember.js

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.

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

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.

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

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

setting model from route upon action received in ember.js

I am trying to set a model value from an action received by my route.
//app/routes/map.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return {
trail: null
};
},
actions: {
event: function(name, value) {
if (name === 'trail.selected') {
this.modelFor('map').set('trail', value);
}
}
}
});
when I try to use
this.modelFor('map').set('trail', value);
I get the following error:
Uncaught TypeError: this.modelFor(...).set is not a function
When I try to use
this.modelFor('map').trail = value;
I get that error
Uncaught Error: Assertion Failed: You must use Ember.set() to set the trail property (of [object Object]) to <nested-component#model:mtg-trail::ember617:kvdpo>.
EDIT Added template
//app/templates/map.hbs
<h2>Trail's name: {{trail.name}}</h2>
{{#if trail}}
{{map-draw trail=trail.id}}
{{/if}}
Your routes model isn't an ember object so set won't work. Try:
model: function() {
return Ember.Object.create({
trail: null
});
},
Also, changing the models content from an action should really be done on the controller.
Well, since the action you are calling is on the 'map' route itself, why not just:
this.set('model.trail', value);