Ember.js dependency injection - javascript

let's assume I have this controller
MyApp.LayoutFooterController = Ember.ObjectController.extend
formData:
name: null,
phone: null,
message: null
cleanFormData: ->
#set('formData.name', null)
#set('formData.phone', null)
#set('formData.message', null)
send: () ->
#container.lookup('api:contact').send(
#get('formData.name'),
#get('formData.phone'),
#get('formData.message')
)
#cleanFormData()
For this I've created service class
MyApp.Api ||= {}
MyApp.Api.Contact = Ember.Object.extend
init(#$, #anotherDep) ->
send: (name, phone, message) ->
console.log name, phone, message
and initializer
Ember.Application.initializer
name: 'contact'
initialize: (container, application) ->
container.register 'api:contact', MyApp.Api.Contact
Problem is, that I can not figure out how to set container to be able to resolve my service class dependecies init(#$, #anotherDep) through Ember container.
Can anybody give me explanation, how to use the Ember.js dependecy injection (or service locator, I guess) container to inject other libs or objects?
Maybe, that I'm not doing it well at all.
EDIT
When I looked to Ember's container source code I found a solution:
Ember.Application.initializer
name: 'contact'
initialize: (container, application) ->
container.register 'api:contact', { create: () -> new MyApp.Api.Contact(application.$) }
But is this clean?

Generally you don't want to be wiring up all of the pieces yourself, you want to use needs in your controller to let Ember do it for you. I'm not sure at all how Ember deals with 3 level class names vs two, so I'm just going to demonstrate with two levels. (MyApp.ApiContact instead of MyApp.Api.Contact.) Also, send is a native Ember method that is present on all (or almost all) objects, so you'd want to use something like sendMessage instead so that you don't end up with hard to diagnose conflicts. After you have told Ember that your controller needs apiContact, you can just call this.get('controllers.apiContact') to get a hold of it.
MyApp.LayoutFooterController = Ember.ObjectController.extend({
needs : ['apiContact'],
// All your other stuff here
sendMessage : function(){
this.get('controllers.apiContact').sendMessage(...);
}
});

Related

Accessing asynchronously loaded object based on relations in Ember

I have a trouble with asynchronously loaded models in Ember. I thought I have already understood the whole "background Ember magic", but I haven't.
I have two models, let's say foo and boo with these properties:
foo: category: DS.belongsTo("boo", { async: true })
boo color: DS.attr("string")
In my route, I load all foos:
model: function(params) {
return this.store.findAll("task", "");
},
than in my template I render a component: {{my-component model=model}}. In the component's code I need to transform the model into another form, so I have:
final_data: function() {
this.get("model").forEach(function(node) {
console.log(node.get("category"));
});
return {};
}.property("model"),
When I try to access the "category" in the model, my code crashes:
EmberError#http://localhost:4200/assets/vendor.js:25705:15
ember$data$lib$adapters$errors$$AdapterError#http://localhost:4200/assets/vendor.js:69218:7
ember$data$lib$adapters$rest$adapter$$RestAdapter<.handleResponse#http://localhost:4200/assets/vendor.js:70383:16
ember$data$lib$adapters$rest$adapter$$RestAdapter<.ajax/</hash.error#http://localhost:4200/assets/vendor.js:70473:25
jQuery.Callbacks/fire#http://localhost:4200/assets/vendor.js:3350:10
jQuery.Callbacks/self.fireWith#http://localhost:4200/assets/vendor.js:3462:7
done#http://localhost:4200/assets/vendor.js:9518:1
.send/callback#http://localhost:4200/assets/vendor.js:9920:8
It seems to me, like the Ember didn't load the boos. How should I access them right to make Ember load them?
It's trying to load category, but the adapter is encountering some error. Can't tell what from your example.
Check your network tab.
When you access an async association from a template, Ember knows what to do. From code, such as your component's logic, Ember has no idea it needs to retrieve the association until you try to get it. The get will trigger the load, but will return a promise. You can do this:
get_final_data: function() {
Ember.RSVP.Promise.all(this.get("model") . map(node => node.get('category'))
.then(vals => this.set('final_data', vals));
}
As I'm sure you can see, this creates an array of promises for each node's category, calls Promise.all to wait for them all to complete, then stores the result into the final_data property.
Note, this is not a computed property; it's a function/method which must be called at some point, perhaps in afterModel.

ember.js, filtering PromiseManyArray in the controller

I am trying to define a computed property that consists of a filtered hasMany relationship. When I loop over the items of the PromiseManyArray, I get undefined when trying to access the attribute I want to filter on. On later calls to this computed property, everything works fine.
This is a simplified version of my controller code:
export default Ember.Controller.extend({
availableModules: function () {
let thisModule = this.get('model')
console.log(thisModule.get('library.modules')) // This logs <DS.PromiseManyArray:ember604>
// loop over siblings
return thisModule.get('library.modules').filter(mod => {
// mod.classification is undefined
return mod.get('classification') !== 'basis'
})
}.property('model')
})
For the Module model we can assume that it has a classification attribute, and it belongs to a Library object, and the Library model hasMany modules.
I have tried something like this, and it logs properly the attribute classification, but I don't know how to return anything so that the template can render it.
availableModules: function () {
let thisModule = this.get('model')
thisModule.get('library.modules').then(mods => {
mods.forEach(mod => {
console.log(mod.get('classification'))
})
})
}.property('model')
So the problem seems to be that inside of the PromiseManyArray.filter method, the attributes of the found objects are not yet resolved... How can I create a promise that will return all filtered objects once those have been resolved? I don't know how to get my head around this. Thanks.
Inspired by Bloomfield's comment, and with help of this thread in the ember forum, I have found an acceptable solution. Basically it consists of resolving all the relationships in the route, so that when the controller is called, you don't have to deal with promises.
Solution:
In the model hook of the route, return a hash of promises of all the needed information
Define a custom setupController, and inside of it, store the model and the extra data in the controller
The route code looks like this:
export default Ember.Route.extend({
model(params) {
let module = this.store.findRecord('module', params.mod_id)
return Ember.RSVP.hash({
module: module,
siblingModules: module.then(mod => mod.get('library.modules')), // promise based on previous promise
})
},
setupController(controller, hash) {
controller.set('model', hash.module)
controller.set('siblingModules', hash.siblingModules)
},
})
Note: for the route to still work properly, the {{#link-to 'route' model}} have to explicitly use an attribute, like the id: {{#link-to 'route' model.id}}
Extra info
Bloomfield's approach consisted of using the afterModel hook to load the extra data in an attribute of the Route object, and then in the setupController, set the extra data in the Controller. Something like this:
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('module', params.mod_id)
},
afterModel(model) {
return model.get('library.modules').then(modules => {
this.set('siblingModules', modules)
})
},
siblingModules: null, // provisional store
setupController(controller, model) {
controller.set('model', model)
controller.set('siblingModules', this.get('siblingModules'))
},
})
But this feels like a hack. You have to return a promise in afterModel, but you can't access the result. Instead the result has to be accessed via .thenand then stored in theRoute` object... which is not a nice flow of information. This has however the advantage that you don't have to specify any attribute for the links in the template.
There are more options like using PromiseProxyArray, but that's too complicated for a newcomer like me.
For anyone running into PromiseManyArray issues in modern times, make sure you have async: false explicitly set on any hasMany relationships directly serialized by the API. Modern versions of Ember will behave unexpectedly if you don't, such as computed properties not working when you use pushObject.

Trigger action on controller from route after model has resolved in Ember

I am fairly new to Ember, being a hardcore backbone aficionado for many years and I'm at a loss for how to handle a situation I've run into.
I am using Pusher and the Pusher Ember library to build a sort of chat application.
The way it works is, a person navigates to a users account page and it creates a new "conversation". Then, once that conversation has been created, I would like to subscribe to a pusher channel that is dynamically named after that conversations id. I need to define the Pusher Subscriptions on my controller.
Here's my route (in coffeescript)
App.ConversationShowRoute = Ember.Route.extend
model: (params) ->
#store.createRecord('conversation', user_id: params.user_id).save()
and my controller:
App.ConversationShowController = Ember.ObjectController.extend
init: ->
subscriptions = []
subscriptions[#model.get('id')] = ['newMessage']
PUSHER_SUBSCRIPTIONS: subscriptions
Unfortunately, the model has not resolved at this point so I don't yet know what my #model.id is, and it fails.
Any advice for the best way to handle this?
I had the same issue when I added ember-pusher to my application. The solution I went with is to define a variable in the App namespace that could be referenced. (Not ideal and something I'll need to fix later)
init: function () {
this.channelName = 'presence-document-' + App.documentId + '-channel';
this.PUSHER_SUBSCRIPTIONS[ this.channelName ] = [
'pusher:subscription_succeeded', 'pusher:member_added', 'pusher:member_removed',
'client-send-status', 'client-send-message'
];
this._super();
}
A second, cleaner option, would be to try a needs relationship with your user controller, but I'm not sure if that will be available until after init is complete.
App.ConversationShowController = Ember.ObjectController.extend({
needs: ['user'],
userId: Ember.computed.alias('controllers.user.id'),
init: function() {
this.PUSHER_SUBSCRIPTIONS[this.get('userId')] = ['newMessage'];
this._super(); // Maybe try putting this first if properties haven't resolved.
}
});
A third option would be to lookup the user controller (singleton) during init.
App.ConversationShowController = Ember.ObjectController.extend({
init: function() {
var userController = this.container.lookup('controller:user');
this.PUSHER_SUBSCRIPTIONS[userController.get('id')] = ['newMessage'];
this._super();
}
});
UPDATE
Since you need the conversation id you can observe when the model changes and wire up pusher the way ember-pusher/bindings.js does. You would no longer need to override controller.init, just set PUSHER_SUBSCRIPTIONS: {} to start with.
afterModelLoad: function() {
this.channelName = 'conversation-' + this.get('model.id');
this.PUSHER_SUBSCRIPTIONS[this.channelName] = ['newMessage'];
// From bindings.js init
var events = this.PUSHER_SUBSCRIPTIONS[this.channelName];
this.pusher.wire(this, this.channelName, events);
this.removeObserver('afterModelLoad'); /* If you only want to run once */
}.observes('model')

Ember's *needs* dependencies

I have a signup process that consists of a few steps and would like to store the state within a service that can be accessed by each of the controllers for each of the steps.
I was able to get this working, but in a way that doesn't seem to jive with Ember's way of doing things. Instead of setting the controller's needs: value I had to add an initializer, which contains the following:
export default {
name: 'signup-state',
initialize: function(container, app) {
app.inject('controller:signup/index', 'signup-state', 'service:signup-state');
app.inject('controller:signup/method', 'signup-state', 'service:signup-state');
app.inject('route:signup/method', 'signup-state', 'service:signup-state');
}
};
The above was based on a comment by wycats on the discuss board [1].
Doing this just seems wrong. I would think that the needs controller would take care of this. So if this is just plain wrong stop me here since doing this a better way may fix the problem.
The above works, except for when it comes time to test the controller. When I call a method on the controller, that calls a method on the service, I get an error.
Here is the controller code
export default Ember.Controller.extend({
/**
* Reference to the signup-state service => initializers/signup-state.js
*/
setState: function(key, val) {
var state = this.get('signup-state');
state.set(key, val); <== state is undefined in tests
},
actions: {
signupAsAdmin: function() {
this.setState('userType', 'admin');
this.transitionToRoute('signup.method');
}
}
});
And here is the controller TEST code
import { test, moduleFor } from 'ember-qunit';
moduleFor('controller:signup/index', 'SignupController', {
needs: ['service:signup-state']
});
test('signing up as an admin set the userType state to admin', function() {
var controller = this.subject();
// blows up here => Cannot read property 'set' of undefined
controller.send('signupAsAdmin');
});
Calling the signupAsAdmin function within the controller, results in making a set call on the service object, which results in an “undefined” error.
The initializer code is run as noted by adding console.log statements, but doesn't seem to result in making the service available to the controller during the tests.
Any help is appreciated.
Note: I am using ember-cli, so I don't have a global App variable available.
Update Manually registering (something I thought that ember-cli was doing) does work.
export default {
name: 'signup-state',
initialize: function(container, app) {
app.register('service:signup-state', 'signup-state');
// Remove Injects
// app.inject('controller:signup/index', 'signup-state', 'service:signup-state');
// app.inject('controller:signup/method', 'signup-state', 'service:signup-state');
}
};
The above results in a null value returned when calling the get('signup-state') in the controller.
http://discuss.emberjs.com/t/services-a-rumination-on-introducing-a-new-role-into-the-ember-programming-model/4947/10?u=olsen_chris
I'm new to the idea of using the dependency injection for a service so I might be missing something, but looking at this example test in the ember code base made me wonder, are you just missing a app.register('service:signup-state',App.ModelForSignupState) to give it bones?

Ember.js: questions concerning controllers, 'this', 'content' and model structure

I am getting a little deeper into my first functional app and need to better understand what it going on in my controller.
Here I have a controller that handles the action when a user clicks on an 'Option'. Looking at the this object raises a few questions:
What exactly is this? I would expect it to be an instance of my Option model, but it is missing some properties (like "identity: 'model: Option'").
If this is an instance of my Option model, why is the 'model' property undefined? Why doesn't it just know that?
What is this.content? It looks like some stuff is inside content (id and isSuppressed) and some is not (this.isSelected) - why is that?
Disclaimer: Though there aren't any presenting problems so far, there certainly could be errors in my ember app architecture.
Screenshot debugging controller:
Option Model & Controller
App.Option = Ember.Object.extend({
identity: 'model: Option',
id: '',
cost: '',
isSelected: false,
isSuppressed: false
});
App.OptionController = Ember.Controller.extend({
actions: {
toggleOption: function() {
this.set('isSelected', !this.get('isSelected'));
var id = this.get('content.id');
this.send('deselect', this.get('content.id'));
}
}
});
App.OptionsController = Ember.ArrayController.extend({
actions: {
deselect: function(exception) {
var opts = this.rejectBy('id', exception)
opts.setEach('isSuppressed', true);
}
}
});
It depends where this is, if your in the controller it's the controller. If your controller is an ObjectController/ArrayController it will proxy get/set calls down to the underlying model. content/model are the same thing in the context of the controller.
The properties rarely live directly on the instance, usually they are hidden to discourage accessing the properties without using the getters/setters.
In your code above there is a good chance your OptionController should be extending ObjectController. Unless the controller isn't backed by a model. If you use Ember.Controller.extend then it won't proxy getters/setters down to the model, it will store, retrieve properties from the controller itself.

Categories