Controller computed property based on model only valid in loop - ember.js

I have a the following in Ember
Route
model: function() {
return this.store.findAll('competency');
}
Controller
calendarItems: Ember.computed('model', function() {
return this.get('model').map(function(competency) {
return {
'name': competency.get('title'),
'start': competency.get('endDate')
};
});
})
Template
{{#each model as |item|}}
{{log calendarItems}}
{{/each}}
{{log calendarItems}}
For some reason unknown to me the {{log calendarItems}} inside the loop displays correctly with all of the store items in the models mapped correctly. But only when the {{log calendarItems}} is not present outside the loop.
When the {{log calendarItems}} is also present outside the loop it causes all 4 log statements to return [] as though the model had nothing to map.
If {{log calendarItems}} is on its own it also returns [].
Am I missing something fundamental about Ember here?
Thanks in advance,
Ryan

This won't necessarily fix the logging, but it should fix the computed property in that it should update as records become available (if the real problem is that the objects are loading asynchronously, which is kind of my suspicion)
calendarItems: Ember.computed('model.#each.{title,endDate}', function() {
return this.get('model').map(function(competency) {
return {
'name': competency.get('title'),
'start': competency.get('endDate')
};
});
})

Related

Ember.JS data model: Filtering a computed property

I have an Ember data model logger defined as below:
import DS from 'ember-data';
import EmberObject, { computed } from '#ember/object';
export default DS.Model.extend({
someAttribute: DS.hasMany('attr'),
test: computed('someAttribute.[]', function(){
return this.get('someAttribute').filterBy('description', 'some value');
})
});
The above model gets passed as logger variable from the controller into my component template. In my template:
{{#if logger.test}}
<h1> Testing </h1>
{{log logger.test.description }}
{{/if}}
It seems like the logger.test in the template is always false. In the same template if I add the following:
{{#each logger.someAttribute as |t|}}
{{t.description}}
{{/each}}
I can see all the values being enumerated. Not sure what I am missing? Any assistance would be greatly appreciated.
Okay, I figured out. Turns out models return promises and the if statement doesn't handle promise well enough. The right way to do this would be to return a DS.promiseArray from the computed property and then everything works like a charm:
return DS.PromiseArray.create({
promise: this.get('someAttribute').then(logs => {return logs.filterBy('description')})
});
Acknowledgements: https://emberigniter.com/guide-promises-computed-properties/
I don't exactly understand what you're trying to achieve, but I would either
Load the DS.hasMany('thing', {async: false}) and make sure they are included in the store. (See https://embermap.github.io/ember-data-storefront/latest/). If the relationship is set to async: false when it's accessed, it's accessed synchronously, so there is no issues with promises.
Using ember-concurrency can help manage the loading of the records and displaying them on the page.

Ember #each won't iterate over array

I'm experiencing a really weird behaviour wherein I have an Ember array that has a length, a first object, but I can't iterate over it.
I have a session object which queries the user's team members:
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Service.extend({
store: Ember.inject.service(),
...
teamMembers: Ember.computed('token', function() {
const promise = this.get('store').findAll('teamMember', {include: 'user,organization'});
return DS.PromiseObject.create({ promise: promise });
})
});
As far as I can see this is working correctly, because when I access it from inside my template, I can access the array length, and the first object:
<p>The length of the array is {{session.teamMembers.length}}</p>
<p>The first entry in the array is {{session.teamMembers.firstObject.name}}</p>
These work perfectly, returning 2 and my own name, respectively. However, when expressed as an each statement, it returns nothing:
<ul>
{{#each session.teamMembers as |teamMember|}}
<li>{{teamMember.name}}</li>
{{/each}}
</ul>
The ul element is completely empty. If I have an {{else}} clause, the else clause appears until the promise fulfills, and then I'm left with an empty ul element. The Ember Inspector shows all the values have been loaded correctly.
If I change the method as follows:
teamMembers: Ember.computed('token', function() {
return [{name: 'Paul Doerwald', role: 'lead'}, {name: 'Justin Trudeau', role: 'member'}];
})
Then everything works as expected.
I'm clearly doing something wrong in the teamMembers method, presumably returning the wrong array type or something, but I can't figure out what.
Many thanks for your help!
For array there is DS.PromiseArray. Wrapping promise into this will make it work with each helper as is. You can use if guard around to display loading state.
//service
teamMembers: Ember.computed('token', function() {
const promise = this.get('store').findAll('teamMember', {include: 'user,organization'});
return DS.PromiseArray.create({promise});
})
// template
{{#if session.teamMembers.isFulfilled}}
{{#each session.teamMembers as |teamMember|}}
<li>{{teamMember.name}}</li>
{{/each}}
{{else}}
loading...
{{/if}}

Ember updating array does not update template

I'm going mad with this problem:
I have a component with a template that uses a {{#each}} loop to iterate an array of comments;
comments: Ember.computed('commentsModel', function() {
return this.get('commentsModel').toArray();
}),
actions: {
addOne: function() {
this.set('comments', [{content: 'agent k'}]);
}
}
The component is called like this:
{{photo-comments commentsModel=model.comments}}
Where model.comments is returned by the model hook of the route;
When I click the button that triggers "addOne" event, the template updates correctly, all the comments are removed and only the new comment "agent k" is shown;
but if I want to ADD a comment into the comments array, instead of overwriting the whole array, it doesn't work;
I get no errors but the template simply ignores it and does not updates!
I tried with push:
actions: {
addOne: function() {
this.set('comments', this.get('comments').push({content: 'agent k'});
}
}
with pushObject:
actions: {
addOne: function() {
this.set('comments', this.get('comments').pushObject({content: 'agent k'});
}
}
nothing! Since this is driving me crazy, can someone tell what's wrong with this?
Template:
{{log comments}}
{{#each comments as |comment|}}
<div class="card">
<div class="card-content">
{{comment.username}}<br>
{{comment.content}}
</div>
<div class="card-action">
{{comment.createdAt}}
</div>
</div>
{{/each}}
I think the underlying issue here is that you're misusing the Ember.computed helper.
Your component should define a default value for the comments property, if there is one:
comments: null,
actions: {
addOne: function() {
this.get('comments').pushObject({content: 'agent k'});
}
}
Your template should call the component by passing the model value:
{{photo-comments comments=model.comments}}
Assuming of course, that model.comments is available because it was retrieved by the route's model hook.
In your example code, you would have been attempting to push a new object to a computed property wrapping an array representation of a model, rather than pushing a new value to the model itself.

Observe error property on Ember.Model

I'm trying to create a component, which creates an input field for a specific field on a Ember.model.
When calling model.save() I would like to display the possible error messages regarding that field, right beneath the field. But I can't seem to get notified when my Errors property on Ember.Model changes.
Is there a way to observe the model.errors property, in order to display the correct error messages?
I tried:
.property{'model.errors'} and Ember.computed
.observes('model.errors')
.observes('model').on('becameInvalid')
I think i'm pretty close, as my errors with the solution below, are being dipslayed, however when I change my input to something else invalid, and try to save again, my original errors do not get cleared, and the new ones do not get added.
When I put breakpoints, or console.logs, I can see that the code never enters that particular section for displaying errors again, so my guess is that the computed property is not working. And my template is never updated with new errors.
Here's my code at the moment:
My component: components/inputfield-text.js
import Ember from 'ember';
export default Ember.Component.extend({
value: Ember.computed('model', 'field', {
get: function() {
return this.get('model').get(this.get('field'));
},
set: function(key, value) {
this.get('model').set(this.get('field'), value);
return value;
}
}),
errors: function(){
var errors = this.get('model.errors');
console.log(errors)
return errors.errorsFor(this.get('field'));
}.property('model.errors', 'model', 'field')
});
My Component's template: templates/components/inputfield-text.hbs
{{input type='text' value=value class='form-control' placeholder=placeholder}}
{{#each errors as |error|}}
<div class="error">
{{error.message}}
</div>
{{/each}}
And for the sake of completeness, the code I use for embedding the component in a template:
{{inputfield-text model=model field='name'}}
Found it, I had to add [] to my computed property, correct code below, notice difference between.
property('model.errors', 'model', 'field')
And
property('model.errors.[]')
Correct component code: components/inputfield-text.js
import Ember from 'ember';
export default Ember.Component.extend({
value: Ember.computed('model', 'field', {
get: function() {
return this.get('model').get(this.get('field'));
},
set: function(key, value) {
this.get('model').set(this.get('field'), value);
return value;
}
}),
errors: function(){
var errors = this.get('model.errors');
console.log(errors)
return errors.errorsFor(this.get('field'));
}.property('model.errors.[]')
});

Ember: Update ObjectController property from ArrayController action?

Disclaimer: I'm quite new to Ember. Very open to any advice anyone may have.
I have a action in a ArrayController that should set an ObjectController property. How I can access the right context to set that property when creating a new Object?
Here is abbreviated app code show my most recent attempt:
ChatApp.ConversationsController = Ember.ArrayController.extend({
itemController: 'conversation',
actions: {
openChat: function(user_id, profile_id){
if(this.existingChat(profile_id)){
new_chat = this.findBy('profile_id', profile_id).get('firstObject');
}else{
new_chat = this.store.createRecord('conversation', {
profile_id: profile_id,
});
new_chat.save();
}
var flashTargets = this.filterBy('profile_id', profile_id);
flashTargets.setEach('isFlashed', true);
}
},
existingChat: function(profile_id){
return this.filterBy('profile_id', profile_id).get('length') > 0;
}
});
ChatApp.ConversationController = Ember.ObjectController.extend({
isFlashed: false
});
The relevant template code:
{{#each conversation in controller itemController="conversation"}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.profile}} Conversation: {{conversation.id}}</h3>
other stuff
</li>
{{/each}}
I don't see why you need an object that handles setting a property for all the elements in your list. Have each item take care of itself, this means components time.
Controllers and Views will be deprecated anyway, so you would do something like:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [...];
}
});
App.ConversationComponent = Ember.Component.extend({
isFlashed: false,
actions: {
// handle my own events and properties
}
});
and in your template
{{#each item in model}}
{{conversation content=item}}
{{/each}}
So, whenever you add an item to the model a new component is created and you avoid having to perform the existingChat logic.
ArrayController and ItemController are going to be depreciated. As you are new to Ember I think that it would be better for you not to use them and focus on applying to coming changes.
What I can advice you is to create some kind of proxy object that will handle your additional properties (as isFlashed, but also like isChecked or isActive, etc.). This proxy object (actually an array of proxy objects) can look like this (and be a computed property):
proxiedCollection: Ember.computed.map("yourModelArray", function(item) {
return Object.create({
content: item,
isFlashed: false
});
});
And now, your template can look like:
{{#each conversation in yourModelArray}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.content.profile}} Conversation: {{conversation.content.id}}</h3>
other stuff
</li>
{{/each}}
Last, but not least you get rid of ArrayController. However, you would not use filterBy method (as it allows only one-level deep, and you would have the array of proxy objects, that each of them handles some properties you filtered by - e.g. id). You can still use explicit forEach and provide a function that handles setting:
this.get("proxiedCollection").forEach((function(_this) {
return function(proxiedItem) {
if (proxiedItem.get("content.profile_id") === profile_id) {
return proxiedItem.set("isFlashed", true);
}
};
})(this));