Ember #each won't iterate over array - ember.js

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

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.

Filtering an array in ember

Ok so I'm fairly new to programing, I know how to run a filter on a JSON Array but I cant seem to figure it out when I'm pulling the data from firebase and viewing it in an Ember app.
this is my route.js code:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('user');
}
});
This is my template.hbs code the href="#!" is the generic from materialize for the dropdown buttons:
<div class="list-wrapper col-xs-10 col-xs-offset-1">
<div class="col-xs-12 button-div">
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li>Female</li>
<li>Male</li>
<li>All</li>
{{/md-btn-dropdown}}
</div>
{{#each model as |info|}}
<div class="col-xs-3 user-card">
<div class="card-info">
<ul>
<li>Last Name- {{info.lastName}}</li>
<li>First Name- {{info.firstName}}</li>
<li>Gender- {{info.gender}}</li>
<li>{{info.email}} </li>
</ul>
</div>
</div>
{{/each}}
</div>
{{outlet}}
This is my controller.js code which I no is all wrong:
import Ember from 'ember';
export default Ember.Controller.extend({
customFilter: function(gender) {
return function(el) {
var r = el.user;
return r.gender === gender;
};
}
});
and this is my model:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Model.extend({
lastName: DS.attr('string'),
firstName: DS.attr('string'),
gender: DS.attr('string'),
email: DS.attr('string')
});
I've searched high and low and I'm sure I'm just missing something basic and stupid. What I want is for the dropdown menu to be able to filter and display only female, male or all. Again I'm new to this stuff so I apologize if this is a pretty basic thing. Thank You
What your missing is an action that updates your controller when an item in the dropdown is actually selected.
Some helpful reading:
Triggering changes with actions
Computed Properties
Here's how to put actions in your dropdown component
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li><a {{action "filterUpdated" "female"}}>Female</a></li>
<li><a {{action "filterUpdated" "male"}}>Male</a></li>
<li><a {{action "filterUpdated"}}>All</a></li>
{{/md-btn-dropdown}}
In your controller you then need to handle this action like so:
import Ember from 'ember';
export default Ember.Controller.extend({
// the people property is an alias of the model object
// which essentially makes people a synonym for model
// read more http://emberjs.com/api/classes/Ember.computed.html#method_alias
people: Ember.computed.alias('model'),
// holds the currently selected gender, e.g., "female". A null value indicates there is no filter.
currentFilter: null,
/*
filteredPeople is a computed array containing people models.
The array is recomputed every time the model changes or the currentFilter changes,
see the .property() bit at the end.
read more: http://emberjs.com/api/classes/Ember.computed.html#method_filter
*/
filteredPeople: Ember.computed.filter('people', function(person/*, index, array*/) {
// this function is passed each item in the model array, i.e., the person argument
// there's no need to use the index nor array arguments, so I've commented them out
if(this.get('currentFilter') === null) {
// when the filter is null, we don't filter any items from the array
return true;
} else {
// otherwise only return true if the gender matches the current filter
return person.gender === this.get('currentFilter');
}
}).property('people', 'currentFilter'),
actions: {
filterUpdated: function (value) {
if (Ember.isEmpty(value)) {
// an empty value means we should clear the filter
this.set('currentFilter', null);
}
else {
this.set('currentFilter', value);
}
}
}
});
Finally, edit your template to change
{{#each model as |info|}}
to
{{#each filteredPeople as |info|}}
Also at a meta level, don't apologize for asking questions! Everyone is new at something at somepoint, and often asking is the best way to learn. That's what stackoverflow is all about :)
Something like this would work:
gender: 'All',
filteredModel: Ember.computed.filter('model', function(person) {
return person.gender === this.get('gender');
}).property('gender'),
this assumes that it starts on all, and then when the dropdown changes the value of gender, then the filteredModel will get updated. You can then in your hbs file change the result to:
{{#each filteredModel as |info|}}

Where and how to include Ember model functions

I am working on a simple Ember app that shows ID's for list elements. The ID's are computed with simple functions based on the element sequence (and some other simple factors). For example:
<div class="content-id element">[ELEMENT ID]</div>
<div class="content-name">{{element.name}}</div>
I don't want to hardcode the element id's into the JSON object, but rather compute them in a simple function. Would it be best to place this function in the route object or in the component? What's the syntax?
Thank you.
If you want to compute something based only on model properties, you should use a computed property. In following example I define a computed property citiesString:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Model.extend({
name: DS.attr('string'),
cities: DS.hasMany('city', { async: false }),
citiesString: Ember.computed('cities', function () {
var cities = [];
this.get('cities').forEach(function (item, index, enumerable) {
cities.push(item.get('name'));
}, this);
return cities.join(', ');
})
});
You can use computed property like any other:
{{#each elements as |element index|}}
{{element.citiesString}}
{{/each}}
If you want to use element's index (if I understand "element sequence" right), I can't think out other way than writing a helper, pass to it element, index, and return ID. Example of helper:
//app/helpers/my-helper.js
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function (element, index) {
return index; //do something useful instead of this
});
How to use it:
{{#each elements as |element index|}}
{{my-helper element index}}
{{/each}}
You should keep in mind that index can change (as can order of element in array) and different users may see different ID for the same element.

ember: Strange behaviour on {{#each ..}} with itemController

At one of our many emberjs-apps I'm running into problems while updating from an old AppKit structure to ember-cli 0.2.6 with ember 1.12.1. In this project every {{#each item in myarray itemController="my-item"}}raises:
Uncaught Error: Assertion Failed: The value that #each loops over must be an Array. You passed monopoto#controller:array:, but it should have been an ArrayController
To get to the essence I simplified things to:
foo.js:
export default Ember.Controller.extend({
myData: [1,2,3]
});
foo.hbs:
{{#each item in myData}}
{{item}}
{{/each}}
This works fine and delivers: 123
If I add an item controller like this:
foo-item.js:
export default Ember.Controller.extend({
foo: function(){
return "bar" + this.get("model");
}.property("model")
});
and modify the {{each}} to use that controller:
{{#each item in myData itemController="foo-item"}}
{{item.foo}}
{{/each}}
the error occurs.
I did the same on another ember project and everything works fine with using an item-controller like this. I testet this with serveral ember versions on both projects. One fails always and the other one works always. Any Ideas?
A controller can't take a number. It can only take objects.
This should work.
export default Ember.Controller.extend({
myData: [{ value: 1 },{ value: 2 },{ value: 3 }]
});
myData is attached to your controller instance, not the array controller. If I understand correctly your problem you need to do something like:
{{#each ctl in controller itemController="foo-item"}}
{{ctl.foo}}
{{/each}}
Let me know if this solves your issue.

How does one access model data in a router/controller?

Bear with me please, I'm new.
Been breaking my head over this problem and sort of here as last resort. It's about how to access a model's data when that route loads. For instance, when /meals/2 loads, I want a function to run that sets the background of the document using that model's background-image string property. Or when /meals loads, the a function that uses a property of the collection's first item.
Any help on 'the ember way' to do this would be much appreciated.
Menu.hbs
{{#each meal in model}}
<span {{action 'mealSelected' meal.image_large}}>
{{#link-to 'menu.meal' meal tagName="li" class="meal-block" href="view.href"}}
[...]
{{/link-to}}
</span>
{{/each}}
<div id="meal-info-wrapper">
{{outlet}}
</div>
Model:
export default DS.Model.extend({
name: DS.attr('string'),
image: DS.attr('string')
});
Router.js
export default Router.map(function() {
this.route('about');
this.route('menu', { path: '/' }, function() {
this.route('meal', { path: '/meal/:id/:slug' });
});
});
routes/menu.js
export default Ember.Route.extend({
model: function() {
return this.store.find('menu');
},
afterModel: function() {
Ember.$(document).anystretch('temp-images/bg-1.png');
}
});
What I want to do in routes/menu.js for instance would be to have that image url be supplied by the model.
afterModel will run only once the model has been resolved, and the model is passed as an argument. So, based on my understanding of your app, you can adjust your routes/menu example to:
export default Ember.Route.extend({
model: function() {
return this.store.find('menu');
},
afterModel: function(model) {
Ember.$(document).anystretch(model.get('firstObject.image'));
}
});
Correct me if I misunderstood something, what you want to do is:
Change the background image of a DOM element based on a property found
in each Model's record.
Model loading is an async operation, you want to do the image swaping once you are sure the data is loaded. You used the afterModel hook to guarantee that, but that is not enough.
You want to modify the DOM inside your template, but you need to make sure that the template has been rendered. So, the DOM manipulation logic, instead of placing it in afterModel, it belongs to the didInsertElement event that Views have.
I suggest you use a component (its a view too), something like:
// your template
{{#each meal in model}}
{{meal-component content=meal}}
{{/each}}
// the meal-component
didInsertElement: function() {
var imgURLProperty = this.get('content.imgURLProperty');
Ember.$(document).anystretch(imgURLProperty);
}
Of course, you can't copy paste any of that. It just shows you the main mechanic of how you can modify a template based on the properties of a model.