Can console.log be passed in to test a Ember.js component from my application template? - ember.js

I am building a simple button component and I would like to test that my click handler is working by passing in console.log (or some other function) into my component. This is in Ember 4.
app/components/eu-button.hbs looks like:
<button
type={{ this.type }}
class={{ this.colors }}
...attributes
{{on "click" (fn #onClick) }}
>
{{#if this.args.icon }}<FaIcon #icon={{ this.args.icon }} />{{/if}}
{{ this.args.text }}
{{ yield }}
</button>
and implementation is:
import Component from '#glimmer/component';
export default class EuButtonComponent extends Component {
get type() { return "button"; }
get colors() { return "various classes"; }
}
I am calling it from my app/templates/application.hbs like this:
<EuButton #text="Test" #icon="pencil" #onClick={{ fn console.log "test" }}/>
In the hopes that I could see the console print the word "test" on a button click. However, I'm getting:
Uncaught Error: Attempted to resolve a value in a strict mode template, but that value was not in scope: console
I have tried passing in #onClick={{ fn window.console.log "test" }} and #onClick={{ fn document.console.log "test" }} with similar errors.
I think my error is more a misunderstanding of JS that Ember (or Glimmer) so I'd appreciate any help on understanding that function's scope or, alternately, a function I could use in place of console.log in this way.

I don't think you can do it from the .hbs file. But if that's not your goal (which i doubt it is) then you can add a function called an action to your controller application.js (or wherever you call the component from; might be a containing component) file and there you'll be able to do whatever you want.
import Controller from '#ember/controller';
// add this decorator here
import { action } from '#ember/object';
export default class ApplicationController extends Controller {
// here's your handler
#action
onClick(message) {
console.log(message);
}
}
then you'll be able to call it from the .hbs:
<EuButton #text="Test" #icon="pencil" #onClick={{ fn this.onClick "test" }}/>
Relevant reading: https://guides.emberjs.com/release/components/component-state-and-actions/#toc_html-modifiers-and-actions or even better https://guides.emberjs.com/release/in-depth-topics/patterns-for-actions/

Related

Getting error after migrating to Ember octane

I am using Ember 3.16.3 and i am getting this error on the following code :
Error: Assertion Failed: You must pass a function as the second
argument to the on modifier
//login.hbs
<form {{on "submit" this.login}}>
<Input type="email" placeholder="email" #value={{this.email}} />
<button type="submit">login</button>
</form>
.
//login.js
import Route from '#ember/routing/route';
import { tracked } from '#glimmer/tracking';
import { action } from '#ember/object';
export default class LoginRoute extends Route {
#tracked email = '';
#action
login(event) {
event.preventDefault();
// do some operations ...
}
}
As specified in the error, the on modifier should receive a valid function to execute. As mentioned in the guides,
If you add the {{action}} helper to any HTML DOM element, when a user clicks the element, the named event will be sent to the template's corresponding component or controller.
This holds good for on modifier or any values that are used in the template. You can think of routes to be a part where we fetch data for the corresponding page. Any other backing property or computation has to be defined inside a controller or component to be used in the template.
Hence, moving your login action to a Controller will solve this issue. Additionally, you need to move email to the controller as well, or you won't see updates to it work correctly.

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

Ember: globally-available search component (and action)?

I want to have a search field inside a component that can be placed anywhere in the app. It can appear on any template, or nested in components. The search form would accept user input (search term) and submit would trigger a search action which transitions to a results template.
Seems simple enough, but I can't figure out how to make an action globally available. And if I could, how do you pass the inputted term to the action in the first place? There's surprisingly little info on how to handle form submits with Ember CLI.
Thus far I've just been submitting a regular form with action='/results'. But that's obviously reloading the app.
I've been messing with creating an action in the index controller like this:
export default Ember.Controller.extend(defaultParams, {
term: '',
actions: {
keywordSearch() {
this.transitionToRoute('results', { queryParams: { q: this.get('term') }});
}
}
});
Then passing a closure action down to my search component, which is nested 2 deep from the index template.
index.hbs:
{{index-search keywordSearch=(action "keywordSearch")}}
index-search.hbs (component):
{{search-field keywordSearch=keywordSearch }}
search-field.hbs (nested component):
<form {{ action (action keywordSearch) on='submit' }}>
{{ input value=term }}
<button type="submit">Search</button>
</form>
And that will run the action, but the term is not supplied. How do you supply term to the closure action?
And...do I really need to pass the action down to every single place the search field is going to appear in the app, or is there an easier way to do it?
Instead of writing actions in all components and routes, you can create a service for search. Inject the service into the component and handle the route transition from service method. Check the sample code below,
Search-component.hbs
<form {{ action (action search) on='submit' }}>
{{ input value=keyword }}
<button type="submit">Search</button>
</form>
Search-component.js
export default Ember.Component.extend({
globalSearch: Ember.inject.service('search'),
actions: {
search() {
const { keyword } = this.getProperties('keyword');
this.get('globalSearch').showResults(keyword).then(() => {
alert('Success');
}, (err) => {
alert('Error while searching: ' + err.responseText);
});
}
}
});
Service - app/services/search.js
import Ember from 'ember';
export default Ember.Service.extend({
init() {
this._super(...arguments);
},
showResults(keyword) {
// write code for transition to search results route here
}
});

Ember.js this.set() not working from inside action

Folks, I'm having some beginner problem and would love some assistance. This is in ember 2.6 if it makes any difference. Basically, I'm trying to change some text on the screen when the button is pressed inside a component. Here's the handlebars and component js code:
<h2>Stupid Button</h2>
<div>
<form id="sign_in_form" {{action 'triggerTheAction' on='submit'}}>
<button id="signin" type="submit">FIRE!</button>
{{someTextThatShouldChange}}
</form>
</div>
And the component's js code:
export default Ember.Component.extend({
someTextThatShouldChange: 'Initial Value',
actions: {
triggerTheAction: (email, password) => {
alert('alerts just fine');
this.set('someTextThatShouldChange', 'New value for text!');
}
}
});
The error I get in console:
TypeError: _this.set is not a function
Really appreciate the help. I have a feeling I'm just not understanding something fundamental about Ember.
You are using an arrow function to define the action handler. This means that it's binding this to the outer scope, which in your case is the ES6 module. By the specification, a module's this is undefined.

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();
}
}