Route that observes property on component that links to it - ember.js

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.

Related

What is the best way for all instances of an Ember component to share a variable?

I have an Ember component which is essentially a panel. There can be multiple instances of this panel on a page, but only one can be "active" at any given time. Each instance of the component must be aware if any of the other panels become "active" so they can remove their "active" state. I would really rather not move the JavaScript to make this happen to a parent component. Instead, I would like to keep it within this component. In Angular, I used to use a static variable to do this. What is best way to do this in Ember?
I would really rather not move the JavaScript to make this happen to a
parent component
Do you want to avoid having the parent component dealing with anything related to panel "activity"? If so, why?* If not:
Ember automatically gives each component's tag (unless it's a tagless component) an id that is accessible from the js code as elementId. You could create a property activePanelId on the parent component and pass it to all panels: {{pa-nel activePanelId=activePanelId}} and then check in each panel
{{#if (eq elementId activePanelId)}}
{{!whatever is different on the active panel}}
{{/if}}
or use it in the js code:
isActive: Ember.computed('activePanelId', function() {
return this.get('activePanelId')===this.get('elementId');
},
If the panel becomes active by an action related to itself (e.g. clicking on it), just set activePanelId to the elementId in the respective action - since the property activePanelId exists only once on the parent component, all other panels to which it is passed will take note.
If using the elementId feels to hacky, you might as well give each panel a distinct name and store the activePanelName in the calling component.
*If you really do not want the property in the parent component, you could move it to a service that you then inject into the panel components, but I cannot yet imagine a good reason for preferring that.
#arne.b 's anwser sums it pretty well, the best way of handling component's state with common parent data, but if you are dead serious about not using a parent component there is kind of way to get it done.
Example:
import Ember from 'ember';
export default Ember.Component.extend({
sharedObject: {},
sharedArray: [],
});
In the above component you can use sharedObject or sharedArray to exchange state with multiple instances of the component. Any changes in the object or array will be reflected to all the instances of the same component.
A sample Ember twiddle.

Hiding parent view in Master/Detail pattern

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.

Concurrent states with Ember.router

I'd like to implement this statechart in Ember.js v1.5.x using Ember.Router, but I have problems with the concurrency and history mechanism.
Basically when I activate the Summary route/state I'd like to transition to No changes made and Transient state in the same time. How can I achieve this?
P.S. I know that e.g. stativus have these capabilities but don't know how to use it with Ember.js routing. An example would bee good.
(image source: Ian Horrocks: Constructing the User Interface With Statecharts p.153).
:)
Yeah statecharts are lovely and all, but Ember actually affords sub-states through computed properties.
I'm not overly familiar with state charts, and I'd really need to consume the resources (horrocks) you mentioned here (https://github.com/emberjs/ember.js/issues/4767#issuecomment-41458710) before I'd be fully conversant in the nomenclature of that particular example (which I can do if you'd like).
To that end, and having said that, please take my answer with a grain of salt, because I may not fully understand the context. I just hope to help.
So in Ember you have routes. Those routes explain the interface of your application. These will effectively be your states. Routes are not your actions, or events. They provide a URL for your app to present itself to the world.
So, state A seems to be presenting the Students. You have two sub-states in there... 0 students and >0 students. You would handle these with the same Route (call it StudentsRoute), because they're both about the same set of data, just different substates of it. The route would have a path called /students probably. At that point, you'd have a controller gets fed a model by the router (the list of students), so to that end, this controller would be an extension of Em.ArrayController.
This array controller (auto-named StudentsController, extends Em.ArrayController) automatically has a 'model', and that model, once resolved, is the students "array".
In StudentsController, you could easily have a computed property called zeroCount which represents the state of zero or not about the model. Computed properties automatically stay up to date. That'd be defined like this:
App.StudentsController = Em.ArrayController.extend({
zeroCount: function() {
// zeroCount is true if zero, otherwise false
return this.get('model.length') == 0;
}.property('model.length');
});
In your students template, you could conditionally render one of two sub-templates depending on this zeroCount state... you'd do that like this:
{{#if zeroCount}}
{{partial "noStudents"}}
{{else}}
{{partial "someStudents"}}
{{/if}}
Mind you, for this example, that'd be somewhat overkill... you probably don't need to render other templates (partials) like that.. there's an easier simpler way to do it because this is a common pattern in ember (rendering a list, and optionally rendering something else if there are no items in it, without needing the zeroCount property).
{{#each model}}
<p>This renders against each student... <br>
so if your students have a property called name, <br>
then you could just write {{name}} and it'd render the
students name</p>
{{else}}
<p>This renders when there are no students</p>
{{/each}}
You'd put a delete link on each of those items... and the live bound properties handle all the states for you... (thus, when model has zero items in it, the template goes into the else block of the each... otherwise it goes into the main block).
The delete action, handled by something like Delete inside your #each model template (handlebars) directive goes to the controller and looks for an action inside of it called, unsurprisingly, delete... and that'd look like this:
App.StudentsController = Em.ArrayController.extend({
actions: {
delete: function(itemToDelete) {
itemToDelete.delete();
// assuming the model "class" understands delete
}
}
});
The edit state would have its own route... possibly a nested route on the students, called edit, possibly not depending on if you wanted the list to appear on the screen while the edit page appears...
The "changes made" state is effectively handled not on the route, but on the model... as it should be... the model is responsible for persisting the object graph, or telling the view and controller whether or not the model has changed (Ember Data, for example, afford isDirty as a state on each model instance that can tell you whether it has changed or not)...
Hopefully this whets your appetite. I recommend going through some of the examples on the Ember site... they really do help, and following the Ember TODOMVC app if you haven't checked that out...
Ember thrives on these kind of flow-based state driven UIs... check out Tom and Yehuda's keynote at confreaks if you haven't already... they talk about flows in exactly the same way you're talking about these states and sub-states.
Hope that helps.

Primitive event delegation in a view - what's the Ember way?

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

How can we load multiple instances into a template?

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 :-)