Catch All Action Handler - ember.js

Now, this is really a question that's evolved from this. There is a whole lot more info there, but I guess, it's better to ask a direct specific question. So here it goes:
We typically define actions in a controller like so:
var FooController = Ember.Controller.extend({
actions: {
login: function() {
}
}
});
Is there a way to define a catch all action handler, like so (hypothetically):
actions: {
login: function() {
},
*: function(actionName, paramArray) {
}
}
This would be analogous to embers catch all route which I believe has been implemented, though I haven't tried it.
I need this because my Ember.Component renders a user supplied partial template using the {{partial}} helper. This partial might have {{action}}'s specified in them. These actions don't bubble up to the calling controller or route and are lost inside the component. This fact is mentioned in the docs in para 4.
If a catch all action was possible, my component could implement it and send the action back to the caller using something like this:
actions: {
*: function(actionName, paramArray) {
this.sendAction(actionName, paramArray)
}
}

The functionality around the actions hash on controllers, views and routes is provided by the action handler mixin [1]. As you can see, it injects the send(actionName) method and makes enables an object to receive actions. You can now easily override this function and catch all actions instead of looking for it in the actions hash -- which is what the implementation does.
[1] https://github.com/emberjs/ember.js/blob/v1.3.0/packages/ember-runtime/lib/mixins/action_handler.js#L8

Related

Using this.sendAction() in components?

I am using Ember 2.2.0
When writing components I used to propagate events from the component to the route (or whatever embeds the component) with this.sendAction(…). I checked out the docs lately and discovered that they advise another approach.
https://guides.emberjs.com/v2.2.0/components/triggering-changes-with-actions/
this.get('action')();
Since Ember is known to be very opinionated I want to stick as much to the best practices as possible. But I am not sure if the docs might be out of date or the tutorials using sendActions are.
So I would like what is the way to do this?
When you are using this.sendAction('actionName') you're bubbling up an action which you'll have to catch on the component/controller with actions
//controller/route/component.js
actions: {
actionName: function() {
//Do something
}
}
If you want to send that up the chain, you'll have to call sendAction('') again on the component/controller and catch it again on the parent (and so on).
The other approach this.get('action')() uses closure actions, which are regular javascript functions. These are the preferred way to invoke actions in Ember 1.13.X as far as I know. One neat thing that closure actions have is that you can have return values. Meaning that you can have something like this:
//a controller
actions: {
saveResult() {
return this.get('model').save(); //notice the return (which returns a promise)
}
}
//some template that uses the controller above
{{a-component save=(action 'saveResult')}} // Passes the saveResult action to the component
//a-component.js
actions: {
someAction: function() {
this.attrs.save().then(() => {
//Do something with the return value
});
}
}
A lot can be written about closure actions, but others have written far better than I could, so I recommend the following articles:
Ember Closure Actions #1
Ember Closure Actions #2
Ember Closure Actions #3
And if you're new to the whole DDAU (Data Down Actions Up) concept, I really recommend Sam's article about the concept in general.
Update: There's also an addon (linked in the comments by #locks) which allows closure actions to bubble to routes. Note that if you look to upgrade Ember to more recent versions (3.8 and up), route-actions will not be compatible.
For those using Ember version >=3.4:
Use closure actions instead of sendAction
sendAction has been deprecated in 3.4. You can find an example of how to define closure actions under the relevant deprecation text:
https://deprecations.emberjs.com/v3.x/#toc_ember-component-send-action

Ember.JS A BEFORE() function for every controller action

Is it possible to create some sort of before() function for every action inside a controller?
I know I can do an init() for every action, but what if I have a some code I want to run for every thing? It would be way too redundant to create the same init() function for each action.
No, there is no way to run code before every action without explicitly calling it yourself. At one point there was an idea to lookup actions using get(), which would have allowed this, but it never got implemented. Not sure what your use case is, but if you really do need to call functionality before every action call, just call it yourself. It'll be much more readable than any clever hacks we could come up with.
You could possibly do something like this:
First make sure your controller extends the `Ember.TargetActionSupport' mixin.
Ember.Controller.extend(Ember.TargetActionSupport,{
...
});
This will allow you to trigger actions programmatically. See more about this: Ember.TargetActionSupport
Then you would make an entry point action that contains your common code and a call to trigger the proper action:
Ember.Controller.extend(Ember.TargetActionSupport,{
actions: {
common: function (actionName) {
//Do common logic here
this.set('foo', 'bar');
//And then trigger the intended action
this.triggerAction({
action: actionName,
target: this
});
},
otherAction: function () {
//Do some more logic here
this.set('bar', 'baz');
}
}
});
You would call this action from your template like this:
<button {{action 'common' 'otherAction'}}>Press Me</button>

Emberjs: how to reach the action of a parentView within an itemsViewClass

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 :(

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

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)