How to call an action on a route using closure actions? - ember.js

I've got openModal action defined on application route. I'm trying to call this action from within a component.
If I use syntax for action bubbling:
{{my-component openModal="openModal"}}
then everything works as expected and I can trigger this action using this.sendAction("openModal").
However, I'm not sure how to get the same result using the new closure syntax:
{{my-component openModal=(action "openModal")}}
In this case, Ember complains that there's no action openModal defined on the controller. Do I have to define this action on every controller that uses my-component? Is there a way to somehow use target option to tell Ember that this action is defined on a route? Is it ok to mix bubbling and closure syntax in a single component?
I'm using Ember 2.0 beta 1.

Until routable components are introduced somewhere in Ember 2.1 or 2.2 or 2.3 or 2.4 or 2.5 or 2.6 or 2.7, it is impossible to pass a closure action from a route.
For now, you can only pass closure actions from a controller and on to child components.
UPD: Miko Paderes hints that an addon is available: https://github.com/dockyard/ember-route-action-helper

Try this:
{{my-component openModal=(action send "openModal")}}
...which makes use of the send method on the controller.
I can't say I fully understand it given that the second argument to send is the context but it is still passing additional arguments to my action in the route correctly. I'm currently using version 2.4.5.

You can send closure actions to the route by proxying them through the controller. This works with any Ember version that supports closure actions. The only caveat is that the action name must differ from controller and route.
For instance, given this template:
{{foo-component myAction=(action 'foo')}}
You would need a foo action on your controller, which could proxy to the route:
proxyFooTo: 'fooBar',
actions: {
foo(context) {
this.send('proxyFooTo', context);
}
}
Then on the route
actions: {
fooBar(context) { ... handle fooBar ... }
}

It is also possible to avoid creating a controller, or if there is one already avoid adding more logic in it, by specifying the target property
http://emberjs.jsbin.com/fiwokenoyu/1/edit?html,js,output
in route
js
setupController(controller,model){
this._super(...arguments);
controller.set('theRoute', this);
},
actions:{
openModal(data){
/*...*/
}
}
in template
hbs
{{my-component openModalAction=(action 'openModal' target=theRoute)}}

Try the Ember add-on called ember-route-action-helper,
https://github.com/dockyard/ember-route-action-helper
You can just replace route-action with action at the time "routable- components" become available.
{{todo-list todos=model addTodo=(route-action "addTodo")}}
So the code above has a similar effect as
Inside the route,
setupController(controller,model){
this._super(...arguments);
controller.set('theRoute', this);
},
actions:{
addTodo(data){
/*...*/
}
}
Inside the HBS,
{{todo-list addTodo=(action 'addTodo' target=theRoute)}}

Related

emberjs loading spinner with findRecord

