Suppose we have the following DOM elements:
<ul>
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ul>
Since the beginning we all know that this approach isn't optimal:
$('li').on('click', function() {…});
instead we tend to write it this way:
$('ul').on('click', 'li', function() {…});
But what about Ember? In Ember a view represents a single DOM element. If I were to write this:
<ul>
{{#each people}}
{{#view App.PersonEntryView}}{{name}}{{/view}}
{{/each}}
</ul>
and the PersonEntryView implemented the click handler, we would end up with the first jQuery example, or did we?
I tend to strive for best solutions so I'm curious what's the so-called Ember way of event delegation?
Should I create a view for UL just for the sake of having delegation? I could, but what about more than one list in the app?
So, how do you go about this, guys?
If I were to write .... and the PersonEntryView implemented the click handler, we would end up with the first jQuery example, or did we?
Not exactly. Ember uses jQuery to delegate all events to the root element. When an event occurs it finds the nearest view that has a handler. So when you define a click() handler on PersonEntryView you get the ease-of-development of the 1st example with the performance of the 2nd.
I tend to strive for best solutions so I'm curious what's the so-called Ember way of event delegation?
The docs on this are quite good, check out the guide to
event-delegation and event-bubbling
Should I create a view for UL just for the sake of having delegation? I could, but what about more than one list in the app?
Nope. Just define a click handler on your list elements like:
App.PersonEntryView = Ember.View.extend({
click: function(event) {
//handle event here
}
});
Related
I have a list if items in an items route that uses a component event-item to display each of them. This component has two computed's on it that are setting some classes right now to show the user some info about each item...
classNameBindings: ['winning','closed'],
item: null,
winning: Ember.computed('item.item_high_bid_user_id','userService.user_id',function(){
return this.get('item.item_high_bid_user_id') == this.get('userService.user_id');
}),
closed: Ember.computed('item.item_status',function(){
return this.get('item.item_status') === 2;
})
In the component template each item in the list is wrapped in a link-to that links to the item route, which displays a single item.
In the item template and even route I would like to observe the winning and closed computed's that are on the corresponding component to show or hide some things in the item template (IE. hid the bidding section if an item is closed, etc.)
What would be the proper way to do this?
BTW I'm on Ember 2.2.0 Ember Data 2.2.0 and Ember-cli 1.13.13
If your event-item component is linking to an item route, I assume you're passing the item model into the link-to helper, which means all the attributes needed to compute these properties are still going to be available in the item controller.
// templates/whichever-template-holds-items.hbs
{{#each items as |item|}}
{{event-item model=item}}
{{/each}}
// templates/components/event-item.hbs
<div>
{{link-to 'item' model}} // pass model to item route
</div>
// controllers/item.js
import Ember from 'ember';
export default Ember.Controller.extend({
// include userService
winning: Ember.computed.equal('model.item_high_bid_user_id','userService.user_id'),
closed: Ember.computed.equal('model.item_status', 2)
});
// templates/item.hbs
{{#if winning}}
// show winning stuff
{{/if}}
{{#if closed}}
// show closed stuff
{{/if}}
Also, I noticed you had a mix of both == and === for your conditionals in the code you posted. Most of the time you will want to use ===, see this post.
Almost forgot - Ember.computed.equal
UPDATE (in response to your comment below)
There are a couple ways to alert a controller that a value in a component has changed, but neither are really conducive in your current situation.
The first way (which is ok to do) would be to follow DDAU (data down, actions up) and send an action from your component up to your controller, but this only works if the component is inside the controller's view, which is not the case for what you're doing.
The second way (which is not really ideal IMO) would be to use a service in sort of a pub/sub fashion which would allow distant component/controllers to talk to each other (you can read more about this method here). You'll probably get mixed responses as far as doing things this way since it can be kind of disruptive to the data flow of your app. But sometimes you're choices are limited.
With all this said, I would probably stick with re-computing in the controller rather than trying to send data across your app from one controller to another. In the end it will still be less code and less work for the framework. Hope this was helpful.
I am building a mobile-first app which therefore needs to be quite economic with screen real-estate and there are cases where when navigating between a resource and one of it's sub-routes I want to be able to have the subroute take the place of the {{outlet}} but at the same time remove the parent view/template's DOM elements. So if my route were set as:
Router.map(function() {
this.resource('workouts', function() {
this.route('workout', { path: '/:id' }, function() {
this.resource('exercises', function() {
this.route('new');
this.route('exercise', { path: '/:id' });
});
});
}
}
And let's say I wanted to start my browsing by looking at a specific workout ("1234") with a list of all the exercises undertook in the workout but without the details. I would navigate to:
http://best.app.ever/workouts/1234
and when I clicked on a particular exercise I'd want to see the details of that exercise ("123") at:
http://best.app.ever/workouts/1234/exercises/123
This works without issue but -- as might be expected -- the details of the exercise are inserted into the DOM at the {{outlet}} in the workout.hbs template. What I'm looking for is a graceful and easy way to replace the parent templates DOM entries (at least for smaller media types).
Extra credit solution which would allow for some sort of subtle animation between states to help the user understand the transition.
BTW, I have a working solution which I've convinced myself is more of a hack than the "right way" of doing this in Ember ... it goes like this:
In the child view (aka, exercises/exercise) I have overloaded the default View and add the following:
export default Ember.View.extend({
hideMaster: function() {
var master = this.$().parents('body').find('#master-screen');
master.hide();
}.on('didInsertElement')
});
This depends on the "master" (in this case the workout.hbs template) having a DOM element "master-screen" that encompases that part of the DOM I want to hide. This type of solution would also lend itself easily to animating states too but I suspect some smart mind out there has a more Ember-like way of doing this ... if so please speak up. :)
I would be tempted to use the exercises/index template to display the list of exercises. That way it’ll be automatically swapped out when you transition into exercises/exercise. You could then use liquid-fire to add a graceful transition.
However, that solution won’t allow you to keep the full list of exercises on-screen for desktop browsers. For that case, I’d say the right approach would be to design a component dedicated to the appropriate responsive behaviour. It’d do a similar job to the view you’ve defined, but be far more explicit when you come to look at the templates further down the road.
Dumb Example:
exercises.hbs:
{{#x-responsive-view}}
{{#x-parent-content}}
<ul>
{{#each exercise in exercises}}
<li>...</li>
{{/each}}
</ul>
{{/x-parent-content}}
{{#x-child-content}}
{{outlet}}
{{/x-child-content}}
{{/x-responsive-view}}
x-responsive-view would hide x-parent-content if we’re on mobile AND x-child-content contains something.
What is the best way of toggling the presence of some element (represented by a template) that is a direct child of <body>?
I'm talking about a modal box, notification, light-box etc. that is triggered either by some user event or by a route.
Examples: Dialog for newsletter sign-up that is shown after a user clicks a button. Overlay for content editing that is triggered by appending /edit to the item's route
Edit:
The first solution I though of was using Session to control state and then lining up all the application's modals and messages inside #if statements at the end of my main layout template like this:
<template name="layout">
<!-- yields and stuff -->
{{#if isActiveModal 'editArticle'}}{{> editArticle}}{{/if}}
{{#if ...
</template>
The problem is modularity; if a teammate is working inside some page template and needs to display a specific message or dialog, he or she has to edit the main layout to add it. I would have liked some easy way of conditionally appending a template to <body>.
Should be possible with the tools at hand, shouldn't it?
I actually use bootbox throughout several of my Meteor apps and it works great and does not interfere with any of Meteor's rendering. You can use the normal alert, confirm, prompt, as well as a custom dialog function, in a non-blocking way. There is a smart package available:
https://github.com/TimHeckel/meteor-bootboxjs
See some of my apps and smart packages for examples (grep for bootbox.):
https://github.com/mizzao/CrowdMapper/
https://github.com/HarvardEconCS/turkserver-meteor/
If you use the latest version of the new template engine, you can work with the body tag as its own special template through UI.body:
meteor update --release template-engine-preview-10.1
HTML:
<body>
{{#with alert}}
{{> modal}}
{{/with}}
</body>
<template name="modal">
<div class="modal">
X
<p>{{message}}</p>
</div>
</template>
JS:
if (Meteor.isClient) {
UI.body.alert = function() {
return Session.get("modal-alert");
};
UI.body.events({
"click .close": function() {
Session.set("modal-alert", null);
}
});
}
Since I posted the question, Blaze has become more sophisticated. Elements can now be rendered anywhere in the DOM while keeping a logical view hierarchy. See this discussion:
https://forums.meteor.com/t/most-useful-meteor-apis/1560/8
I've inserted the main outline below – the full thread also has code examples.
...
[Use] Blaze.render to output a template anywhere in the DOM – such as in a dimmable wrapper directly inside <body> – while maintaining a parent-child relationship between the view of the opening template and the view of the modal. This relationship is required for them to communicate directly.
(Bear in mind that a Session variable, among other limitations, can only hold serializable data...)
With the hierarchy intact, it's possible to set up shared reactivity between the modal and the background and call methods on the other, too. If we volunteered to use Blaze.renderWithData, then the two templates can even share the same data context!
Manually traversing the view hierarchy is pretty tedious, but with the parent() method from aldeed:template-extension, it isn't an issue.
Depending on whether one likes to use template instances as a 'view-model' or prefers to pass stuff around, reciprocal reactivity between the two templates can either be mediated by a new ReactiveVar assigned to a property on the parent template instance or view, or by a more retrieve-on-demand TemplateVar.
The entire exercise with Blaze.render can be duly incapsulated by using tricks like block helper templates and keyword arguments, so that everything is kept declarative.
We're trying to put together a portal, where a layout can have any number of core widgets in any sequence in the main layout.
To simulate this, we've got a number of outlets:
<h1>{{title}}</h1>
{{outlet pos1}}
{{outlet pos2}}
{{outlet pos3}}
{{outlet pos4}}
{{outlet pos5}}
{{outlet pos6}}
{{outlet pos7}}
{{outlet pos8}}
{{outlet pos9}}
{{outlet pos10}}
And in the router, we're attempting to load them in one by one:
connectOutlets: function(router, group) {
router.get('applicationController').connectOutlet('group', group);
router.get('groupController').connectOutlet('pos9', 'toDo', App.ToDo.find(41));
router.get('groupController').connectOutlet('pos3', 'toDo', App.ToDo.find(15));
However, when there are more than one, the final context is used. So in this example, we get two instances of the toDo object, both of which are for id #15.
Am I approaching this in the right way and is it possible to do this programatically, rather than having a fixed layout of outlets?
Thanks,
Dan
Edit: My answer is based on the assumption that this complex solution is really needed in your case. Based on your simple example one could also say, that you could use an ArrayController for all your ToDo items. But here is my try on the answer to the complex problem:
the problem are the following 2 lines:
router.get('groupController').connectOutlet('pos9', 'toDo', App.ToDo.find(41));
router.get('groupController').connectOutlet('pos3', 'toDo', App.ToDo.find(15));
What you basically do there is:
Connect the outlet with name pos9 with the Controller named 'todo'. Set the content of this controller to ToDo with Id 41.
Connect the outlet with name pos3 with the Controller named 'todo'. Set the content of this controller to ToDo with Id 15 (so you are overriding the content of the same controller).
The result is that you end up with both outlets connected to the same instance of a controller. And you the same ToDos since you have set the content property of this single instance twice. The core problem is from my point of view: EmberJS uses single instances of controllers by default.
So my solution approach would be to instantiate a new Controller for each outlet you have. Unfortunately this also requires modification to the lookup of the View. As you likely know, Controller and View are matched by name. So roughly the algorithm would be in pseudocode:
Create new instance of Controller, e.g.: var newController = App.ToDoController.create();
Inject this controller into the router with the appropriate name, e.g. router.set('todoControllerForPos9', newController);
Based on this name, you must enable Ember to find the matching view, e.g. App.set('TodoControllerForPos9View', App.ToDoView);
Finally call connectOutlet on the router, e.g.: router.get('groupController').connectOutlet('pos9', 'todoControllerForPos9', App.ToDo.find(41));
As you might guess, i ran into this problem myself. I did ask this question before and this is the solution, i came up with. I think, this is a missing feature i ember. I call it dynamic outlet names. See my original question here: How to implement a list of multiple elements, where an element can be expanded by click? (Dynamic Outlet Names?)
Have a look at the Update section and the fiddle provided there and you will recognize my pseudo code provided here.
It would be still great, if someone could have a look at my solution, because it is still hacky at the moment, but seems valueable to me. Hope this will gain some attention now with this big answer :-)
I have an ember application with nested routes but I'm having a problem getting a child view destroyed when transitioning back to the parent route. Probably easiest to look at this fiddle http://jsfiddle.net/j32yT/2/ - it starts by displaying a list of "users"; clicking Create leads to a user creation "form" with a Save button. On save, the action is fired back at the route which transitions back to its parent.
I was hoping that the create view would be destroyed when transitioning away, but it doesn't seem to. Adding in a function to handle the route exit does let me clean up, but it seems a bit messy. Have I misunderstood how the routing works?
UPDATE
By introducing an index route within my users route, I was able to achieve what I wanted - there's a new fiddle here http://jsfiddle.net/AsJca/1/ - am I on the right path here? New to this stuff, so don't yet understand what may constitute best practice!
You have an outlet for Application View and one for UsersView,
<script id="application-template" type="text/x-handlebars">
{{outlet}}
</script>
<script id="users-template" type="text/x-handlebars">
{{#each user in controller}}
{{user.name}}
{{/each}}
<button {{action create_user}}>Create</button>
{{outlet}}
</script>
When you do :
router.get('applicationController').connectOutlet('users');
the outlet of applicationView gets filled with UsersView, but see that the Users View has its own outlet.and when you do router.get('usersController').connectOutlet('createUser'); this outlet is filled with the create new form. So it resides along with the create new button that resides along with the outlet in users-template.
You can change it to,
router.get('applicationController').connectOutlet('createUser');
if you want to replace it, but again think of what you actually need, you know that.