I'm discovering EmberJS and started to migrate an existing website to this framework. I was having an issue with a Bootstrap-based dropdown. This issue actually helped me understand Ember's concepts a bit better but I still have some questions.
I used the ember-bootstrap module to generate this dropdown (among other things) and here is what the code is supposed to be:
{{#bs-dropdown as |dd|}}
{{#dd.button}}
Sort by
{{/dd.button}}
{{#dd.menu as |ddm|}}
{{#ddm.item}}{{#ddm.link-to "index"}}Price low to high{{/ddm.link-to}}{{/ddm.item}}
{{#ddm.item}}{{#ddm.link-to "index"}}Price high to low{{/ddm.link-to}}{{/ddm.item}}
{{/dd.menu}}
{{/bs-dropdown}}
Now, I want some javascript code to be executed when the user clicks on one of the items. After checking the module's documentation, I found where the menu item component was defined and edited its code as follows:
export default Component.extend({
layout,
classNameBindings: ['containerClass'],
/* ... */
actions: {
// My addition
sortByPrice(param){
alert("sorting");
},
// End of the addition
toggleDropdown() {
if (this.get('isOpen')) {
this.send('closeDropdown');
} else {
this.send('openDropdown');
}
},
},
});
Then I updated the hbs file as follows:
{{#dd.menu as |ddm|}}
{{#ddm.item action "sortByPrice" low_to_high}}
{{#ddm.link-to "index" action "sortByPrice" low_to_high}}
Prix croissant
{{/ddm.link-to}}
{{/ddm.item}}
{{/dd.menu}}
This didn't work, and that's why you I added the *action* to the link-to element as well and declared similarly the action on its component file.
import LinkComponent from '@ember/routing/link-component';
export default LinkComponent.extend({
actions: {
sortByPrice(param){
alert("sorting");
console.log("sorting");
},
},
});
As you can see, the *link-to* component extends the LinkComponent one. I eventually understood that it wasn't possible for this element to handle click events natively, as explained in this thread.
Out of frustration, I ended up with a less elegant approach that still does the trick:
{{#bs-dropdown id="sort" as |dd|}}
{{#dd.button}}
Sort by
{{/dd.button}}
{{#dd.menu as |ddm|}}
{{#ddm.item action "sortByPrice" low_to_high}}
<a
class="dropdown-item"
onclick="sortByPrice('low_to_high'); return false;"
href="#"
>
Price low to high
</a>
{{/ddm.item}}
{{/dd.menu}}
{{/bs-dropdown}}
Now here are my questions:
- Why is it that defining actions on both the Component file and the hbs one didn't change the result?
- Why doesn't the LinkComponent handle click events natively? I get that a link is supposed to redirect users to a new page (which is still arguable), but the DOM event is still fired, so does Ember deliberately ignore it and choose not to let developers handle it? I want to know the logic behind this.
- Is there a better approach than my solution?
Thanks.
Cheers for studying EmberJS and posting a beautiful, explicit question!
Your mistakes
Never modify the code inside
node_modules/andbower_components/folders. If you really need to monkey-patch something, you can do it in an initializer. But your use case does not require monkey patching.You attempted to define an action in the menu item component, but you apply it in a parent template. That action has to be defined in that parent's template component/controller.
This invocation is incorrect:
Here are the problems:
The
ddm.link-tocomponent is supposed to create a link to another route. It does not seem to support passing an action into it.You're just passing a bunch of positional params to the component. If
ddm.link-todid support accepting an action, the correct invocation would look like this:In this case,
"index"is a position param andargNameis a named param.low_to_highwithout quotes is a reference to a property defined on the current scope. You probably meant a string instead:"low_to_high".Never use JS code in template directly. This you should never do in Ember:
Instead, pass an action (defined in the local scope: in a component or controller):
The
onclickproperty name is optional. An action defined without a property impliesonclick(you only need to provide the property name if you need to attach the action to a different event):For the link to be styled properly in a browser, a
hrefattribute is required. But you don't have to pass a value'#'to it. The hash symbol was required in old-school apps to prevent the link from overwriting the URL. Ember overrides URL overwriting for you, so you can simply pass an emptyhref.Here's the final correct usage:
Answers to your questions
Because you defined them in different scopes.
If you define an action in
app/components/foo-bar.js, the action must be applied inapp/templates/components/foo-bar.hbs.If you define an action in
app/controllers/index.js, the action must be applied inapp/templates/index.hbs.In a PWA, you do not do actual page redirects. Such a redirect would reload the whole app.
Instead, the
LinkComponentoverrides the click and tell the Ember's routing system to perform a transition. Routes must be set up properly and the route passed to theLinkComponentmust exist.It seems that your goal is not to perform a transition but to change a variable, so the
LinkComponentis not applicable here. That's unless you wire the sort order property to an URL query param, in which case you can change the sort order by making a transition to a different query param.See below for the simplest approach that uses
ember-bootstrap's dropdown.A working example
Controller:
Template:
Here's a working demo.