Ember component call an action in a route or controller - ember.js

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

Related

Ember integration test fails after click event

I have this integration test:
test('can change chord text', function(assert) {
this.render(hbs`{{chart-editor-chord chord=chord}}`);
this.$().click();
assert.ok(!!this.$('.chord-input').length);
});
but the assertion fails, the component template looks like this:
<div {{action 'changeChord'}} class="measure-chord chord-big">
{{#if chord.editing}}
<input type="text" value="{{chord.name}}" class="chord-input">
{{else}}
{{chord.name}}
{{/if}}
</div>
and the component code:
import Ember from 'ember';
export default Ember.Component.extend({
store: Ember.inject.service(),
actions: {
changeChord() {
this.chord.set('editing', true);
}
}
});
I'm updating the chord model in the changeChord() action and it does work if I test in the browser, but the integration test fails. So, does this change in the model have to be rendered synchronously to the template? I tried using wait() in the test but that doesn't make a difference. So how should I test this?
While I'm trying to create a twiddle for you, I found three things:
Where do you create chord mock in your test?
You are not sending event to the correct html component. Use this.$('.measure-chord') or this.$('.chord-big').
Instead of this.chord.set you should use this.get('chord').set. Actually Ember.set(this, 'chord.isEditing', ...) is even better.
And bonus: You don't need a div wrapper, component does this for you.
twiddles:
working copy
without div
It looks like your click helper is clicking the div that your component.js controls instead of the initial div in your template. If you specify the div in your click helper it should work:
this.$('.measure-chord').click();

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

Ember2.8: Sending an action from a component to the controller

Reading up on the documentation for Ember, I was under the impression that when an action is triggered by a component, it will go up the hierarchy until it hits an action with that name. But here's what's happening right now. I have a game-card component written like so:
game-card.hbs
<div class="flipper">
<div class="front"></div>
<div class="back">
<img {{action "proveImAlive"}} src={{symbol}} />
</div>
</div>
game-card.js
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['flip-container'],
actions: {
//blank for now because testing for bubbling up
}
});
Now according to what I've read, since game-card.js does not have a 'proveImAlive' action, it will try to bubble up the hierarchy i.e. the controller for the particular route.
play.js (the route /play)
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
proveImAlive() {
console.log('Im aliiiiveeee');
}
}
});
But when I finally run my application, I get this error:
Uncaught Error: Assertion Failed: <testground#component:game-card::ember483> had no action handler for: proveImAlive
Now my question is twofold:
Why is this error happening?
I want some of my component's actions to bubble up to the route's controller. For example, when a game-card is clicked, i'd like to send the id value (to be implemented) of that card up to the controller so it can store it on an array.
game-card is clicked --> sends value of 1 --> arrayinController.push(1)
How can I achieve this?
First, I'd like to point out that you linked to the documentation of Ember v1.10.0. You should consult the documentation for the version of Ember you are utilizing, which you mention is v2.8.0.
Now according to what I've read, since game-card.js does not have a 'proveImAlive' action, it will try to bubble up the hierarchy i.e. the controller for the particular route.
This isn't quite what happens because components are isolated, so there is no implicit bubbling. When the Guides say "actions sent from components first go to the template's controller" and "it will bubble to the template's route, and then up the route hierarchy" they mean that you have to explicitly send an action up from the Component. If the component is nested inside another component, you have to do this for each layer, until you reach the Controller.
Why is this error happening?
You need to bind the action in the template: {{game-card proveImAlive="proveImAlive"}}
i'd like to send the id value (to be implemented) of that card up to the controller so it can store it on an array.
I am going to be using closure actions for this part of the answer. As mentioned by #kumkanillam, they have better ergonomics, and they are the current proposed way to use actions if you consult the Guides.
I have prepared a Twiddle for you.
a) Initialize array in the controller
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
gameCards: null,
init() {
this.set('gameCards', []);
}
}
b) Implement the action that pushed to the array
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
gameCards: null,
init() {
this.set('gameCards', []);
},
actions: {
proveImAlive(cardNo) {
this.get('gameCards').pushObject(cardNo);
console.log('Im aliiiiveeee - cardNo', cardNo);
}
}
});
c) Bind the closure action down
{{game-card proveImAlive=(action 'proveImAlive')}}
d) Trigger the action passing the arguments
<div class="flipper">
<div class="front"></div>
<div class="back">
<button {{action proveImAlive 1}}> ProveIamAlive</button>
</div>
</div>
You need to explicitly set the action handler:
{{component-name fooAction=fooHandler}}
This is required because it helps keep components modular and reusable. Implicit links could result in a component triggering unintended behavior.
Your code should work, only if you have included game-card component into play.hbs. I doubt the controller for the particular route is not play in your case.
Here is the working-twiddle
Instead of bubbling actions, use closure actions. For better understanding you can go through the below links,
https://dockyard.com/blog/2015/10/29/ember-best-practice-stop-bubbling-and-use-closure-actions
http://miguelcamba.com/blog/2016/01/24/ember-closure-actions-in-depth/
https://emberigniter.com/send-action-does-not-fire/

