Hiding parent view in Master/Detail pattern - ember.js

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.

Related

Ember: Re-render a route's template when the model changes

I've noticed that a route doesn't re-render when the model changes (i.e. transitionTo the same route with a different model). I have some jQuery plugins set up on a particular page and I need them to re-render when the model changes, so it appears as a fresh page.
Is there a way to do this? Perhaps by observing the model's ID and firing a re-render of the route somehow?
Thanks in advance
I have an ember twiddle that, I believe, does what you're looking for, but first I would like to argue there are no straightforward ways to do what you're asking because it is the opposite of what an SPA is designed to do.
Data binding (without refreshing the view) is typically a boon of an SPA, and the SPA works hard to avoid brute force reloading/refreshing/rerendering the view at all costs. It took me a while to find a solution to your question as it is stated because it seems to go against Ember design principles. Even hooks like route refresh() are meant to update the model and bound data, not reload the template.
Although other people have asked the same question before, it seems that most answers guide users towards not refreshing the whole view. More often than not, the ideal of refreshing the view is an incorrect assumption.
Following previous examples, I would suggest that your goal shouldn't be to refresh the template completely, but rather, figure out how you can make your jQuery plugin better fit in to a Single Page App/client-side JS friendly design and have it reload as a natural part of the route lifecycle.
For instance, maybe the plugin can be reloaded/reset/re-run in afterModel() or somewhere similar.
That said, I was able to accomplish what you asked for using (in my opinion, a hack) Ember.run.later() so that I could invalidate an if block and force the content to rerender. Note, this is typically not what users want since (aside from design principle reasons) it causes UI flicker.
I have a component like so.
/* will-rerender.hbs */
{{#if show}}
{{yield}}
{{/if}}
And it has a JS file like so.
/* will-rerender.js */
import Ember from 'ember';
export default Ember.Component.extend({
show: false,
didReceiveAttrs() {
this._super(...arguments);
/*
Ugly hack, but we need to reset `show` in
a separate run loop in order to force the view
to rerender.
*/
this.set('show', false);
Ember.run.later(() => {
this.set('show', true);
});
}
});
You can invoke it like this...
/* your template */
{{#will-rerender cacheKey=model.id}}
... your content to rerender ...
{{/will-rerender}}
Whenever the model.id changes the component will invoke didReceiveAttrs() causing show to invalidate and the view to refresh.
As an aside, I think the behavior of switching between models would be much more natural with {{link-to}} rather than calling transitionTo yourself.

Route that observes property on component that links to it

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.

Ember JS: Can I make my UI state persistent?

I have off canvas navigation on the left and right of my ember application.
The state is controlled by a couple simple properties triggered by actions.
Template:
<a {{action 'leftToggle'}} class="left-blade">Debtor List</a>
<a {{action 'rightToggle'}} class="right-blade">Acivity</a>
Controller:
export default Ember.Controller.extend({
isLeft: false,
isRight: false,
actions: {
leftToggle: function() {
this.toggleProperty('isLeft');
},
rightToggle: function() {
this.toggleProperty('isRight');
}
}
});
I would like these properties to persist across page reloads, or if a user navigates away and then comes back to the page.
I am not sure whether to store the properties in a model? or use localStorage, or what? It would seem like using a model just to store a simple thing like that is overkill, or if there is a better way? I am still learning ember, so I would like to learn a good habit.
This is mostly out of the scope of Ember.
Persisting data happens in a few places, localStorage, sessionStorage, cookie, url, or some sort of record persisted server side and fetched/updated client side.
This is really a pick your poison, you could attempt to use the Ember's new query-params feature and tack the state onto the url, which would involve Ember the most, but isn't necessarily the best option. Local storage seems just as easy to me.

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