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

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.

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

In Ember, why does my template want {{model.key}}, not just {{key}}?

I am trying out Ember, and finding a discrepancy with the docs. I used the Ember CLI to ember generate template index and ember generate route index. Then I set up a trivial model in index.js:
model: function () {
return {name: "Joe"};
}
From my reading of the docs and examples, I expected to be able to access this value simply with {{name}} in my index.hbs template, but instead I only get the value with {{model.name}}. Why?
Before Ember 1.11 you could use ObjectController, that works like a proxy to corresponding route model, and you could write {{name}} for model.name.
ObjectController was deprecated in Ember 1.11, details here:
http://emberjs.com/deprecations/v1.x/#toc_objectcontroller. So in last Ember versions you should use Controller class instead ObjectController, that doesn't work as proxy of model. You could think of it as of Ember Object with model property from corresponding route. So {{name}} means property of Controller, {{model.name}} - property of model.
For example:
//route
model: function () {
return {name: "Joe"};
}
//controller
import Ember from 'ember';
export default Ember.Controller.extend({
name: 'Marry'
});
//template
{{name}} //=> Marry
{{model.name}} //=> Joe
I think this might be a thing about explicitness but I'm not 100% sure - you can also have data sent to the template on a property other than model so it might be about allowing that to be more easily understood - model is a poor property name IMO anyway
You could use the with helper if the syntax is too verbose for you:
{{#with story}}
<div class="intro">{{{intro}}}</div>
<div class="body">{{{body}}}</div>
{{/with}}

How can I know which item in a handlebars each loop triggered a function in my Ember controller?

I am new to Ember, and I am trying to set up a list of folders. When you click on the icon next to a folder, it will load (i.e. find('folder', folder_id) ) the child folders. If the top level folder has 16 sub-folders, I am trying to set a property on those sixteen folders as they are finished loading -- so if the model for one of the sub-folders is finished loading, I want to set a property on it while the other fifteen folders are still being retrieved and serialized.
In my folder model:
import DS from 'ember-data';
export default DS.Model.extend({
files: DS.hasMany('file'),
children: DS.hasMany('folder', { inverse: 'parent', async: true }),
parent: DS.belongsTo('folder', {inverse: 'children'}),
name : DS.attr('string'),
nodeId : DS.attr('string'),
classId : DS.attr('string'),
parentId: DS.attr('string'),
contents: DS.attr(),
isVisible: DS.attr('boolean'),
childName: DS.attr('string')
});
In my template/view:
{{#each child in children}}
{{#if child.isLoading}}
Loading -->
{{else}}
{{setChildProperty}}
{{/if}}
{{/each}}
In my controller:
import Ember from 'ember';
export default Ember.Controller.extend({
children: function() {
var model = this.get('model');
var children = model.get('children');
return children;
}.property(),
setChildProperty: function(){
// how can I know, here in the controller, what the index is for
// the child that triggered this function, so that I can set a
// property on it without getting some type of
// 'didSetProperty / root.state.loading' error.
// The code below will cause errors because not all of the
// children have finished loading:
// var model = this.get('model');
// var self = this;
// var children = model.get('children');
// var contents = model.get('contents');
//
// children.forEach(function(item, index){
// var folderName = contents[index].folder;
// item.set('name',folderName);
// });
}.property('children.#each.isLoading'),
});
My Ember-CLI version is 0.1.15
Any help would be greatly appreciated.
UPDATE
In regards to mpowered's solution, the real problem is the nature of my folder models, in that the folder model does not have a name property, instead it has a list of child names. And since the child relationships are retrieved asynchronously when a user clicks on a sub-folder, I need to get the child folder names from another array, the contents array, which has identical indices. So using mpowered's solution my problem would be like so:
foldr: {{folder.id}}<br>
{{#each child in folder.children}}
{{#view 'toggle-list'}}
<i {{bind-attr id="child.id"}} class="fa fa-caret-right"></i>
{{/view}}
Index: {{_view.contentIndex}}
<!-- I need to be able to echo the above index in the
folder.contents array to get the child name.
-->
<!-- these work when uncommented, but I need a dynamic solution
name: {{folder.contents.[1].folder}}
name: {{folder.contents.1.folder}}
-->
<!-- None of these work:
name:{{!folder.contents.[_view.contentIndex].folder}}
name:{{!folder.contents.index.folder}}
name:{{!folder.contents.[index].folder}}
name:{{!folder.contents.{{!_view.contentIndex}}.folder}}
-->
Child:{{child.id}}..
<br>
<div {{bind-attr id="child.childName"}} class="folder-child hidden">
{{#if child.isVisible}}
isVisible is true<br>
{{folder-tree-component folder=child}}
{{/if}}
</div>
{{/each}}
I should also note that I am using a PODS structure and I have no control over the JSON response I get from the server to populate my models (other than Ember serializers of course).
There are many things that are concerning about this.
First, properties are not actions. You don't EVER want to change the state of an object when you're getting a property unless you have very very good reasons for doing so, or if you're implementing a getter/setter pattern. Delete setChildProperty, because that's all bad. In the template, you should just be displaying the property, not trying to "do" anything with it.
Second, this should probably be created as a component, because it sounds like the recursive structure you have here would lend itself well to reusable components. Something like folder-tree-component.hbs:
{{folder.name}}
{{#each child in folder.children}}
{{folder-tree-component folder=child}}
{{/each}}
And in your main route:
{{folder-tree-component folder=model}}
// Or, alternatively
{{#each child in model.children}}
{{folder-tree-component folder=child}}
{{/each}}
If I understand you correctly, you want a computed property on your model, not to "set" something on the model (or the controller/component) when it's finished loading. When the property is requested, it will compute the value and cache it in case you ask for it again. On your model:
name: function() {
// something with this.get('contents')
}.property('contents', 'otherDependency') // <- These will tell Ember to recompute the property when changed
I would learn more about ember fundamentals before trying to tackle this, there are some very simple, yet crucial things to learn about how Ember ticks, and a file tree isn't the simplest implementation to begin with.

No longer able to bind to global data structures in Ember?

I've been using some Ember objects in my code, such as "App.SelectedBlock" to access selected items in lists (a practice that started when I was using Sproutcore years ago) and now it looks like binding to these objects from the Handlebars templates is going to be deprecated and I'm not sure how to go about fixing that. I'm running Ember 1.8.1 and right now it will still work but I'll get "DEPRECATION: Global lookup of App.SelectedBlock from a Handlebars template is deprecated." and I'm pretty sure it's full removed in 1.9.0. I'm not sure how to go about fixing this without having to completely restructure my code. Any suggestions?
I guess you're doing smthing like:
{{App.SelectedBlock.id}}
You should not call global variables inside Handlebars template. This is a bad practice. But you can do smthing like this:
// in your template controller
selectedBlock: function(){
return Ember.get('App.SelectedBlock');
}.property('App.SelectedBlock')
In hbs template:
{{selectedBlock.id}}
You can create a property on the controller and use that instead of looking up App.SelectedBlock globally
App = Ember.Application.create();
App.ApplicationController = Ember.ObjectController.extend({
programmers: function(){
return App.SelectedBlock.get('programmers');
}.property()
});
App.SelectedBlock = Ember.Object.create({
programmers: [
{firstName: "Yehuda", id: 1},
{firstName: "Tom", id: 2}
]
});
Then, in your template you can do:
<script type="text/x-handlebars">
<h2>Welcome to Ember.js</h2>
{{view "select"
content=programmers
optionValuePath="content.id"
optionLabelPath="content.firstName"}}
</script>
See a working example here