Ember: setting controller properties in init()? - ember.js

Having a hard time figuring out why controller properties added in init() are added as 'undefined' rather than with default values. I guess you're supposed to define in init() to avoid "leaking state"? I think I am missing something fundamental here.
When a controller property like an empty array is specified right on the controller it's added as 'Array[0]', which then allows you to pushObject stuff into it. When added in init() they are added as 'undefined' so pushObject fails.
See demo on this twiddle and/or the code below:
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['q','sort_method','search_type','filter1','filter2'],
init(){
this._super(...arguments);
Ember.set(this, 'q', null);
Ember.set(this, 'sort_method', 'relevance'); // <-- sets default value to 'undefined'
Ember.set(this, 'filter1', []); // <-- sets default value to 'undefined'
},
filter2: [], // <-- sets default values properly but will it cause state issues?
search_type: 'bar'
});
Basically, I'm wanting to figure out how to declare my list of query params in ONE place (like in config file), rather than 3 places (route, controller queryParams array, and to the controller itself)
I created a twiddle to illustrate what I mean.

why controller properties added in init() are added as 'undefined'
If you dig deep into ember sources, you will find a lot of code in Ember.Route that "prepare" query parameters to work (for example, you may try to analyze this code). I guess that answer to this question is somewhere in route's code (probably route initialize properties before/after controller's init is called or something like that).
Basically, I'm wanting to figure out how to declare my list of query params in ONE place (like in config file), rather than 3 places (route, controller queryParams array, and to the controller itself)
You can declare parameters in environment.js. Then, in controller import it and pass to Ember.Controller.extend
import ENV from 'project-name/config/environment';
export default Ember.Controller.extend(Ember.$.extend({}, ENV.myDefaults.controller, {
actions: {
addFilter: function(key, value) {
alert(`adding "${value.toString()}" to controller property '${key}'`);
console.log("DUMP OF EMBER 'this':", this);
this[key].pushObject(value.toString());
}
}
}));
See twiddle for details

Related

dynamic looping on a controller property

