Send actions to the Application controller in EmberJS - ember.js

I'm trying to toggle a global property on the application controller, by clicking a button on one of the templates. I've read some stuff on action bubbling but can't it to work.
Here's the property and action on the Application controller
export default Ember.Controller.extend({
isAuthenticated: true,
actions: {
logIn: function(){
this.toggleProperty('isAuthenticated');
}
}
});
And here's the action with a login.hbs template file (I'll turn this into a proper button soon)
<span {{action 'logIn'}}>
{{#link-to 'boards' class="btn-l bg-blue white db mtl link bn w-100"}}Login{{/link-to}}
</span>
How could I ensure the action toggles the property on the Application Controller?

In your login controller,you need to inject application controller.
import Ember from 'ember';
export default Ember.Controller.extend({
appCont:Ember.inject.controller('application')
});
and in login.hbs you need to specify which target will receive the method call.
<button {{action 'logIn' target=appCont}}> Login </button>
In this <button {{action 'logIn'}}> Login </button> , context of a template is login controller, actions used this way will bubble to login route when the login controller does not implement the specified action. Once an action hits a login route, it will bubble through the route hierarchy.
Reference: http://emberjs.com/api/classes/Ember.Templates.helpers.html#toc_specifying-a-target
EDIT: If you want to call functions available in Controller inside Component then you need to pass actions toComponent`
Login.hbs
{{component-a logIn=(action 'logIn') }}
component-a.hbs
<button {{action (action logIn)}}> LogIn</button>

Related

Ember component call an action in a route or controller

I have a component the main purpose of which is to display a row of items.
Every row has a delete button to make it possible to delete a row. How is possible to pass an action from a template to the component which will trigger an action in a router ?
Here is the template using the component:
#templates/holiday-hours.hbs
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true}}
{{/each}}
Here is the component template:
# templates/components/holiday-hour.hbs
...
div class="col-sm-1">
{{#if shouldDisplayDeleteIcon}}
<button type="button" class="btn btn-danger btn-sm mt-1" {{action 'deleteHoliday' holiday}}>
<span class="oi oi-trash"></span>
</button>
{{/if}}
</div>
I'm using the same component to display a row and to create a new item (holiday-hour).
I'm using ember 3.1.2
Thank you
You have to send the actions up from the component to the route. The main way to do this is by adding actions to your component that "send" the action to the parent. Once the action is sent you have to tell the component what action on the route to trigger by passing in the action as a parameter. Below is an example of how to do this.
Component js
# components/holiday-hour.js
...
actions: {
deleteHoliday(){
this.sendAction('deleteHoliday');
}
}
Template for route
#templates/holiday-hours.hbs
...
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true deleteHoliday='deleteHoliday'}}
{{/each}}
Route js
#routes/holiday-hours.js
...
actions: {
deleteHoliday(){
//code to delete holiday
}
}
I will try to give a general answer because your question is not giving enough/all info regarding the route actions etc. Long answer short, using closure functions. Assuming this is your route js file routes/holiday-hours.js
import Route from '#ember/routing/route';
export default Route.extend({
model(){ /*... some code */ },
setupController(controller){
this._super(controller);
controller.set('actions', {
passToComponent: function(param) { //.... function logic }
})
}
});
Note: in the above snippet, I'm using setupController to create actions. Alternatively, you can put the actions inside a controller file otherwise actions directly inside the route will throw an error.
So I want the action passToComponent to be called from the component. This is what you do to make it accessible inside the component.
{{#each model as |holidayHour|}} {{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true callAction=(action 'passToComponent')} {{/each}}
Now we have passed the action to the component and here's how to call it from the component. Note: I have added a param just to show that it can take a param when called within the component.
import Component from '#ember/component';
export default Component.extend({
actions: {
deleteHoliday: ()=> {
this.get('callAction')() /*Pass in any params in the brackets*/
}
}
});
You will also see demonstrations using sendAction which is rather old and acts more of an event bus that is not very efficient. Read more from this article

How to create action for my own component

I am creating one ember app.
Flow is like " page1 displays list of feeds item and clicking on any of the feed will take user to page2 showing details about that feed"
What i am doing:
i have one component named app-feed. Template is as below
<div onclick={{action 'click' feed}}>
{{#paper-card class="card-small" as |card|}}
<!-- --> {{card.image src=feed.imagePath class="small-feed-img" alt=feed.title}}<!---->
{{#card.header class="flex-box short-padding" as |header|}}
{{#header.avatar}}
<img class="profile-small" src="http://app.com/users/{{feed.userName}}.jpg" alt="{{feed.name}}" />
{{/header.avatar}}
<span class="tag-sm like-box">
{{feed.likes}} {{paper-icon "thumb_up" size="18"}}
{{feed.commentCount}}{{paper-icon "chat_bubble" size="18"}}
</span>
{{/card.header}}
{{#card.actions class="action-block"}}
{{#paper-button iconButton=true}}{{paper-icon "favorite" size="18"}}{{/paper-button}}
{{#paper-button iconButton=true}}{{paper-icon "share" size="18"}}{{/paper-button}}
{{#paper-button iconButton=true}}{{paper-icon "shopping_basket" size="18"}}{{/paper-button}}
{{/card.actions}}
{{/paper-card}}
</div>
component.js is as below
import Ember from 'ember';
export default Ember.Component.extend({
actions:{
click(feed){
console.log("Click event fired:"+feed.id); //Output is correct in console
this.sendAction("onClick", feed); //sending onClick Action
}
}
});
I'm populating list of this component in one of my route.
Template is as below
{{#app-sidenav user=model}}{{/app-sidenav}}
<div class="content">
<div class="row">
{{#each model as |item|}}
{{#app-feed-small onClick=(action "getDetail" item) class="col-xs-5" feed=item}} {{/app-feed-small}}
{{/each}}
</div>
</div>
route.js is as below
import Ember from 'ember';
export default Ember.Route.extend({
store: Ember.inject.service(),
model(){
//Populating module. Works just fine
} ,
actions:{
getDetails(feed){
console.log("Getting details of "+feed.id);
}
}
});
I have defined getDetails action as mentioned in my template.js of the route still i am getting below error
""Assertion Failed: An action named 'getDetail' was not found in (generated feed.index controller)""
feed.index is my route.
I used same method and modified paper-chip's source to get action corresponding to click on paper-chip's item which worked. But i am not able to do same in my own component.
Please let me know what is missing
Your problem is that in your second last code snippet, the one with your template. You refer to the action as getDetail but in route.js your last code snippet you declare the action as getDetails which is different to the code in your template. It's a common spelling error, one has an "s" st the end whereas the other doesn't.
The actions should be in controllers. And if controller bubbles up then the action in route be called.
For your case you don't need controller.
You can use ember-transition-helper
I assume you have in router.js :
this.route('feeds', function(){
this.route('edit', {path: '/:id'});
});
Now your template is going to be :
{#app-sidenav user=model}}{{/app-sidenav}}
<div class="content">
<div class="row">
{{#each model as |item|}}
{{#app-feed-small onClick=(transition-to "feeds.edit" item) class="col-xs-5" feed=item}} {{/app-feed-small}}
{{/each}}
</div>
</div>
sendAction is an ancient way to calling action inside controller/route.
The new style is to use closure action, which passes action as a value by creating a closure at the time of value passing.
Yes, you are correct. The action has been sendAction is able to bubble up from,
correspond controller -> correspond route -> upper route -> ... -> application route
However, closure action does NOT bubble.
Please refer to Ember component send action to route where #kumkanillam detailed explained how to call action inside route using different method and the differences between sendAction and closure action.
I have also made a sample project and write a simple explanation for it at,
https://github.com/li-xinyang/FE_Ember_Closure_Action

Integrating ember-modal-dialog inside a site header Ember 2.0

I'm very new to the Ember framework and I have a question regarding getting a modal setup. I have the site-header as a component. When I click a login button I'd like for a modal to popup. I found the Ember Modal Dialog
plugin and was able to set it up so that there is a modal always shown in application.hbs. However, I'm having trouble understanding a couple of things but first here are my files.
application.hbs
{{site-header}}
{{outlet}}
{{#if isShowingModal}}
{{#modal-dialog close="toggleModal"
alignment="center"
translucentOverlay=true}}
Oh hai there!
{{/modal-dialog}}
{{/if}}
{{site-footer}}
site-header.js
import Ember from 'ember';
export default Ember.Component.extend({
isShowingModal: false,
actions: {
toggleModal: function() {
this.toggleProperty('isShowingModal');
}
}
});
So, I have this code for the button in my site-header.hbs:
<li class="login-button"><button class="btn btn-block btn-danger" {{action 'toggleModal'}}>Login</button></li>
As I understand it, action is saying to find toggleModal property in the site-header.js and execute the function above which does the property toggling.
However, how does application.hbs "see" the value of isShowingModal? Can it even see that value since the modal isn't showing up?
When most developers have modals, do they all go inside application.hbs since you want them to appear in the middle of the screen {{outlet}} area? How can you improve the process for including multiple modals?
What changes should I make to make the modal show up when the user clicks a button in the header?
Ok give this a try. In site-header.js
export default Ember.Component.extend({
actions: {
toggleModal() {
this.sendAction('toggleModal');
}
}
});
Application.hbs
{{site-header toggleModal='toggleModal'}}
{{outlet}}
{{#if isShowingModal}}
{{#modal-dialog close="toggleModal"
alignment="center"
translucentOverlay=true}}
Oh hai there!
{{/modal-dialog}}
{{/if}}
{{site-footer}}
The application controller
export default Ember.Controller.extend({
actions: {
toggleModal() {
this.toggleProperty('isShowingModal')
}
}
})
So the action is sent from the site-header component to the site-header component. From there it gets sent to the application controller. In application.hbs, toggleProperty='toggleProperty' connects the action from the component to the controller's action hash. In the controller action hash, it gets handled by toggleModal() and toggles the isShowingModal property. When the modal is closed, ember-modal-dialog fires an close action that is handled by the toggleModal() which again toggles the isShowingModal property.
Application template cannot see the properties of site-header. Only site-header component can see its properties. There are other methods to access them.
It is not necessary to include all modals in application. The plugin will most probably handle the positioning. In your case, you can move the modal code to the site-header component also.
Put the modal code in your site-header template. (or) You can have isShowingModal variable in application, send an action from site-header to application and toggle its value.

Rendering a form inside a bootstrap modal in Ember

There are plenty of questions already that ask about modals in Ember (like this one and this one). Even the cookbook has an article that explains how to use modals, but none of these cover form submissions that require validation from the server within the modal (ie, username already taken).
Following along with the cookbook, I have a named outlet in my application template.
{{outlet}}
{{outlet modal}}
And an action that triggers rendering a template inside the modal outlet (using bootstrap).
App.ApplicationRoute = Ember.Route.extend
actions:
openModal: (template, model) ->
#controllerFor(template).set('model', model)
#render template, into: 'application', outlet: 'modal'
closeModal: ->
#render '', into: 'application', outlet: 'modal'
Obviously I'm calling this action from within a link.
<a href="#" {{action openModal "person.edit" model}}>Edit</a>
Where model is the model of the current route.
In the PesonEditView I hook into the didInsertElement function to enable the bootstrap modal. Inside this hook, I also listen to the hidden.bs.modal event fired by bootstrap when the close button is clicked to clear out the modal outlet.
App.PersonEditView = Ember.View.extend
didInsertElement: ->
#$('.modal').modal('show').on 'hidden.bs.modal', =>
#get('controller').send('closeModal')
My question is, how can I define a save action that will close the modal (using bootstraps animations) after it has validated on the server? The sequence of events that I see are required are, 1) save gets triggered on controller, 2) if successful save, close the modal (which would require calling #$('.modal').modal('hide') from the view).
I'm not sure what Ember experts would suggest here, since it seems as though the view and controller will need to communicate very closely.
Thanks!
EDIT
In response to edpaez's comment, I have tried resolving the promise returned by save from within the view. For example, in my template
<button type="button" class="btn btn-primary" {{action "save" target="view"}}>Save</button>
The view
App.PersonEditView = Ember.View.extend
actions:
save: ->
#get('controller').send('save').then(#closeModal.bind(#))
closeModal: ->
#$('.modal').modal('hide')
# the rest omitted for brevity
And the controller
App.PersonEditController = Ember.ObjectController.extend
actions:
save: ->
#get('model').save()
I get the following error
Uncaught TypeError: Cannot call method 'then' of undefined
Try targeting the save action to the view:
<button type="button" class="btn btn-primary" {{action "save" target="view"}}>Save</button>
and call a save method in the controller. That method would return a promise that would resolve when the save method from the model resolves.
App.PersonEditView = Ember.View.extend
actions:
save: ->
#get('controller').save().then(#closeModal.bind(#))
closeModal: ->
#$('.modal').modal('hide')
App.PersonEditController = Ember.ObjectController.extend
save: ->
#get('model').save()
This way, the controller abstracts the model-saving logic and the view gets notified when the model saves so it can behave as expected.
Make sure you call the save method in the controller instead of sending an action. the send method returns nothing.
Hope it helps!

Binding a controller using ember view helper, or, conditional outlet usage

I am trying to accomplish something like this, in an ember view:
{{#if loggedIn}}
<p> I'm Logged In! </p>
{{else}}
{{view App.LoginView contentBinding="App.UserInfo"}}
{{/if}}
This doesn't work out of the box because the context for LoginView ought to be loginController, and *that controller's content" ought the be App.UserInfo.
This discussion has some related notes, and suggests outlets:
Instantiate a controller class using the {{view}} helper?
Outlets provide a clean solution to this - for example, I could do:
{{#if loggedIn}}
<p> I'm Logged In! </p>
{{else}}
{{outlet login}}
{{/if}}
and then have the router connect the controller for this view (call it homeController) to the login outlet with LoginView and some context.
However, using outlets, if the loggedIn property changes, the outlet isn't reconnected/redrawn, so if I log in and then log out again I get a blank page.
Is there a nice way to either bind the appropriate controller and controller content using the view helper, or set up the outlet in a way that makes it redraw appropriately if the loggedIn property changes?