Click events on Ember - javascript

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/ and bower_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:
{{#ddm.link-to "index" action "sortByPrice" low_to_high}}
Here are the problems:
The ddm.link-to component 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-to did support accepting an action, the correct invocation would look like this:
{{#ddm.link-to "index" argName=(action "sortByPrice" low_to_high)}}
In this case, "index" is a position param and argName is a named param.
low_to_high without 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:
<a onclick="sortByPrice('low_to_high'); return false;">
Instead, pass an action (defined in the local scope: in a component or controller):
<a onclick={{action 'sortByPrice' 'low_to_high'}}>
The onclick property name is optional. An action defined without a property implies onclick (you only need to provide the property name if you need to attach the action to a different event):
<a {{action 'sortByPrice' 'low_to_high'}}>
For the link to be styled properly in a browser, a href attribute 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 empty href.
Here's the final correct usage:
<a href {{action 'sortByPrice' 'low_to_high'}}>
Answers to your questions
Why is it that defining actions on both the Component file and the hbs one didn't change the result?
Because you defined them in different scopes.
If you define an action in app/components/foo-bar.js, the action must be applied in app/templates/components/foo-bar.hbs.
If you define an action in app/controllers/index.js, the action must be applied in app/templates/index.hbs.
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.
In a PWA, you do not do actual page redirects. Such a redirect would reload the whole app.
Instead, the LinkComponent overrides the click and tell the Ember's routing system to perform a transition. Routes must be set up properly and the route passed to the LinkComponent must exist.
It seems that your goal is not to perform a transition but to change a variable, so the LinkComponent is 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.
Is there a better approach than my solution?
See below for the simplest approach that uses ember-bootstrap's dropdown.
A working example
Controller:
export default Ember.Controller.extend({
isSortAccending: true,
actions: {
changeSortDirection (isSortAccending) {
this.set('isSortAccending', isSortAccending);
}
}
});
Template:
<p>
Current sort order:
{{if isSortAccending "ascending" "descending"}}
</p>
{{#bs-dropdown as |dd|}}
{{#dd.button}}
Sort by
{{/dd.button}}
{{#dd.menu as |ddm|}}
{{#ddm.item}}
<a href {{action "changeSortDirection" true}}>
Price high to low
</a>
{{/ddm.item}}
{{#ddm.item}}
<a href {{action "changeSortDirection" false}}>
Price high to low
</a>
{{/ddm.item}}
{{/dd.menu}}
{{/bs-dropdown}}
Here's a working demo.

Related

Ember passing an action closure through an outlet

I am building a simple Ember app, but I have run into difficulty passing an action closure to a child component when that component is rendered in the {{outlet}} of a navigable container.
For context, here is a quick look at the aesthetically-astonishing app I have been building:
I have a roles/role path that displays a component (the yellow section above) with the following markup. Note that the model for this component is an instance of a Role:
// file: app/components/role.hbs
<p>{{#role.name}}</p>
<div>
{{sel-nav-tabs items=this.tabConfig}}
<div class='route-content'>{{outlet}}</div>
</div>
(Where "sel" stands for "someone else's library".)
this.tabConfig is defines in the corresponding class:
// file: app/components.role.js
import Component from '#glimmer/component';
export default class RoleComponent extends Component {
get tabConfig() {
return [
{ label: 'Users', route: 'roles.role.users' },
{ label: 'Privileges', route: 'roles.role.privileges' },
];
}
}
Into the outlet in role.hbs will be rendered the appropriate list component, either users or privileges.
The users list is rendered by the following component. Note that the model is the list of User instances associated with the Role from its parent:
// file: app/components/role/user-list.hbs
<ul>
{{#each #users as |user|}}
<li>
{{user.name}}
{{#sel-button type="toolbar" onActivate=this.removeUser}}
{{sel-icon/remove-circle}}
{{/sel-button}}
</li>
{{/each}}
</ul>
and when the button is clicked it calls an action defined in the RoleUserListComponent class:
// file: app/components/role/user-list.js
import Component from '#glimmer/component';
import { action } from "#ember/object";
export default class RoleUserListComponent extends Component {
#action removeUser(user) {
// remove the user model from the role... but which role?
}
}
The catch is that the relationship between users and roles is many-to-many, so I can't simply unset the user's owner and let Ember Data take care of things. The obvious answer seemed like passing an action closure from the role component to its child user-list component.
Except, there seems to be no way to pass the action closure through the {{outlet}}. What I was hoping for was something like:
{{outlet onActivate=(action removeUser #role)}}
which would pass the closure to any component that was rendered there. I tried instead to use {{yield user}} in the child to let the parent render the delete button and give it the appropriate action, but that also hit the outlet wall.
I also tried to use controllers, which aren't documented that well, probably since their role seems to have been evolving dramatically over Ember's maturation. But while this brief explanation does mention passing down actions, it doesn't go into details, and the few up-to-date examples I found all seem to break when an outlet joins the party.
I'm suspecting that {{outlet}} just plain isn't closure-friendly.
While defining a service would probably work, that doesn't seem to be what services are intended for, and I'd be cluttering up my global space to solve a local problem.
What is the best practice (or, really, any practice) for dealing with getting messages through outlets? I looked for ways to query the earlier parts of the path, but I didn't find any that were defined in the relevant classes.
EDIT to add more detail:
The route template for /roles/role is simply:
// file app/templates/roles/role
{{role role=#model}}
Where the Role component is in the first listing above. (I also added the role.js file contents above.) My reasoning for doing that was that by making a component I created a logical place to put the config (rather than inline helper functions) and it just gave me a sense of tidiness to have all ui elements be in components.
If a refactor can be the anchor to a good solution (essentially copying the entire Role component into the route template), however, I'll happily do it.
{{outlet}} only supports one optional string argument for a named outlet and nothing else, so you won't be able to achieve this through the use of {{outlet}}!

How to display a "not found" page with parameterised route in Durandal

I have a Durandal application, and I use router.mapUnknownRoutes to display a user-friendly error page if the URL does not correspond to a known route. This works fine -- if I go to, say /foo, and that doesn't match a route, then the module specified by mapUnknownRoutes is correctly displayed.
However I cannot find any way to display that same error page when I have a parameterised route, and the parameter does not match anything on the backend.
For example, say I have a route like people/:slug where the corresponding module's activate method looks like this:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
});
};
If I go to, say /people/foo, then the result depends on whether dataService.load('foo') returns data or an error:
If foo exists on the backend then no problem - the observables are set and the composition continues.
If foo doesn't exist, then the error is thrown (because there is no catch). This results in an unhandled error which causes the navigation to be cancelled and the router to stop working.
I know that I can return false from canActivate and the navigation will be cancelled in a cleaner way without borking the router. However this isn't what I want; I want an invalid URL to tell the user that something went wrong.
I know that I can return { redirect: 'not-found' } or something similar from canActivate. However this is terrible because it breaks the back button -- after the redirect happens, if the user presses back they go back to /people/foo which causes another error and therefore another redirect back to not-found.
I've tried a few different approaches, mostly involving adding a catch call to the promise definition:
this.activate = function (slug) {
dataService.load(slug).then(function () {
// ... set observables used by view ...
}).catch(function (err) {
// ... do something to indicate the error ...
});
};
Can the activate (or canActivate) notify the router that the route is in fact invalid, just as though it never matched in the first place?
Can the activate (or canActivate) issue a rewrite (as opposed to a redirect) so that the router will display a different module without changing the URL?
Can I directly compose some other module in place of the current module (and cancel the current module's composition)?
I've also tried an empty catch block, which swallows the error (and I can add a toast here to notify the user, which is better than nothing). However this causes a lot of binding errors because the observables expected by the view are never set. Potentially I can wrap the whole view in an if binding to prevent the errors, but this results in a blank page rather than an error message; or I have to put the error message into every single view that might fail to retrieve its data. Either way this is view pollution and not DRY because I should write the "not found" error message only once.
I just want an invalid URL (specifically a URL that matches a route but contains an invalid parameter value) to display a page that says "page not found". Surely this is something that other people want as well? Is there any way to achieve this?
I think you should be able to use the following from the activate or canActivate method.
router.navigate('not-found', {replace: true});
It turns out that Nathan's answer, while not quite right, has put me on the right track. What I have done seems a bit hacky but it does work.
There are two options that can be passed to router.navigate() - replace and trigger. Passing replace (which defaults to false) toggles between the history plugin using pushState and replaceState (or simulating the same using hash change events). Passing trigger (which defaults to true) toggles between actually loading the view (and changing the URL) vs only changing the URL in the address bar. This looks like what I want, only the wrong way around - I want to load a different view without changing the URL.
(There is some information about this in the docs, but it is not very thorough: http://durandaljs.com/documentation/Using-The-Router.html)
My solution is to navigate to the not-found module and activate it, then navigate back to the original URL without triggering activation.
So in my module that does the database lookup, in its activate, if the record is not found I call:
router.navigate('not-found?realUrl=' + document.location.pathname + document.location.hash, { replace: true, trigger: true });
(I realise the trigger: true is redundant but it makes it explicit).
Then in the not-found module, it has an activate that looks like:
if (params.realUrl) {
router.navigate(params.realUrl, { replace: true, trigger: false });
}
What the user sees is, it redirects to not-found?realUrl=people/joe and then immediately the URL changes back to people/joe while the not-found module is still displayed. Because these are both replace style navigations, if the user navigates back, they go to the previous entry, which is the page they came from before clicking the broken link (i.e. what the back button is supposed to do).
Like I said, this seems hacky and I don't like the URL flicker, but it seems like the best I can do, and most people won't notice the address bar.
Working repo that demonstrates this solution

Use of Meteor-ui-progress-circle (accessing to Template variables created in the HTML)

It may be a very dumb question... I am using Meteor-ui-progress-circle and I want redrawing the template when the percentage (wich is store in a reactive collection Progress) is changed (currently, when I click on a "play" button).
I think I have to use Blaze.render but I don't really understand how it work.
Here a part of my main template (in Jade) :
div.panel-body
div.col-md-9.col-sm-8
p Lorem ipsum...
div.col-md-3.col-sm-4#progress-circle
+progressCircle progress="0" radius="100" class="green"
And my JavaScript :
Template.controlBar.events(
{
"click .play-button": function ()
{
var tmp = Progress.findOne({});
if (!tmp)
{
Meteor.call('createProgress');
tmp = Progress.findOne({});
}
var val = tmp.progressValue;
val += 10;
if (val > 100)
return;
Meteor.call('updateProgess', tmp._id, val);
Template.progressCircle.progress = tmp.progressValue;
Blaze.render(Template.progressCircle, $("#progress-circle")[0]);
},
Doing this... I have several template that are displaying each time I click on the play button. I don't understand how to specify that I don't want a new template but just re-render the one I already have.
Not sure I quite understand your question, but I'll try to help by giving my best understanding of templating and how I have come to use them. If someone sees any incorrect information here, please speak up so I can get a better understanding myself and correct this answer.
First, the Template.XXX.events handlers. In your event handler, you are using a function with no arguments. You can actually accept 2 arguments for these event handler functions: the event and the template. So, you can do something like thus:
Template.controlBar.events({
'click .play_button': function(event, tmpl) {
tmpl.$('div#progress-circle').doSomething();
}
});
Notice the tmpl.$() call? That says to use jQuery to find the specified selector, but ONLY in the current template. This is a wonderful way to use classes to generalize your components, but then be able to filter the selection to only those within the same template...
...Which brings me to my next bit of advice: Use child templates excessively. Any component that I can identify as an "autonomous component" on my page I will consider as a separate template. For instance, I was recently working on a custom reporting page that had a table and some D3 graphs representing some real-time data. In this report page, I had one main template defined for the "page", then each of the D3 graphs where defined as a separate template, and the table was another separate template. This allows several advantages:
Compartmentalization of the "components" of the page, allowing code reuse (I can now put the same graph on ANY page, since it's now an autonomous "component"
The advantage of using the Template.XXX.events trick above to "narrow" the scope of my element searches to elements within that template
Prevents total page refreshes as Meteor is smart enough to only refresh templates that need to be refreshed, which also speeds the responsiveness of the page itself
As a result, I try to apply my Templates liberally. In your case, it would sound to me that if I were to have multiply progress bars on the page that I might turn those into separate templates. I might even do it if I had a single progress bar if it made sense to separate it out for ease of data handling.
Finally, inter-communications between Templates. This can be tricky at times, but the best, most efficient way to do this I have found is through Session variables. The pattern I typically use is to have my data for my template be returned by a Template .helper, which does something like this:
Template.controlBar.helpers({
progressData: function() {
if (!Session.equals('playId', null)) {
return Progress.findOne({_play_id: Session.get('playId')});
}
}
});
Because Helpers are reactive, and Sessions is reactive, the template is re-rendered anytime the 'playId' is altered in the Session. The corresponding Session variable can be set from anywhere in the client code. Again, this tends to work best when you narrow the scope of your templates to the individual components. It is important to note here that the Session object in Meteor is NOT the same as "sessions" in other languages like Java and such, which typically use cookies and a session token/id. Meteor sessions work considerably different, and do not survive page reloads or closing of browsers.

Ember: transition to route passing the ID instead of obj

I have some route like /ads/:ad_id and from my controller I can do
this.transitionToRoute('ads.ad', adObj)
How can I do the similar thing but this time passing the ID instead of the loaded object?
O course I understand that I can load an obj by ID first, but Ember's power is in doing lost of boilerplate for us.
Update: So, as by default Ember serializes the model to URL params by doing like
mode_instance -> { model_name_id: model_instance.id }
My trivial attempt was doing
this.transitionToRoute('ads.ad', { id: adObjId })
But when passed a model object Ember does not re-fetch it.
So, the question: I have a route (single ad view) that depends on ad ID. I have this ID as number. I want to transition to this route like if I simply entered the url /ads/ID
This can be accomplish by passing the URL to transitionTo. For example,
this.transitionToRoute('/ads/' + adObjId)
The model() method will be called with the params from the URL.
Here is a use case for this:
Transitioning from a list view to a detail view. In the list view, the records don't have any relations tied to them, but the detailed view should side-load relational data. For this reason, the models are not 1:1 between the list view and detailed view. There should be a way to transition simply using the id.
Cp
What's your use case for this? Most cases when you would want to specify an object by id, you already have the object to pass to transitionTo. Can you provide more context about what you're trying to do? I think you can probably accomplish it without using the object id.
In any case, I don't think there's a good way to do this, because when you transition via transitionTo(someRoute, someModel), the route's model hook is not called, and the model you pass in (someModel) is supplied directly to the other route hooks (setupController(controller, model), redirect(model), renderTemplate(controller, model)).
See Ember.JS Route api -- model method for more details.

EmberJS: How to transition to a router from a controller's action

I have an action:
{{action create target="controller"}}
which I have targeted to the bound controller (rather than the router) like this:
App.AddBoardController = Ember.Controller.extend
create: ->
App.store.createRecord App.Board, {title: #get "boardName"}
App.store.commit()
//TODO: Redirect to route
How do I redirect back to a route from the controller action?
Use transitionToRoute('route') to redirect inside an Ember controller action:
App.AddBoardController = Ember.Controller.extend({
create: function(){
...
//TODO: Redirect to route
this.transitionToRoute('route_name');
}
...
In fact, this is not Ember idiomatic. From what I know, and what I have learnt from Tom Dale himself, here are some remarks about that code:
First, you should not transitionTo from elsewhere than inside the router: by doing so, you are exposing yourself to serious issues as you don't know in which state is the router, so to keep stuff running, you will quickly have to degrade your design, and by the way the overall quality of you code, and finally the stability of your app,
Second, the action content you are showing should be located inside the router to avoid undesired context execution. The router is indeed a way to enforce a coherent behavior for the whole app, with actions being processed only in certain states. While you are putting the actions implementation into Controllers, those actions can be called at anytime, any including wrong...
Finally, Ember's controllers are not aimed to contain behavior as they rather are value-added wrappers, holding mainly computed properties. If you nevertheless want to factorize primitives, maybe the model can be a good place, or a third party context, but certainly not the Controller.
You should definitely put the action inside the router, and transitionTo accordingly.
Hope this will help.
UPDATE
First example (close to your sample)
In the appropriated route:
saveAndReturnSomewhere: function (router, event) {
var store = router.get('store'),
boardName = event.context; // you pass the (data|data container) here. In the view: {{action saveAndReturnSomewhere context="..."}}
store.createRecord(App.Board, {
title: boardName
});
store.commit();
router.transitionTo('somewhere');
}
Refactored example
I would recommend having the following routes:
show: displays an existing item,
edit: proposes to input item's fields
Into the enclosing route, following event handlers:
createItem: create a new record and transitionTo edit route, e.g
editItem: transitionTo edit route
Into the edit route, following event handlers:
saveItem: which will commit store and transitionTo show route, e.g
EDIT: Keep reading, Mike's answer discusses some of the problems with this approach.
You can just call transitionTo directly on the router. If you are using defaults this looks like App.router.transitionTo('route', context).

Categories

Resources