EmberJS: reject an upcoming transition
How would I reject an upcoming transition via willTransition, but using a custom pop up or jQuery .dialog box such as this:
willTransition: function(transition) {
//Some conditional logic would go here to determine if they need to be prompted to save
$("#confirmTransition").dialog({
resizable: false,
modal: true,
draggable: false,
buttons: {
"Yes": function() {
self.transitionTo(transition.targetName);
$(this).dialog( "close" );
},
"No": function() {
transition.abort();
$(this).dialog( "close" );
}
}
});
}
Rather than using confirm() which would actually wait until the user enters something. I can use a .then, but it fires after the transition completes. Would I be better off using beforeModel on all routes to handle a conditional abort, and then rely on willTransition to provide the popup to confirm?
Updated per accepted question:
willTransition: function(transition) {
//Some conditional logic would go here to determine if they need to be prompted to save
var self = this;
if(!self._transitioning){
$("#confirmTransition").dialog({
resizable: false,
modal: true,
draggable: false,
buttons: {
"Yes": function() {
self._transitioning = true;
transition.retry().then(self._transitioning = false);
$(this).dialog( "close" );
},
"No": function() {
transition.abort();
$(this).dialog( "close" );
}
}
});
}
}
You could use the same approach to animate route transitions: You can take a look at the relatebase blog (as well as the jsbin example referred by the blog).
Essentially you handle a little state machine in the willTransition action: You abort the original transition and as soon as the user closes the dialog you retry the original transition.
Hope it helps!
Related
I have a certain route that shows a list of projects, and it gets initial data from my RESTAdapter based on who the user is.
I am now implementing a search function that will issue a new API call so the user can get records besides the default ones for them, and the response should replace the model for that route. I have all that working, but I'm not sure how to do a loading or progress indicator (as the response from the database could potentially take 5-10 seconds depending on the amount of data). I know about loading substates, but in this case I'm not transitioning between routes. I just want to have at minimum a spinner so the user knows that it's working on something.
Would anyone that's done this before be willing to share how they handled a)replacing the model with new data, and b)keeping the user informed with a spinner or something?
Form action called when user clicks the Search button
searchProjects: function() {
var query = this.get('queryString');
if (query) {
var _this = this;
var projects = this.store.find('project', {q: query});
projects.then(function(){
_this.set('model', projects);
});
}
}
a) replacing the model with new data
You don't need to do anything. If you sideload records properly from the backend, Ember will automatically update them on the frontend.
b) keeping the user informed with a spinner or something
The loading substate is an eager transition. Ember also supports lazy transitions via the loading event.
You can use that event in order to display the spinner.
Here's an example from the docs:
App.ApplicationRoute = Ember.Route.extend({
actions: {
loading: function(transition, route) {
showSpinner();
this.router.one('didTransition', function() {
hideSpinner();
});
return true; // Bubble the loading event
}
}
});
UPD1
I need to do at least what I'm doing right? Setting the model to the response?
You need to reflect the search in the URL via query params. This will let the router automatically update the model for you.
what I would put in showSpinner to affect stuff on the page (like, can I use jQuery to show or hide a spinner element?), or show the actual loading substate.
I would set a property on that page's controller:
App.IndexRoute = Ember.Route.extend({
queryParams: {
search: {
refreshModel: true
}
},
model () {
return new Ember.RSVP.Promise( resolve => setTimeout(resolve, 1000));
},
actions: {
loading (transition, route) {
this.controller.set('showSpinner', true);
this.router.one('didTransition', () => {
this.controller.set('showSpinner', false);
});
return true;
}
}
});
App.IndexController = Ember.Controller.extend({
queryParams: ['search'],
search: null,
showSpinner: false,
});
Demo: http://emberjs.jsbin.com/poxika/2/edit?html,js,output
Or you could simply put the spinner into the loading template, which will hide obsolete data:
http://emberjs.jsbin.com/poxika/3/edit?html,js,output
Or you could put your spinner into the loading template:
Just in case others want to see, here's my working code based on #lolmaus's answers.
These Docs pages were helpful as well
Route's queryParams and Find method
Controller
//app/controllers/project.js
export default Ember.ArrayController.extend({
queryParams: ['q'],
q: null,
actions: {
searchProjects: function() {
var query = this.get('queryString');
if (query) {
this.set('q', query);
}
}
}
})
Route
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params) {
if (params.q) {
return this.store.find('project', params);
} else {
return this.store.findAll('project');
}
},
queryParams: {
q: {
refreshModel: true
}
},
actions: {
loading: function(/*transition, route*/) {
var _this = this;
this.controllerFor('projects').set('showSearchSpinner', true);
this.router.one('didTransition', function() {
_this.controllerFor('projects').set('showSearchSpinner', false);
});
return true; // Bubble the loading event
}
}
});
My issue now is that when I use the parameter query, it works great, but then if I clear the query (with an action, to effectively "go back") then the records fetched by the query stay in the store, so when it does a findAll() I have both sets of records, which is not at all what I want. How do I clear out the store before doing findAll again?
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
I have some actions that take some time and I wan't to indicate that to the user by showing a loading spinner. I know realized that sometimes the longer running action is directly triggered before the loading spinner is shown:
this.set('betRound.isLoading', true);
var _this = this;
Ember.run.sync();
Ember.run.later(function(){
_this.transitionToRoute('betround.edit', _this.get('betRound.content'));
}, 50);
I tried to achieve this by delaying the transition with 50ms, but on some slower mobile devices, the loading spinner that depends on "isLoading" is not shown.
I would go with some kind of callback mechanism, where you'd put your loading spinner in a view or component and notify the controller when DOM is ready. Here's an example of a Mixin you can attach to any view or component to make it send a notice after it loads.
App.LoadAware = Ember.Mixin.create({
didInsertElement: function () {
this._super();
var target = this.get("loadedNoticeTarget");
Ember.assert("We must have loadedNoticeTarget in LoadAware component or view", target);
target.send("loadAwareLoaded");
}
});
You would then apply it like this:
App.LoadingSpinnerComponent = Ember.Component.extend(App.LoadAware);
In your template:
{{#if isLoading}}
{{loading-spinner loadedNoticeTarget=this}}
{{/if}}
And then, in your controller:
App.IndexController = Ember.Controller.extend({
actions: {
goToSlowRoute: function () {
this.set("_waitingForSpinner", true);
this.set("isLoading", true);
},
loadAwareLoaded: function () {
Ember.assert("We must be waiting on spinner at this point", this.get("_waitingForSpinner"));
this.set("_waitingForSpinner", false);
this.transitionToRoute("slowRoute");
}
},
isLoading: false
});
In this example, you would initiate transition by sending goToSlowRoute message to the controller. Full JSBin here.
I want my items in my ArrayController to listen to
FB.Event.subscribe('edge.create', function(response){
Ember.Instrumentation.instrument("facebook.like", response);
})
I'm making use of the a seperate itemController.
Like2win.ContestsController = Ember.ArrayController.extend({
itemController: "contest",
});
Like2win.ContestController = Ember.ObjectController.extend({
init: function() {
this._super();
instance = this;
Ember.subscribe("facebook.like", {
before: function(name, timestamp, payload) {
instance.send('onLike', payload);
},
after: function(name, timestamp, payload) {
//
}
})
},
For some reason only the last item in my array ends up listening to the event. I'm just starting out with Emberjs so I expect the answer to be simple.
Ember.Instrumentation is a simple software instrumentation api. It's purpose is performance profiling, tracing, not application level event dispatching.
You can see this api in action by setting Ember.STRUCTURED_PROFILE to true. This will start logging the render times for all templates rendered to the DOM by ember.
The specific issue you are having deals with how the Ember runloop works. The after hooks are only fired once with the last context given. This is done to ensure that multiple property changes of the same property do not result in re-rendering the DOM that many times. So the last property change on the runloop wins and the DOM updates with that property value.
What you really need to do is just translate the FB.Event of type edge.create into an application event that your app understands, something like `facebookLike', similar to what you have done above.
I would do this in the enter hook of your ContestRoute. Further exiting from the ContestRoute should probably unsubscribe from this event. So you probably need an unsubscribe in the exit hook.
enter: function() {
var self = this;
FB.Event.subscribe('edge.create', function(response) {
self.get('controller').send('facebookLike', response);
});
}
exit: function() {
// unsubscribe from edge.create events here
}
Then you can handle this event in your ContestController like so,
facebookLike: function(response) {
}
I want to save user progress, before user leaves a page. What is the best way to do this in Ember.js (v 1.0.0-pre.4)?
In pure JQuery it will look like:
$(window).unload(function() {
ajaxSaveUserProgress();
return true;
});
In Ember I am trying to do something like this:
Exam.TestView = Ember.View.extend({
unload: function(event){
controller.ajaxSaveUserProgress(); // call controller method
console.log('UNLOADED'+get(this, 'controller.test'));
}
});
Personally I'd put this code in the ApplicationRoute, as I believe the ApplicationRoute's setupController is only executed the once when the application is first initialised. You'll have to double-check this, but that's my understanding of it.
I've commented out the code you'll want because I've also demonstrated how the AJAX request needs to be set to synchronous, otherwise the window will close and your AJAX request won't have finished. We naturally need to wait for it to finish before the window is closed.
App.ApplicationRoute = Ember.Route.extend({
setupController: function() {
// var controller = this.controllerFor('foo');
// controller.ajaxSaveUserProgress();
jQuery(window).on('unload', function() {
jQuery.ajax({ type: 'post', async: false, url: 'foo/bar.json' });
});
}
});
Please ignore my jQuery instead of $ (Personal preference!)
Ember's got a standard way of handling this now. From the docs:
App.FormRoute = Ember.Route.extend({
actions: {
willTransition: function(transition) {
if (this.controller.get('userHasEnteredData') &&
!confirm("Are you sure you want to abandon progress?")) {
transition.abort();
} else {
// Bubble the `willTransition` action so that
// parent routes can decide whether or not to abort.
return true;
}
}
}
});