EmberJS 3.4
I'm loading a Project entity from a backend which takes a couple of seconds. Now I would like to show a spinner during loading.
as described I created a project-loading.hbs (also tried with loading.hbs) https://guides.emberjs.com/release/routing/loading-and-error-substates/
project model class:
export default AuthenticatedRoute.extend({
model(params) {
return this.store.findRecord("project", params.projectname);
},
actions: {
refresh: function() {
this.refresh();
}
}
});
though it takes time to load the entity, the loading template seems not to be rendered/shown. Am I doing something wrong ?
For a route called project (routes/project.js), the loading template should be called project-loading.hbs.
I cloned your project and actually made it work (Ember CLI 3.4.3) by adding templates/project-loading.hbs, adding a sleep(30) call to your /api/projects/:name endpoint and going to a URL like http://localhost:4200/projects/hallo.
Do you have the problem when transitioning to the route internally (by using transitionTo or the {{link-to}} helper with a model for instance) or by entering the URL manually? Note that model hook is not executed when you transition to a route and pass in a model context (see https://guides.emberjs.com/v3.4.0/routing/specifying-a-routes-model/).
I ended up with adding the following code to the application adapter:
// not very ember way of doing this, but quite simple :)
$(document).ajaxStart(() => {
$('#spinner').removeClass('hide');
});
$(document).ajaxStop(() => {
$('#spinner').addClass('hide');
});
I really would prefer doing it the ember way. for now, this seems to do the trick.
for anyone interested, here's the complete project: https://github.com/puzzle/mailbox-watcher/tree/master/frontend

Accessing Ember Controller Properties within the same controller

I'm very new to EmberJS 2.0 and trying to slowly understand it by building my own website with it. Anyways, I've managed to get Firebase integrated with Ember and my controller is able to authenticate correctly. However, I'd like to understand why when I execute:
this.send('toggleModal');
inside the authenticate action property function (.then()) it doesn't work but if I execute it outside then everything works fine.
1) Is the 'this' keyword getting confused with something other than the Ember controller?
Here is the sample:
// /app/controllers/application.js
import Ember from 'ember';
export default Ember.Controller.extend({
isShowingModal: false,
actions: {
toggleModal: function() {
this.toggleProperty('isShowingModal');
},
authenticate: function(username, pass) {
this.get('session').open('firebase', {
provider: "password",
email: username,
password: pass
}).then(function (data) {
console.log(data.currentUser);
console.log(session.isAuthenticated); //Why is 'session' not defined?
this.send('toggleModal'); //This doesn't work. Throws an error.
});
this.send('toggleModal'); //This works.
},
logOut: function() {
this.get('session').close();
}
}
});
2) Also, I've noticed that when using Emberfire I'm able to use the property 'session.isAuthenticated' within the template application.hbs however, shouldn't 'session' be an object that is injected to all routes and controllers using Torii? Why is that property inaccessible/undefined within the application.js controller? I'm using https://www.firebase.com/docs/web/libraries/ember/guide.html#section-authentication as a reference.
3) In the guide above the actions for authentication are put inside the route. However, according to this quora post the route should only handle template rendering and model interfacing. Is this post incorrect? The authentication logic should reside in the application.js controller correct? https://www.quora.com/What-is-the-best-way-to-learn-Ember-js
1) Is the 'this' keyword getting confused with something other than the Ember controller?
Yes. This is one of the most common sticking points of Javascript. There's a lot of articles out there about it, but this one looked pretty good. To solve it you'll either need to use an arrow function, bind the function to the current context, or save the context in a local variable. (Read that article first though.)
2) Also, I've noticed that when using Emberfire I'm able to use the property 'session.isAuthenticated' within the template application.hbs however, shouldn't 'session' be an object that is injected to all routes and controllers using Torii? Why is that property inaccessible/undefined within the application.js controller? ...
That's because the template pulls the property from the current context (your controller). Inside of your controller you'll have to use this.get('session') instead. (After you fix the issue I mentioned above.)
3) ... Is this post incorrect? ...
I wouldn't say incorrect, just a bit oversimplified. I would follow whatever conventions the library uses as that's probably the best way given the library's requirements.
You're partially right about this although it's not really confused. this (where you're modal call doesn't work) isn't scoped to the Controller anymore, because it's inside a function. Either:
replace the function (data) call with data => if you're using ember cli. Or
var _self = this; up top and reference _self instead.
This should at least get you started.

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

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.

Using Ember.js, how do I run some JS after a view is rendered?

How do I run a function after an Ember View is inserted into the DOM?
Here's my use-case: I'd like to use jQuery UI sortable to allow sorting.
You need to override didInsertElement as it's "Called when the element of the view has been inserted into the DOM. Override this function to do any set up that requires an element in the document body."
Inside the didInsertElement callback, you can use this.$() to get a jQuery object for the view's element.
Reference: https://github.com/emberjs/ember.js/blob/master/packages/ember-views/lib/views/view.js
You can also use afterRender method
didInsertElement: function () {
Ember.run.scheduleOnce('afterRender', this, function () {
//Put your code here what you want to do after render of a view
});
}
Ember 2.x: View is deprecated, use component instead
You have to understand the component's lifecycle to know when does certain things happen.
As components are rendered, re-rendered and finally removed, Ember provides lifecycle hooks that allow you to run code at specific times in a component's life.
https://guides.emberjs.com/v2.6.0/components/the-component-lifecycle/
Generally, didInsertElement is a great place to integrate with 3rd-party libraries.
This hook guarantees two (2) things,
The component's element has been both created and inserted into the DOM.
The component's element is accessible via the component's $() method.
In you need JavaScript to run whenever the attributes change
Run your code inside didRender hook.
Once again, please read the lifecycle documentation above for more information
Starting with Ember 3.13, you can use components that inherit from Glimmer, and this example below shows what that could look like:
import Component from '#glimmer/component';
import { action } from '#ember/object';
/* global jQuery */
export default class MyOctaneComponent extends Component {
#action configureSorting(element) {
jQuery(element).sortable();
}
}
<div {{did-insert this.configureSorting}}>
<span>1</span>
<span>2</span>
<span>3</span>
</div>
These view style components don't have lifecycle hooks directly, instead, you can use render-modifiers to attach a function. Unofficial introduction to modifiers can be found here
The benefit of this is that, it's clearer what the responsibilities of the template are and become.
Here is a runnable codesandbox if you want to play around with this:
https://codesandbox.io/s/octane-starter-ftt8s
You need to fire whatever you want in the didInsertElement callback in your View:
MyEmberApp.PostsIndexView = Ember.View.extend({
didInsertElement: function(){
// 'this' refers to the view's template element.
this.$('table.has-datatable').DataTable();
}
});