I am using ember 2.17.
I added this property to a controller:
export default Controller.extend({
newAttachments: new Array()
...
})
I add elements in it through this controller action:
setAttachment(file) {
console.log('trying');
this.get('newAttachments').push(file);
}
When I use the action, the message is displayed in the console, and in Ember inspector I can see the array is no longer empty :
However, the following code in the view has no output :
{{#each newAttachments as |file|}}
<p>in loop</p>
{{/each}}
Why is it not displaying anything? In a component it would work, why not here ?
Ember can't observe native arrays. Therefor the framework doesn't know that a value is pushed into the array. You should use ember's own Ember.NativeArray and it's pushObject method instead. That one ensures that the framework is informed if an entry is added to or removed from array. Changed code would look like this:
import { A } from '#ember/array';
export default Controller.extend({
newAttachments: A(),
setAttachment(file){
this.get('newAttachments').pushObject(file)
}
})
You shouldn't add the array as a property of an EmberObject as this might introduce a leak between instances. That's not a production issue in that case cause controllers are singletons in ember.js. But you might see strange behavior in tests. Refactoring for native classes will resolve that issues as class fields are not leaked between instances. For old EmberObject based classes initializing the value in init hook or using a computed property are common ways to deal with that issue:
// computed property
import { computed } from '#ember/object';
import { A } from '#ember/array';
export default Controller.extend({
newAttachments: computed(() => A()),
});
// init hook
import { A } from '#ember/array';
export default Controller.extend({
init() {
this._super(...arguments);
this.set('newAttachments', A());
}
});
Please note that you don't need to use get() if running Ember >= 3.1.

Create a computed bool using relationship field Ember

I'm trying figure out how my computed property identify if my relationship was set.
Project = Model.extend({
participantes: hasMany('author')
...
I need change my css based if has a author.
{{my-component project=project}}
//---------- my-component.js
export default Ember.Component.extend({
classNameBindings: ['hasParticipante'],
hasParticipante = Ember.computed('project.participantes', function(){
//the code I need gonna here
})});
This will probably not work since a relationship is always a PromiseArray or PromiseObject. Probably you can check on the content:
Ember.computed.bool('author.content')
Your component seems completely wrong:
You can't use = in Object creation.
Ember.get needs two parameters. The context and the property, like get(this, 'post').
You should not use arrow functions for computed properties since you have no access to the object then.
But you don't need that line at all. Just do classNameBindings: ['hasAuthor']

Sorting parent's model the ember way

In a simple, standard ember.js route ...
this.resource('foo', function () {
this.route('show');
this.route('new');
});
I have a foo template that shows a new button (link to foo/new route) and the foo.bar data of the foo objects (with links to foo/show in a sorted way using the foo controller)
import Ember from 'ember';
export default Ember.Controller.extend({
sortProperties: [ 'bar:desc' ],
sortedList: Ember.computed.sort('model', 'sortProperties')
});
The routes foo/show and foo/new do the obvious, nothing special.
I don't need or want a "real" foo/index route, since all my navigation needs are catered for in the foo route and template. Therefore I want to transitionTo from foo/index whenever I enter that route to the first (as in given by the sorting above) foo/show route (or foo\new if there is nothing to show).
I've learned that "the ember way" is to transitionTo out of the route, but never ever out of the controller.
Now, my problem is, that I get access to the unsorted data very easily (it's the model of my parent route, that I can access via modelFor) and I can sort this "manually", but I'd rather use the already sorted data from my parent's controller. Now, I'm at a bit of a loss which hook to use in the route to be sure that my parent route's controller's computed properties are ready - and it doesn't feel right. So, what would be the ember way to do this?
Edit: Non-working example at http://emberjs.jsbin.com/hujoxigami/2/edit?html,js,output ...
Use the afterModel hook as you mention in one of your comments.
App.FooIndexRoute = Ember.Route.extend({
afterModel: function(posts, transition) {
if (posts.get('length') > 0) {
this.transitionTo('foo.show', posts.get('firstObject'));
}
}
And there is nothing wrong with sorting your model before you return it from the FooRoute. (Even better if you can receive it already sorted from your API/data storage)
App.FooRoute = Ember.Route.extend({
model: function() {
// not fully functional, just illustrative
return [{id:1, name:"Hans", detail:"german"},
{id:3, name:"Henri", detail:"french"},
{id:2, name:"Harry", detail:"english"}].sortBy('name');
}
});
The sorting properties in the controller remain relevant if you want to provide sorting controls for the user.

How do get a set of Ember Data models as a controller that can be injected to others?

In my Ember application, I wanted to have a controller wrapping a collection of models, that I could inject into other controllers.
I've set it up like this:
app/controllers/zones.js:
export default Ember.Controller.extend({
model: function () {
return this.store.find('zone');
}
});
app/controllers/zones/index.js:
export default Ember.Controller.extend({
needs: ['zones'],
zones: Ember.computed.alias('controllers.zones.model')
});
This seems like it ought to work, but unfortunately, it doesn't. I get this error in my JavaScript console (in the browser):
Error: Assertion Failed: The value that #each loops over must be an Array. You passed function () {
"use strict";
return this.store.find('zone');
}
I've tried moving stuff around, or using ArrayController rather than just Controller, but I still get this error.
This makes very little sense to me, any ideas?
Here is the thing, model is the function need to resolve the model for route not controller. That model then automatically injected to controllers model property.
Ember way
In ember way I would suggest move this model definition to the route for controller. Something like this.
export default Ember.Route.extend({
model: function (param) {
return this.store.find('zone');
}
});
This is the Ember way of doing thing. Resolve model in route then have controller to filter / decorate it.
I would also suggest using ArrayController instead of Controller since you are handling number of models.
The other way
Again if you want to have model resolved in controller. I warn you its not the Ember way but you can do it something like this -
export default Ember.ArrayController.extend({
//dont override the model property
mydata: function () {
return this.store.find('zone');
}.property('model'),
});
I figured out the problem – I just needed to set my overridden model implementation to be a property, like this:
app/controllers/zones.js (injected controller):
export default Ember.Controller.extend({
model: function () {
return this.store.find('zone');
}.property() // `.property()` turns the function into an iterable object for use in templates and the like.
});
The main controller is still the same.
app/controllers/zones/index.js (active route controller):
export default Ember.Controller.extend({
needs: ['zones'],
zones: Ember.computed.alias('controllers.zones.model')
});

In Ember.js how to notify an ArrayController's corresponding itemController, when a property on the ArrayController changes

I have an EmailsController (ArrayController), which stores all the emails. I have an EmailController (ObjectController) that has a parameter that stores if the actual Email is selected or not. I am trying to implement a button in the emails template, that selects or deselects all the Emails. So somehow I need to notify the EmailController via an action of the EmailsController and change the EmailController's isChecked parameter.
I am trying to use the itemController, the needs, and the controllerBinding parameters, but nothing works.
Here are the controllers:
App.EmailsController = Ember.ArrayController.extend({
needs: ["Email"],
itemController: 'Email',
checkAll: true,
actions: {
checkAllEmails: function() {
this.toggleProperty("checkAll");
console.log(this.get("checkAll"));
}
}
});
App.EmailController = Ember.ObjectController.extend({
needs: ["Emails"],
controllerBinding: 'controllers.Emails',
isChecked: true,
checkAllChanged: function() {
//this should execute, but currently it does not
this.set("isChecked",this.get('controller.checkAll'));
}.property("controller")
});
Here is the corresponding jsFiddle: http://jsfiddle.net/JqZK2/4/
The goal would be to toggle the selection of the checkboxes via the Check All button.
Thanks!
Your mixing a few different mechanisms and your using a few wrong conventions. It's not always easy to find this stuff though, so don't fret.
Referencing Controllers
Even though controllers are created with an Uppercase format, the are stored in the lowercase format and your needs property should be:
needs: ['emails'],
You then access other controllers through the controllers property:
this.get('controllers.emails.checkAll')
Computed Properties
Computed properties can be used as a getter/setter for a variable and also as a way to alias other properties. For example, if you wanted the isChecked property on the Email controller to be directly linked to the value of the checkAll property of the Emails controller, you could do this:
isChecked: function() {
return this.get('controllers.emails.checkAll');
}.property('controllers.emails.checkAll')
Although computed properties can do much more, this basic form is really just a computed alias, and there is a utility function to make it easier:
isChecked: Ember.computed.alias('controllers.emails.checkAll')
Observables
An observable basically creates a method that will be called when the value it observes changes. A computed alias would cause all items to uncheck or check whenever you clicked on any one of them, since their isChecked property is linked directly to the checkAll property of the parent controller. Instead of your checkAllChanged method identifying as a property it should use observes:
checkAllChanged: function() {
this.set("isChecked",this.get('controllers.emails.checkAll'));
}.observes("controllers.emails.checkAll")
This way when the checkAll property changes on the parent controller, this method updates the isChecked properties of all items to its value, but if you uncheck or check an individual item, it doesn't affect the other items.
Bindings
Bindings are somewhat deprecated; from reading issues on the Ember github repository I believe the creators of Ember seem to favor using computed properties, aliases, and observables instead. That is not to say they don't work and if your goal was to avoid having to type out controllers.emails every time, you could create one like you did (I wouldn't call it controller though, cause thats really quite ambiguous):
emailsBinding: 'controllers.emails'
Using a computed alias instead:
emails: Ember.computed.alias('controllers.emails')
You could then change your observer to:
checkAllChanged: function() {
this.set("isChecked",this.get('emails.checkAll'));
}.observes("emails.checkAll")
Heres an updated version of your jsFiddle: http://jsfiddle.net/tMuQn/
You could just iterate through the emails, changing their properties from the parent controller. You don't need to specify needs or observe a variable.
App.EmailsController = Ember.ArrayController.extend({
itemController: 'email',
actions: {
checkAllEmails: function() {
this.forEach(function(email) {
email.toggleProperty("isChecked");
});
}
}
});
Also, you typically don't set initial values like you did with isChecked = true; I believe that's creating a static shared property on the prototype (not what you intended). Instead, set the property on init, or pass it in from your original json data.
See the code: http://jsfiddle.net/JqZK2/5/