I have a link to User displayed from various screens(From User List, User Groups etc.). When the link is clicked, User is presented to edit. When cancel button is pressed in the edit form, I would like to transition to previous screen userlist/group. How is this generally achieved in Emberjs.
Thanks,
Murali
You need nothing more than
history.back()
One of the main design objectives of Ember, and indeed most OPA frameworks, is to work harmoniously with the browser's history stack so that back "just works".
So you don't need to maintain your own mini-history stack, or global variables, or transition hooks.
You can put a back action in your application router to which actions will bubble up from everywhere, so you can simply say {{action 'back'}} in any template with no further ado.
Here's my solution, which is very simple and high performance.
// file:app/routers/application.js
import Ember from 'ember';
export default Ember.Route.extend({
transitionHistory: [],
transitioningToBack: false,
actions: {
// Note that an action, like 'back', may be called from any child! Like back below, for example.
willTransition: function(transition) {
if (!this.get('transitioningToBack')) {
this.get('transitionHistory').push(window.location.pathname);
}
this.set('transitioningToBack', false);
},
back: function() {
var last = this.get('transitionHistory').pop();
last = last ? last : '/dash';
this.set('transitioningToBack', true);
this.transitionTo(last);
}
}
});
There is probably a way to DRY(don't repeat yourself) this up, but one way of doing it is to have 2 actions: willTransition which Ember already gives you and goBack which you define yourself. Then, there is a "global" lastRoute variable that you keep track of as follows:
App.OneRoute = Ember.Route.extend({
actions: {
willTransition: function(transition){
this.controllerFor('application').set('lastRoute', 'one');
},
goBack: function(){
var appController = this.controllerFor('application');
this.transitionTo(appController.get('lastRoute'));
}
}
});
And your template would look as follows:
<script type="text/x-handlebars" id='one'>
<h2>One</h2>
<div><a href='#' {{ action 'goBack' }}>Back</a></div>
</script>
Working example here
Related
I'm building an Ember app that needs to fade out a background DIV when a form input becomes focused.
I have defined actions on my Application route, and set a property in my model (because I'm trying to do this without a controller, like the Ember 2.0 way). I'm trying to do Action Up, Data Down. I have the actions going up to the Application route, but the data just isn't making it back down to the component.
I have the actions bubbling up to the application route just fine, but when I update the property this.controllerFor('application').set('showBackground', true); it never makes it back down to the component.
I have this fading out background image on every route of my site, so moving all the actions to each route seems like a lot of code duplication.
What am I doing wrong?
// Application route.js
var ApplicationRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
showBackground: false
});
},
setupController: function(controller, models) {
controller.setProperties(models);
},
action: {
showBackground: function(){
// This runs fine
this.controllerFor('application').set('showBackground', true);
},
hideBackground: function(){
// This runs fine
this.controllerFor('application').set('showBackground', false);
}
}
});
// Background component.js
var BackgroundImage = Ember.Component.extend({
// This never runs for some reason!?
controlImage: function(){
if( this.get('showBackground') ) {
// Open menu!
console.log('show image');
} else {
// Close menu!
console.log('hide image');
}
}.observes('showBackground')
});
// Login template.hbs
{{background-image showBackground=showBackground}}
Is this the correct way to replace "properties" and controllers with routes? All the "move to Ember 2.0" advice I can find doesn't mention how to replace high level properties.
EDIT
I created a JSbin, but I'm not sure if it's setup correctly for the 2.0 style (no controllers), as the import/export (ES6?) stuff doesn't work on JSbin.
http://emberjs.jsbin.com/wunalohona/1/edit?html,js,console,output
I couldn't actually get any of the actions to bubble correctly.
Here is the working demo.
There were multiple issues in the jsbin you provided. Here are some of the issue I fixed.
You need to specify the routes, components on the App namespace or Ember will not be able to find it. The resolver used in ember-cli is custom.
var ApplicationRoute = Ember.Route.extend({ should be
App.ApplicationRoute = Ember.Route.extend({
var BackgroundImage = Ember.Component.extend({ should be
App.BackgroundImageComponent = Em.Component.extend({
More about it here.
You don't need to specify the setupController method in the route. By default the model returned from the model hook is set to the model property of the controller.
https://github.com/emberjs/ember.js/blob/v1.11.1/packages/ember-routing/lib/system/route.js#L1543
The proxying behavior of ObjectController along with ObjectController has been deprecated.
Now refer model property by adding model.+modelPropertyName
You can read more about this in the deprecation page for v1.11
action in the ApplicationRoute should be actions
I would like to devote a single route of my EmberJS app to being a multi-step form. This is the only time I want my URL to remain unchanged, so location: 'none' is not an option (as far as I can tell). I have controllers at other routes that are tightly integrated with the URL as they should be.
But at this single, unchanging URL I would like to accomplish the following:
User answers some questions.
User clicks a button and old questions are replaced with new questions.
Rinse and repeat until the last "page" where all the data is finally .save()-ed on submit.
The way handlebars works is really throwing me for a loop on this.
I have been pouring over the documentation, but can't really find an example. I have a feeling that it is a case where I just don't know what I don't know yet. So if someone could point me in the right direction, hopefully that's all I'd need.
I started with MartinElvar's excellent answer, but wound up in a different place since I needed to validate a form on each page of the wizard. By making each page of the wizard it's own component, you can easily constrain the validation of each page.
Start with a list of steps on your controller:
// app/controllers/wizard.js
export default Ember.Controller.extend({
steps: ['stepOne', 'stepTwo', 'stepThree'],
currentStep: undefined
});
Then make sure that whenever your controller is entered, bounce the user to the first step:
// app/routes/wizard.js
export default Ember.Route.extend({
setupController (controller, model) {
controller.set('currentStep', controller.get('steps').get('firstObject');
this._super(controller, model);
}
});
Now you can go back to the controller and add some more generic next/back/cancel steps:
// app/controller/wizard.js
export default Ember.Controller.extend({
steps: ['step-one', 'step-two', 'step-three'],
currentStep: undefined,
actions: {
next () {
let steps = this.get('steps'),
index = steps.indexOf(this.get('currentStep'));
this.set('currentStep', steps[index + 1]);
},
back () {
let steps = this.get('steps'),
index = steps.indexOf(this.get('currentStep'));
this.set('currentStep', steps.get(index - 1));
},
cancel () {
this.transitionToRoute('somewhere-else');
},
finish () {
this.transitionToRoute('wizard-finished');
}
}
});
Now define a component for page of your wizard. The trick here is to define each component with the same name as each step listed in the controller. (This allows us to use a component helper later on.) This part is what allows you to perform form validation on each page of the wizard. For example, using ember-cli-simple-validation:
// app/components/step-one.js
import {ValidationMixin, validate} from 'ember-cli-simple-validation/mixins/validate';
export default Ember.Component.extend(ValidationMixin, {
...
thingValidation: validate('model.thing'),
actions: {
next () {
this.set('submitted', true);
if (this.get('valid')) {
this.sendAction('next');
}
},
cancel () {
this.sendAction('cancel');
}
}
});
And finally, the route's template becomes straight forward:
// app/templates/wizard.hbs
{{component currentStep model=model
next="next" back="back" cancel="cancel" finish="finish"}}
Each component gets a reference to the controller's model and adds the required data at step. This approach has turned out to be pretty flexible for me: it allows you to do whatever crazy things are necessary at each stage of the wizard (such as interacting with a piece of hardware and waiting for it to respond).
You could achieve this with some actions, and some values that defines the state of the form.
Your controller could have some states properties like the following.
// Step one is default.
stepOne: true,
stepTwo: false,
stepThree: false,
How you want to transition from step to step, is a matter of use case, but you would end of changing the step properties, like so.
actions: {
toStepTwo: function() {
this.set('stepOne', false)
this.set('stepOne', true)
},
// But you could put this with some other functionality, say when the user answer a question.
answerQuestion: function() {
// Run some question code.
// Go to next step.
this.set('stepOne', false)
this.set('stepOne', true)
},
}
In your template you can just encapsulate your content using the if helper.
{{#if stepOne}}
Step one
{{/if}
{{#if stepTwo}}
This is step two
{{/if}}
So the reason for create 3 step properties here, instead of
currentStep: 1,
Is for the sake of handlebars, currently you can't match a current step like so.
{{#if currentStep == 1}}
Well unless you create a handlebars block helper.
I have the following setup:
A ListRoute which has an action doNext.
An ItemRoute (Detail) which also has an event doNext.
App.Router.map(function () {
this.resource('list', function() {
this.route('item');
})
});
App.ListRoute = Ember.Route.extend({
actions:{
doNext:function() {
alert("doNext from List Route!");
}
}
})
App.ListItemRoute = Ember.Route.extend({
actions:{
doNext:function() {
alert("doNext from List Item Route");
}
}
})
When clicking on an {{action doNext}} inside the List it fires the correct action in the List Route.
But when I click on the same link, after I have transitioned int the item, the action of the List Item Route gets fired. I would have expected, that the action would still get sent to the ListRoute.
Is this by design? And is there a way to force my expected behavior?
I've created a fiddle which where you can see this:
http://jsfiddle.net/AyKarsi/HA93a/4/
it's how actions occur, it doesn't matter which template you click the action from, just what route you're in, it starts at the very top, then if it doesn't find it, it goes up the chain. This allows you to send the same action from random places in your application and have them propagate up, or be overridden by the highest/deepest available route.
Your jsfiddle's template name was wrong btw, it should be resource/route
http://jsfiddle.net/HA93a/6/
the best way around it would be to use different names in your actions if you don't want the same action name to have that issue. Honestly it's a little weird in my opinion, and maybe someone should complain, but it was by design.
I currently have my routes defined like this:
App.Router.map(function() {
this.resource('players', { path: ':page_id' }, function() {
this.resource('player', { path: ':player_id' });
});
});
The idea is that I have a list of player names on the left. The player names displayed depend on the page_id. On the right, I display a single player and all its information based on the player_id. The thing is, both are independent, meaning that I could be on the third player page, while displaying the first player in the list, or no player at all.
What I keep trying to do is something like this, which is a method in the PlayersController that gets called when I click to go to the next player page:
doTransition: function() {
var players = App.Player.findAllForPage(this.playersPerPage, this.currentOffset);
players.reopen({
id: this.currentOffset
});
var playerController = this.get('controllers.player');
var currentPlayer = playerController.getWithDefault('content');
if (currentPlayer) {
this.transitionToRoute('player', players, currentPlayer);
} else {
this.transitionToRoute('players', players);
}
}
What I'm trying to do: When I click to go to the next player page, transition to the PlayersRoute if there is no player currently being displayed, otherwise transition to the PlayerRoute so that the player is still displayed when the transitioning is done.
The problem: sometimes the currentPlayer variable is not always null, even if no player is currently being displayed. Is there a way to get around this, perhaps by getting the current route from somewhere?
Given that you say the two sections (list of players based on page_id, and player information based on player_id) are completely independent, it seems to me like you wouldn't nest the routes, and instead, have two named outlets (call them left and right, or page and player, etc) that you selectively render into.
Router:
App.Router.map(function() {
this.resource('page', { path: ':page_id' });
this.resource('player', { path: ':player_id' });
});
application.hbs:
{{outlet page}}
{{outlet player}}
And then you can override your renderTemplate for your page and player routes to render into the appropriate template. To clarify, page route would display what you currently have as the players route (it's dependant on the page_id, but the page has many players, so the route displays the players based on the page), and player route would display the player information based on the player_id.
(As a side note, I don't think you can nest resources the way you do right now with putting resource player under resource players -- I think only routes can be nested.)
EDIT: Using single route with multiple dynamic segments
I think your suggestion could work. From the linked example it seems like you need to create the "parent" resources (not nested routes, but having more general paths, like /page/ and /page/:page_id/player/:player_id) anyway. You can then set up your models individually via the model in the appropriate route, and just provide a serialize hook for the double dynamic segment route:
serialize: function(model) {
return {
page_id : this.modelFor('page').get('id')
player_id : this.modelFor('player').get('id')
};
}
Note we're not relying on the model object passed in because you've said that the page and player panels can be completely independent, so we use modelFor instead.
I think you can also handle your logic about default page to render / default player to render if none are suggested here via the redirect hook.
Finally, you would override renderTemplate in your PagePlayer route to actually do the rendering:
renderTemplate: function(model, controller) {
this.render("page", { into: "page" });
this.render("player", { into: "player"});
}
I think you have to be careful to NOT render the templates in the more general routes because if you if you move from /page/1/player/2 to /page/1/player/3, the page route is NOT re-entered.
While Sherwin's answer gave me a good idea of where I was going, I just wanted to put a complete example and give a general idea of what I ended up implementing. This could be of help for future reference.
I'm going to make it simple by having the models be a simple int, that way we have a direct translation from url to model and vice versa.
Templates:
<script type="text/x-handlebars">
{{outlet a}}
{{outlet b}}
</script>
<script type="text/x-handlebars" id="a">
{{model}}
</script>
<script type="text/x-handlebars" id="b">
{{model}}
</script>
Application:
App = Ember.Application.create();
App.Router.map(function() {
// This route has 2 dynamic segments
this.resource("ab", { path: "/:a/:b" });
});
App.IndexRoute = Ember.Route.extend({
redirect: function() {
// At the entry point, encapsulate the 2 models in the context object,
// and transition to the route with dual dynamic segments
this.transitionTo('ab', {a: 3, b:4});
}
});
App.AbRoute = Ember.Route.extend({
model: function(params) {
// The model is {a, b} directly
return params;
},
renderTemplate: function(){
// Render in the named outlet using the right controller
this.render('a', {into: 'application', outlet: 'a', controller: 'a'});
this.render('b', {into: 'application', outlet: 'b', controller: 'b'});
},
serialize: function(model) {
return {
a: model.a,
b: model.b
};
},
setupController: function(controller, model) {
// Setup each controller with its own model
this.controllerFor('a').set('model', model.a);
this.controllerFor('b').set('model', model.b);
}
});
Additional note:
It would've been possible to have a single 'ab' template rendering {{model.a}} and {{model.b}} from the AbController, but I thought having separate controllers and templates was cleaner, and that it enabled reusability. Additionally, one of the controllers could've been an ArrayController and it would've work perfectly fine.
JS Bin of the example
I can't figure out the correct way to handle modal states/views with the new Ember router. More generally, how do you handle states that you can enter and exit without affecting the "main" state (the URL)?
For example, a "New Message" button that is always available regardless of the current leaf state. Clicking "New Message" should open the new message modal over the current view, without affecting the URL.
Currently, I'm using an approach like this:
Routes:
App.Router.map(function() {
this.route('inbox');
this.route('archive');
});
App.IndexRoute = Em.Route.extend({
...
events: {
newMessage: function() {
this.render('new_message', { into: 'application', outlet: 'modalView' });
},
// Clicking 'Save' or 'Cancel' in the new message modal triggers this event to remove the view:
hideModal: function() {
// BAD - using private API
this.router._lookupActiveView('application').disconnectOutlet('modalView');
}
}
});
App.InboxRoute = Em.Route.extend({
...
renderTemplate: function(controller, model) {
// BAD - need to specify the application template, instead of using default implementation
this.render('inbox', { into: 'application' });
}
});
App.ArchiveRoute = ... // basically the same as InboxRoute
application.handlebars:
<button {{action newMessage}}>New Message</button>
{{outlet}}
{{outlet modalView}}
I've obviously left out some code for brevity.
This approach 'works' but has the two problems identified above:
I'm using a private API to remove the modal view in the hideModal event handler.
I need to specify the application template in all of my subroutes, because if I don't, the default implementation of renderTemplate will attempt to render into the modal's template instead of into application if you open the modal, close it, and then navigate between the inbox and archive states (because the modal's template has become the lastRenderedTemplate for the IndexRoute).
Obviously, neither of these problems are dealbreakers but it would be nice to know if there is a better approach that I'm missing or if this is just a gap in the current router API.
We do kind of the same thing but without accessing the private API.
I don't know if our solution is a best practice, but it works.
In the events of our RootRoute I have an event (same as your newMessage), where we create the view we need to render, and then append it.
events: {
showNewSomething: function(){
var newSomethingView = app.NewSomethingView.create({
controller: this.controllerFor('newSomething')
});
newSomethingView.append();
}
}
This appends the modal view into our app.
On cancel or save in the newSomethingView we call this.remove() to destroy the view and removing it from the app again.
Again, this doesn't feel like a best practice, but it works. Feel free to comment on this if someone have a better solution.
Don't know if you are using the Bootstrap Modal script or which one, but if you are, this question has a proposed solution. Haven't figured out all the pieces myself yet, but is looking for a similar type of solution myself to be able to use Colorbox in an "Ember best practices"-compliant way.