Emberjs: how to reach the action of a parentView within an itemsViewClass - ember.js

Cannot get my head around the following problem. I got a Uncaught TypeError: Cannot read property 'send' of undefined whatever I try. I think it must be the controllerBinding in the itemsViewClass, however I think it is defined correctly.
In the code below there are two showMenu actions. The first one works, but the last one in the itemsViewClass does not.
Please take a look at my code below (I show only the relevant code):
//views/menu.js
import Ember from "ember";
var MenuitemsView = Ember.View.extend({
template: Ember.Handlebars.compile('<div{{action "showMenu" target="view"}}>this works already</div>\
much more code here'),
contentBinding: 'content',
itemsView: Ember.CollectionView.extend({
contentBinding: 'parentView.subCategories',
itemViewClass: Ember.View.extend({
controllerBinding: 'view.parentView.controller', // tried to add controllerBinding but did not help
// this is where the question is all about
template: Ember.Handlebars.compile('<div {{action "showMenu" target="parentView"}}>dummy</div>')
}),
actions: {
showMenu: function(){
// dummy for testing
console.log('showmenu itemsView');
}
}
}),
actions: {
showMenu: function() {
console.log('showMenu parentView!'); // how to reach this action?
}
}
});
export default MenuitemsView;
I have tested with {{action "showMenu" target="view"}} and without a target. It seems not to help.
Do someone have a clue why the second showMenu action cannot be reached?