How to communicate nested component to mixin without using service

I have a nested component. The child-component has an input field bind with mixin variable and action, In parent-component had button action. ( without mixin because parent-component considered as a addon ) while button action triggers the child-component value update to the mixin variable. How trigger a child action from parent-component.
Note: Please refer the attached demo link
https://ember-twiddle.com/d8b01ba563b555fc01374f300db20c5b?openFiles=components.child-component.js%2C
An easier approach than triggering actions on your child-component would be to pass the updated_val down to the child component, and let the ember value-binding do the rest. When the value has changed and you click update, make your ajax call from your dialog-component.
e.g. for passing your updated_val down
//application.hbs
{{dialog-component updated_val=updated_val}}
But since your question was "How to trigger actions on a child component", this might be an approach (see updated twiddle):
//dialog-component.js
import Ember from 'ember';
export default Ember.Component.extend( {
actionCalled: false,
actions:{
callChildAction() {
this.toggleProperty( 'actionCalled' );
},
updateValue(updateVal) {
this.set('updated_val', updateVal);
}
}
});
//dialog-component.hbs
<div class='dialog'>
{{!pass your 'updateValue' action from the dialog-component to the child-component}}
{{ child-component actionCalled=actionCalled updateValue=(action 'updateValue')}}
<button {{action 'callChildAction' }}> Update </button>
</div>
//child-component.js
import Ember from 'ember';
export default Ember.Component.extend( {
child_val: '',
actionObserver: Ember.observer('actionCalled', function(){
this.send('childAction')
}),
actions:{
childAction(){
alert( 'childAction called..' );
// some validation and ajax call.
this.sendAction('updateValue', this.get('child_val'));
},
}
});
//child-component.hbs
{{input value=child_val}}

How can we get the original event in ember's action

I'm updating a personal project where I used the ember.js version 0.9.x.
So a new version was released and I have a problem related with ember action.
I have the following html code:
<li><a href="#" id="startApp" {{action activateView target="view"}}> Home</a> <span class="divider">|</span></li>
where, when I click its call this function activateView:
activateView: function(event, context) {
console.log(event);
}
but the event and the context are undefined. I've already tried this.context and it returns undefined.
The main idea its obtain the id of the link when the user click.
I know about routes and the handlebar helper link to, but I really need that id for other things,
In Ember 2...
Inside your action you always have access to the Javascript event object which has the DOM element e.g.
actions: {
myAction() {
console.log(event.target) // prints the DOM node reference
}
}
The event is not passed using the action helper. If you really want the event object, you need to define a view and use the click event:
App.MyLink = Em.View.extend({
click: function(e) {
}
});
and then:
<li>{{view App.MyLink}}</li>
but requiring access to the dom event is a rare case, because you can pass arguments to {{action}}. In your case:
<li><a href="#" id="startApp" {{action activateView "startApp" target="view"}}> Home</a> <span class="divider">|</span></li>
and in the event:
activateView: function(id) {
console.log(id);
}
There are two ways you can receive event object in actions,
1.If you are using component, then you can define any of this list of event names in component and that is designed to receive native event object. eg., {{my-button model=model}}
export default Ember.Component.extend({
click(event){
//oncliking on this componen will trigger this function
return true; //to bubble this event up
}
})
2.If you are using html tag like button then you need to assign a (closure) action to an inline event handler.
{{#each item as |model|}}
<button onclick={{action 'toggle' model}}>{{model.title}}</button>
{{/each}}
In actions hash toggle function will always receive native browser event object as the last argument.
actions:{
toggle(model,event){
}
}
In the below format, action toggle will not receive event object,
<button {{action 'toggle'}}>{{model.title}}</button>
Input helpers such as {{input key-press="toggle" and {{text-area key-press="toggle"
Explained really well in ember guide https://guides.emberjs.com/v2.12.0/components/handling-events/#toc_sending-actions
you need to pass the id into your function like so to have it accessible in the view, you can pass along what ever you want, but in your example this should do it
html
<li><a href="#" id="startApp" {{action activateView "startApp" target="view"}}> Home</a> <span class="divider">|</span></li>
then you have access to the id or what ever you passed in, in the view
js
...
activateView: function(data){
console.log(data); // should be the ID "startApp"
}
...
Just use event handler directly.
Reference: https://github.com/emberjs/ember.js/issues/1684
I don't have enough reputation for a comment, but here is the relevant documentation using Ember Octane.
The callback function will receive the event as its first argument:
import Component from '#glimmer/component';
import { action } from '#ember/object';
export default class ExampleComponent extends Component {
#action
handleClick(event) {
event.preventDefault();
}
}