Ember JS: Can I make my UI state persistent? - ember.js

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.

Related

ember 2.17: calling a jquery document.ready() function

I am really new to Ember, which I am asked to do and well, love to learn. Basically, the current project uses Gentelella Admin Dashboard. I being trying to get the dashboard to load properly but failed.
After I login, I get redirected to /dashboard/ route, which basically loads the main dashboard interface. Now the problem is I can't click-expand the menus on the sidebar nor toggle the sidebar menu. And the main page is not extended to fill the space, as in our current application.
I know the function init_start() takes care of the resize and the click, which is already added to vendor.js from custom.js but I can't seem to call the function from ember at all.
My latest attempt was using mixins but it failed too:
import Ember from 'ember';
export default Ember.Mixin.create({
activate: function() {
this._super();
init_sidebar();
}
});
then from dashboard.js route:
import HandleTempLoadMixin from '../mixins/handle-temp-load';
export default Route.extend(AuthenticatedRouteMixin,HandleTempLoadMixin, {
});
but still the function is not executed.
I have read that it is best to avoid working with jquery inside ember in SO but I have pretty much many JQuery functions that I cant transfer right now (nor sure why exactly since it says somewhere in the documentation jquery is built into ember itself).
Anyway, what is the best way to initailize the dashboard interface?
From my understanding you have some jQuery stuff that you would like to utilise. I suggest looking into Component's didInsertElement hook and triggering your custom code from there.
You can find more details in here https://guides.emberjs.com/v2.17.0/components/the-component-lifecycle/#toc_integrating-with-third-party-libraries-with-code-didinsertelement-code
In general, try avoid working with view related stuff in Routes. Ember's power comes from strong conventions. Learning where to place your code is crucial.

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.

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.

Does ember require routes?

I'm writing a feed reader plugin for wordpress.
Feeds have entries. Click on a feed, show the entries from that feed.
I have that working in older versions of ember, but when I try to upgrade to the release candidates it seems like we have to have a router. Defining routes etc is turning into a major headache and I'm wondering if I can just use the databinding that I came to ember for.
Is there a way to just use the databinding to controllers and models without having to go through the whole router business?
Is there a way to just use the databinding to controllers and models without having to go through the whole router business?
Yes it's possible to use ember without the router. It will still be there in background but won't cause any trouble. Just set location: 'none' and then customize App.ApplicationController, App.ApplicationView and application.hbs as necessary. You may find it is still useful to customize the App.ApplicationRoute as well.
App.Router.reopen({
location: 'none'
});
FWIW #commadelimited is right, checkout the Peepcode video and consider taking advantage of the router.
Ember's routes have smart defaults so if you're app is pretty basic, you may not need to touch the routes.
location:none simply makes it so you're url doesn't show state changes, which can be useful
If you're app doesnt have any use for routes(maybe it's super small) you can simply put everything into a controller on your initial state/route and not worry about it.
But even if you have a simple setup of a few states:
App.Router.map(function() {
this.resource("index", { path: "/" }, function(){
this.route("stuff", { path: "/stuff" });
this.route("otherstuff", { path: "/otherstuff" });
});
});
the routes default action is take care of the magic behind that, so you shouldnt need to do anything :)