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();
Related
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
I have an {{input}} inside of an element that itself has an action.
How do I prevent clicking into the input from triggering the click event on the parent element?
I have tried bubbles=false.
I have also tried extending {{input}} and in the extended input I caught the click event and called event.preventDefault()
Please try my test case: https://ember-twiddle.com/a2cee9abd63a7400124e2745a7820cf8?numColumns=2&openFiles=controllers.application.js%2Ctemplates.application.hbs
Example hbs:
<div {{action "alert"}}>
I don't want the click into the input to trigger the div's onlick. Help!<br/>
{{input bubbles=false}}
</div>
Define component say my-input which extends Ember.TextField and define click function and return false to prevent bubbling.
import Ember from 'ember';
export default Ember.TextField.extend({
click(){
console.log('click my input');
return false;
}
});
For my-input component, you dont need to define anything to handle click event while including the component. you can just say {{my-input }}
Here is the working twiddle
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/
I have a component that should bubble an action up to its template router.
I pass the name of the action to the component:
{{project-table projects=model viewProject="viewProject"}}
Inside my component (project-table), I have:
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
viewProject: function (project) {
this.sendAction('viewProject', project);
}
}
});
Inside the component template, I have:
<button type="button" {{action "viewProject" project}}>
My Button
</button>
Last but not least, I have my router:
actions: {
viewProject: function (project) {
this.transitionToRoute('project', project);
}
}
The component's action gets invoked correctly. However from there on, the action does not bubble up. Any ideas as to what I might be going wrong?
#JB2, your code is almost perfect, and I'm sure that the action is bubble up to the Route level.
However, please note, that Route has transitionTo method only. http://emberjs.com/api/classes/Ember.Route.html#method_transitionTo
In a Controller, you can use transitionToRoute method.
http://emberjs.com/api/classes/Ember.Controller.html#method_transitionToRoute
It is easy to mix. :) I check the API doc also quite often, which one could I use and where. :)
I have a component with a jquery menu that transforms the DOM. I need to re render the component for initiate the structure again. The jquery menu doesn't work dinamically, so I need to re render the component.
//Parent Component hbs
<div id="container">
{{menu-jquery model=model}}
</div>
//Parent Component js
export default Ember.Component.extend({
refreshMenuData(){
callToServer()// ajax call
updateModel()// generate model from ajax response
-> //how to delete and create menu component? or re render menu component?
}
}
Thanks
A component has a function called rerenderthat you can call to force a rerender of the component. Although, I might recommend a better approach. Upon resolution of the promise that fetches your server data, manually destroy the jquery plugin and then reinit said plugin.
I found a workaround putting the component inside a conditional block. I set loading in false before the server call and true after data parsing.
{{#if loading}}
// show loading message
{{else}}
{{menu-jquery model=model}}
{{/if}}
This force the menu component to re render.
Your question is not quite clear. I am assuming that, you will be using jquery ajax for server calls inside the component. In Ember.Component, you have to use .on("didInsertElement") which works similarly as that of jQuery(document).ready(). So your component will look something like this
export default Ember.Component.extend({
_onReady: function() {
refreshMenuData();
//whatever jquery code you want to execute here
}.on('didInsertElement')
})
If you can provide your example in Ember Twiddle or JSBin, I can help you better.