OK, so this is by no means the only way to do logic separation in Ember, but it's the method I use, and seems to be the method generally used in the examples across the web.
The idea is to think of events and actions as separate logic pools, where an event is some manipulation of the DOM itself, and an action is a translatable function that modifies the underlying logic of the application in some way.
Therefore, the flow would look something like this:
Template -> (User Clicks) -> View[click event] -> (sends action to) -> Controller[handleLogic]
The views and the controllers are only loosely connected (which is why you can't directly access views from controllers), so you would need to bind a controller to a view so that you could access it to perform an action.
I have a jsfiddle which gives you an idea of how to use nested views/controllers in this way:
jsfiddle
If you look at the Javascript for that fiddle, it shows that if you use the view/controller separation, you can specifically target controllers to use their actions, utilising the needs keyword within the controller. This is demonstrated in the LevelTwoEntryController in the fiddle.
At an overview level, what should happen if your bindings are correct, is that you perform an action on the template (either by using a click event handler in the view, or using an {{action}} helper in the template itself, which sends the action to the controller for that template. Which controller that is will depend on how your bindings and routing are set up (i've seen it where I've created a view with a template inside a containerView, but the controller is for the containerView itself, not the child view). If the action is not found within that controller, it will then bubble up to the router itself (not the parent controller), and the router is given a chance to handle the action. If you need to hit a controller action at a different level (such as a parent controller or sibling), you use the needs keyword within the controller (see the fiddle).
I hope i've explained this in an understandable way. The view/controller logic separation and loose coupling confused me for a long time in Ember. What this explaination doesn't do, is explain why you are able to use action handlers in your view, as I didn't even know that was possible :(

Related

Cannot get Ember action to bubble

I have spent a lot of time trying to implement what ought to be a simple thing in Ember: bubbling an action from a component to the application controller. The docs say that in order to propogate an action then all I need to do is declare the action when I declare the template:
{{#if isLoggedIn}}
{{menu-bar action="authenticate"}}
{{/if}}
...and then send the action from the component (which is nested in the login route):
authenticate: function() {
//code omitted for brevity
this.sendAction('authenticate');
}
...which should be received by the application route:
user: Ember.inject.service(),
isLoggedIn: false,
actions: {
authenticate: function(){
console.log('authenticated!');
this.set('isLoggedIn', this.get('user').isLoggedIn());
}
}
but it isn't. And I'm wasting a lot of time trying to work out why.
I'm sure it's really obvious but if someone can tell me what I'm doing wrong I would greatly appreciate it.
Your issue is that the application controller isn't supposed to catch bubbled actions, the application route is. For the life of me I can't find the relevant section in the guides any more, but actions don't bubble through controllers, they bubble through routes. The first place an action usually goes (and is true in your case) is the relevant controller. In your case, you don't want the controller to catch it you want it to bubble. However, if an action isn't caught by a controller it bubbles to that controller's route. From there, it does not bubble to parent controllers, it only bubbles to parent routes.
For instance, let's say you had the following routes:
this.route('books', function() {
this.route('book', { path: '/:book_id' });
});
The bubbling order would be as such:
Component
Book controller
Book route
Books route
Application route
TL;DR: Catch your action in the application route, not controller. If I can find the guide on action bubbling, I'll edit my answer and link to it.
The mistake was that I had put the action="authenticate" on the wrong component! Apologies to GJK and Daniel Kmak for wasting your time trying to solve that one but it seems that I had just got mixed up.

How can a component respond to an action?

tl;dr: Data can be sent in and out of a component, but I only know how to send actions out. Is there a way to send actions in?
In my Ember application, I have something like the following UI from Google Maps:
The background map corresponds to a PinsRoute/PinsView/PinsController, and it shows many pins. When you click one, you enter the PinRoute, which renders the overlay to {{outlet}}. Both the big map and the thumbnail (in the Google Maps image, the picture that says "Street View") are components: FullscreenMapComponent and ThumbnailMapComponent, respectively.
In Google maps, when you click "Street view", it pans and zooms the main map to the selected point. This is essentially what I'm trying to figure out how to wire up.
When the user clicks "streeth view" on my ThumbnailMapComponent, I can send out an action, which the PinsRoute can handle. The question is, how can I then reach down to my FullscreenMapComponent and invoke the appropriate method (.panToSelected(), in this case)?
Here's a good solution. Have the component register itself to whatever controller it's being rendered in, and that way you can access the component in action handlers.
In my case, I added a method to my component
App.FullscreenMapComponent = Ember.Component.extend({
...
_register: function() {
this.set('register-as', this);
}.on('init')
});
and a property to my controller:
App.SensorsController = Ember.ArrayController.extend({
fullscreenMap: null,
...
});
In my template, I bind the two with my new register-as property:
// sensors.hbs
{{fullscreen-map data=mapData
selectedItem=currentSensor
action='selectSensor'
deselect='deselectSensor'
register-as=fullscreenMap }}
And now, let's say I click the thumbnail above and it sends an action that bubbles up to my ApplicationRoute, I can now do this:
// ApplicationRoute.js
actions: {
panTo: function(latLong, zoom) {
this.controllerFor('sensors').get('fullscreenMap').panTo(latLong, zoom);
}
}
Presto! Components responding to actions.
Update (10/6/2016)
Whew, I've learned a lot in two years.
I would no longer recommend this solution. In general, you should strive for your components to be declarative, and have their output and behavior depend solely on their state. Instead, my original answer here solved the problem using an imperative method, which is brittle, harder to understand, and not very easy to extend (what if we wanted to tie the the map's position to the URL?).
If I was refactoring this specific component, I'd probably make {{fullscreen-map}} accept latLong and zoom params. If someone from the outside sets those, the map would respond and update itself. You can use didReceiveAttrs from within the component to respond to param changes, and then from there call the imperative panTo method from Google's API.
The takeaway is that, even if you have to interact with imperative methods (maybe because of a third-party lib), strive to make your own components' APIs as declarative as possible. What first seems like "sending an action down" can usually be expressed as some function of state, and that state is what you want to identify and make explicit.
This is working example but I am not 100% sure that this approach is best
Here what you can do:
When calling action pass your component as parameter:
App.PinController = Ember.Controller.extend({
actions: {
actionThatComponentCalls: function(){
// different component will be called
new App.MyOtherComponent().send('differentAction'):
}
}
});
App.FullScreenMapComponent = Ember.Component.extend({
click: function(){
this.sendAction();
}
});
App.MyOtherComponent = Ember.Component.extend({
actions: {
differentAction: function(){
console.log('different action called');
}
}
});
Hope this helps

How do I implement Actions directly on controllers with EmberJS?

I'm trying to implement an action on a controller and get the warning:
DEPRECATION: Action handlers implemented directly on controllers are deprecated in favor of action handlers on an actions object
If I use Em.ObjectController.create(), when I click my button I get a warning stating that actions must be provided at extend time. However, if I use Em.ObjectController.extend(), when I click the button I get an error saying the action did not exist on the controller.
I created a jsBin to view this
//App.ToolbarController = Ember.ObjectController.create({
App.ToolbarController = Ember.ObjectController.extend({
model: { fu: "baar" },
actions: {
doSomethingUseful: function(data) {
console.log("doing nothing useful...");
}
}
});
I took a look at your jsBin
First off in the future its easier to debug your templates if you post them as an embedded script tag instead of the compiled handlebars function.
I've made a bin that fixes your issue.
I believe your issue had to do with the method you were using to create your view.
{{view App.ToolbarView controllerBinding="App.ToolbarController"}}
I'm not sure if that works properly.
Instead you should use the render helper
{{ render 'toolbar' }}
That way ember will try to find the toolbar view, controller and template and wire them all together properly.

Make a programmatically Created Controller Delegate (Bubble) Events

I have an app with many similar views which I instantiate programmatically to "DRY-up" my app.
The problem is that controllers instantiated programmatically do not delegate actions in the actions hash further. This is clear because there is nothing from which the controller can derive the hierarchy. There should be a way, however, to tell a controller which parent controller it has to use for event bubbling. Does anyone know it?
You shouldn't be initializing controller's on your own. All controller initialization should be handled by Ember itself. Another interesting note, controller's are intended to be singletons in the application. The only exception to this being the itemController when looping over an ArrayController. You can read more about it in the guides. Quote from the guides:
In Ember.js applications, you will always specify your controllers as
classes, and the framework is responsible for instantiating them and
providing them to your templates.
This makes it super-simple to test your controllers, and ensures that
your entire application shares a single instance of each controller.
Update 1:
An example of how to do routing for a wizard:
App.Router.map(function() {
this.resource('wizard', function() {
this.route('step1');
this.route('step2');
this.route('step3');
});
});
This way, you can have a separate controller/view/template per step of the wizard. If you have logic around how much of each step should be completed prior to transitioning to the next one, you can handle that in the individual routes.
Update 2:
In the event that the number of steps aren't predetermined, but are based on the data being fed to the app, you can make a WizardController that is an ArrayController where each item in the array is a step in the wizard. Then, use the lookupItemController hook on the ArrayController, kind of like this:
App.WizardRoute = Ember.Route.extend({
model: function() {
return [
{controllerName: 'step1', templateName: 'step1'},
{controllerName: 'step2', templateName: 'step2'},
{controllerName: 'step3', templateName: 'step3'}
];
}
});
App.WizardController = Ember.ArrayController.extend({
lookupItemController: function(modelObject) {
return modelObject.controllerName;
}
});
{{#each step in controller}}
{{view Ember.View templateName=step.templateName}}
{{/each}}
As another, probably better, alternative, you can override the renderTemplate hook in the route where you're pulling down the model for the next step in the wizard and pass in the appropriate templateName and controller in the render call, kind of like you see here.
Point being, I think it should be possible to do this without having to instantiate controllers yourself.

ember new router

I have a question about Ember routing and controllers. I've just written a small App to get familiar with the new router. Therefore I've built a button that transitions to another state by clicking on it.
App.PostsView = Em.View.extend({
click: function() {
var router;
this.get('controller').transitionTo('about');
}
});
My question now is: what does the get method return?. Obviously an instance of the PostController but on the one hand the controller doesn't have a transitionTo() method and on the other hand that would not make any sense.
this.get('foo') returns a property of your Ember object. Since Views can have a "controller" property, this.get('controller') returns the controller bound to your view's controller property (by default the postsController).
this.get('controller').transitionTo() works because as sly7_7 mentioned, transitionTo() is also defined on the controller and delegates to the router. Note, it's probably going to be deprecated and one should use
this.get('controller').transitionToRoute('about');
instead.
You should not do this at the view level, this is what the router is designed for so instead of capturing a click event in the view, rather implement an action on the button and let the router handle it. I suggest you learn the best practices from the get-go, chances are your application will evolve, requiring more elaborate concepts such as handling transactions commits/rollbacks, creating/updating records. So here's my suggestion to you
In your view
<button type="button" {{action onSaveClick}} />Save</button>
In your router
App.FooRoute = App.Route.extend({
events: {
onSaveClick: function() {
this.transitionTo('bar');
}
}
})
If for any other reasons, say styling or animation, you find yourself forced to capture the click event in the view, then i suggest to, capture the event, perform your styling and finally send an event to the controller. The event can then be handled the same way in the router
App.FooView = Ember.View.extend({
click: function() {
// Do some styling
this.get('controller').send('onSaveClick')
}
})
Finally speaking of best practices, try to learn when working with ember to think of your application as a series of states interacting with each other. You'll find yourself bound to implement the concept right. Have a look at this
the controller has a transitionTo: https://github.com/emberjs/ember.js/blob/master/packages/ember-routing/lib/ext/controller.js#L36, it basically delegate to it's target (which is the router)